diff options
25 files changed, 715 insertions, 24 deletions
diff --git a/app/Controller/ColumnController.php b/app/Controller/ColumnController.php index d3f0e36e..8c366c6e 100644 --- a/app/Controller/ColumnController.php +++ b/app/Controller/ColumnController.php @@ -25,7 +25,7 @@ class ColumnController extends BaseController $this->response->html($this->helper->layout->project('column/index', array( 'columns' => $columns, 'project' => $project, - 'title' => t('Edit board') + 'title' => t('Edit columns') ))); } @@ -49,7 +49,6 @@ class ColumnController extends BaseController 'values' => $values, 'errors' => $errors, 'project' => $project, - 'title' => t('Add a new column') ))); } @@ -102,7 +101,6 @@ class ColumnController extends BaseController 'values' => $values ?: $column, 'project' => $project, 'column' => $column, - 'title' => t('Edit column "%s"', $column['title']) ))); } @@ -168,7 +166,6 @@ class ColumnController extends BaseController $this->response->html($this->helper->layout->project('column/remove', array( 'column' => $this->columnModel->getById($this->request->getIntegerParam('column_id')), 'project' => $project, - 'title' => t('Remove a column from a board') ))); } diff --git a/app/Controller/ColumnMoveRestrictionController.php b/app/Controller/ColumnMoveRestrictionController.php new file mode 100644 index 00000000..3f1b878f --- /dev/null +++ b/app/Controller/ColumnMoveRestrictionController.php @@ -0,0 +1,102 @@ +<?php + +namespace Kanboard\Controller; + +use Kanboard\Core\Controller\AccessForbiddenException; + +/** + * Class ColumnMoveRestrictionController + * + * @package Kanboard\Controller + * @author Frederic Guillot + */ +class ColumnMoveRestrictionController extends BaseController +{ + /** + * Show form to create a new column restriction + * + * @param array $values + * @param array $errors + * @throws AccessForbiddenException + */ + public function create(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $role_id = $this->request->getIntegerParam('role_id'); + $role = $this->projectRoleModel->getById($project['id'], $role_id); + + $this->response->html($this->template->render('column_move_restriction/create', array( + 'project' => $project, + 'role' => $role, + 'columns' => $this->columnModel->getList($project['id']), + 'values' => $values + array('project_id' => $project['id'], 'role_id' => $role['role_id']), + 'errors' => $errors, + ))); + } + + /** + * Save new column restriction + */ + public function save() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->columnMoveRestrictionValidator->validateCreation($values); + + if ($valid) { + $role_id = $this->columnMoveRestrictionModel->create( + $project['id'], + $values['role_id'], + $values['src_column_id'], + $values['dst_column_id'] + ); + + if ($role_id !== false) { + $this->flash->success(t('The column restriction has been created successfully.')); + } else { + $this->flash->failure(t('Unable to create this column restriction.')); + } + + $this->response->redirect($this->helper->url->to('ProjectRoleController', 'show', array('project_id' => $project['id']))); + } else { + $this->create($values, $errors); + } + } + + /** + * Confirm suppression + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $restriction_id = $this->request->getIntegerParam('restriction_id'); + + $this->response->html($this->helper->layout->project('column_move_restriction/remove', array( + 'project' => $project, + 'restriction' => $this->columnMoveRestrictionModel->getById($project['id'], $restriction_id), + ))); + } + + /** + * Remove a restriction + * + * @access public + */ + public function remove() + { + $project = $this->getProject(); + $this->checkCSRFParam(); + $restriction_id = $this->request->getIntegerParam('restriction_id'); + + if ($this->columnMoveRestrictionModel->remove($restriction_id)) { + $this->flash->success(t('Column restriction removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this restriction.')); + } + + $this->response->redirect($this->helper->url->to('ProjectRoleController', 'show', array('project_id' => $project['id']))); + } +} diff --git a/app/Controller/ProjectOverviewController.php b/app/Controller/ProjectOverviewController.php index abdff657..eb002936 100644 --- a/app/Controller/ProjectOverviewController.php +++ b/app/Controller/ProjectOverviewController.php @@ -23,7 +23,7 @@ class ProjectOverviewController extends BaseController 'title' => $project['name'], 'description' => $this->helper->projectHeader->getDescription($project), 'users' => $this->projectUserRoleModel->getAllUsersGroupedByRole($project['id']), - 'roles' => $this->role->getProjectRoles(), + 'roles' => $this->projectRoleModel->getList($project['id']), 'events' => $this->helper->projectActivity->getProjectEvents($project['id'], 10), 'images' => $this->projectFileModel->getAllImages($project['id']), 'files' => $this->projectFileModel->getAllDocuments($project['id']), diff --git a/app/Controller/ProjectPermissionController.php b/app/Controller/ProjectPermissionController.php index 99f556e8..1aa59c6d 100644 --- a/app/Controller/ProjectPermissionController.php +++ b/app/Controller/ProjectPermissionController.php @@ -52,7 +52,7 @@ class ProjectPermissionController extends BaseController 'project' => $project, 'users' => $this->projectUserRoleModel->getUsers($project['id']), 'groups' => $this->projectGroupRoleModel->getGroups($project['id']), - 'roles' => $this->role->getProjectRoles(), + 'roles' => $this->projectRoleModel->getList($project['id']), 'values' => $values, 'errors' => $errors, 'title' => t('Project Permissions'), diff --git a/app/Controller/ProjectRoleController.php b/app/Controller/ProjectRoleController.php new file mode 100644 index 00000000..87868748 --- /dev/null +++ b/app/Controller/ProjectRoleController.php @@ -0,0 +1,107 @@ +<?php + +namespace Kanboard\Controller; + +use Kanboard\Core\Controller\AccessForbiddenException; + +/** + * Class ProjectRoleController + * + * @package Kanboard\Controller + * @author Frederic Guillot + */ +class ProjectRoleController extends BaseController +{ + /** + * Show roles and permissions + */ + public function show() + { + $project = $this->getProject(); + + $this->response->html($this->helper->layout->project('project_role/show', array( + 'project' => $project, + 'roles' => $this->projectRoleModel->getAllWithRestrictions($project['id']), + 'title' => t('Custom Project Roles'), + ))); + } + + /** + * Show form to create new role + * + * @param array $values + * @param array $errors + * @throws AccessForbiddenException + */ + public function create(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->template->render('project_role/create', array( + 'project' => $project, + 'values' => $values + array('project_id' => $project['id']), + 'errors' => $errors, + ))); + } + + /** + * Save new role + */ + public function save() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->projectRoleValidator->validateCreation($values); + + if ($valid) { + $role_id = $this->projectRoleModel->create($project['id'], $values['role']); + + if ($role_id !== false) { + $this->flash->success(t('Your custom project role has been created successfully.')); + } else { + $this->flash->failure(t('Unable to create custom project role.')); + } + + $this->response->redirect($this->helper->url->to('ProjectRoleController', 'show', array('project_id' => $project['id']))); + } else { + $this->create($values, $errors); + } + } + + /** + * Confirm suppression + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $role_id = $this->request->getIntegerParam('role_id'); + + $this->response->html($this->helper->layout->project('project_role/remove', array( + 'project' => $project, + 'role' => $this->projectRoleModel->getById($project['id'], $role_id), + ))); + } + + /** + * Remove a custom role + * + * @access public + */ + public function remove() + { + $project = $this->getProject(); + $this->checkCSRFParam(); + $role_id = $this->request->getIntegerParam('role_id'); + + if ($this->projectRoleModel->remove($project['id'], $role_id)) { + $this->flash->success(t('Custom project role removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this project role.')); + } + + $this->response->redirect($this->helper->url->to('ProjectRoleController', 'show', array('project_id' => $project['id']))); + } +} diff --git a/app/Controller/ProjectUserOverviewController.php b/app/Controller/ProjectUserOverviewController.php index 686de830..5faf5790 100644 --- a/app/Controller/ProjectUserOverviewController.php +++ b/app/Controller/ProjectUserOverviewController.php @@ -122,9 +122,9 @@ class ProjectUserOverviewController extends BaseController { $project = $this->getProject(); - return $this->response->html($this->template->render('project_user_overview/tooltip_users', array( + $this->response->html($this->template->render('project_user_overview/tooltip_users', array( 'users' => $this->projectUserRoleModel->getAllUsersGroupedByRole($project['id']), - 'roles' => $this->role->getProjectRoles(), + 'roles' => $this->projectRoleModel->getList($project['id']), ))); } } diff --git a/app/Core/Base.php b/app/Core/Base.php index 747b1917..cadaef72 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -135,6 +135,7 @@ use Pimple\Container; * @property \Kanboard\Validator\AuthValidator $authValidator * @property \Kanboard\Validator\ColumnValidator $columnValidator * @property \Kanboard\Validator\CategoryValidator $categoryValidator + * @property \Kanboard\Validator\ColumnMoveRestrictionValidator $columnMoveRestrictionValidator * @property \Kanboard\Validator\CommentValidator $commentValidator * @property \Kanboard\Validator\CurrencyValidator $currencyValidator * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator @@ -143,6 +144,7 @@ use Pimple\Container; * @property \Kanboard\Validator\LinkValidator $linkValidator * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator * @property \Kanboard\Validator\ProjectValidator $projectValidator + * @property \Kanboard\Validator\ProjectRoleValidator $projectRoleValidator * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator * @property \Kanboard\Validator\TagValidator $tagValidator diff --git a/app/Decorator/ColumnMoveRestrictionCacheDecorator.php b/app/Decorator/ColumnMoveRestrictionCacheDecorator.php index 331bdebb..cb5e860c 100644 --- a/app/Decorator/ColumnMoveRestrictionCacheDecorator.php +++ b/app/Decorator/ColumnMoveRestrictionCacheDecorator.php @@ -42,13 +42,13 @@ class ColumnMoveRestrictionCacheDecorator * @param int $project_id * @return array|mixed */ - public function getAllSrcColumns($project_id) + public function getAllSrcColumns($project_id, $role) { - $key = $this->cachePrefix.$project_id; + $key = $this->cachePrefix.$project_id.$role; $columnIds = $this->cache->get($key); if ($columnIds === null) { - $columnIds = $this->columnMoveRestrictionModel->getAllSrcColumns($project_id); + $columnIds = $this->columnMoveRestrictionModel->getAllSrcColumns($project_id, $role); $this->cache->set($key, $columnIds); } diff --git a/app/Helper/BoardHelper.php b/app/Helper/BoardHelper.php index c3d28dc4..394d80b7 100644 --- a/app/Helper/BoardHelper.php +++ b/app/Helper/BoardHelper.php @@ -34,8 +34,14 @@ class BoardHelper extends Base public function isDraggable(array $task) { if ($task['is_active'] == 1 && $this->helper->user->hasProjectAccess('BoardViewController', 'save', $task['project_id'])) { - $srcColumnIds = $this->columnMoveRestrictionCacheDecorator->getAllSrcColumns($task['project_id']); - return ! isset($srcColumnIds[$task['column_id']]); + $role = $this->helper->user->getProjectUserRole($task['project_id']); + + if ($this->role->isCustomProjectRole($role)) { + $srcColumnIds = $this->columnMoveRestrictionCacheDecorator->getAllSrcColumns($task['project_id'], $role); + return ! isset($srcColumnIds[$task['column_id']]); + } + + return true; } return false; diff --git a/app/Model/ColumnMoveRestrictionModel.php b/app/Model/ColumnMoveRestrictionModel.php index 63e739bf..aae1a391 100644 --- a/app/Model/ColumnMoveRestrictionModel.php +++ b/app/Model/ColumnMoveRestrictionModel.php @@ -35,6 +35,35 @@ class ColumnMoveRestrictionModel extends Base } /** + * Fetch one restriction + * + * @param int $project_id + * @param int $restriction_id + * @return array|null + */ + public function getById($project_id, $restriction_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.src_column_id', + self::TABLE.'.dst_column_id', + 'pr.role', + 'sc.title as src_column_title', + 'dc.title as dst_column_title' + ) + ->left(ColumnModel::TABLE, 'sc', 'id', self::TABLE, 'src_column_id') + ->left(ColumnModel::TABLE, 'dc', 'id', self::TABLE, 'dst_column_id') + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') + ->eq(self::TABLE.'.project_id', $project_id) + ->eq(self::TABLE.'.restriction_id', $restriction_id) + ->findOne(); + } + + /** * Get all project column restrictions * * @param int $project_id @@ -45,9 +74,11 @@ class ColumnMoveRestrictionModel extends Base return $this->db ->table(self::TABLE) ->columns( - 'restriction_id', - 'src_column_id', - 'dst_column_id', + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.src_column_id', + self::TABLE.'.dst_column_id', 'pr.role', 'sc.title as src_column_title', 'dc.title as dst_column_title' @@ -61,15 +92,18 @@ class ColumnMoveRestrictionModel extends Base /** * Get all source column Ids - * - * @param int $project_id + * + * @param int $project_id + * @param string $role * @return array */ - public function getAllSrcColumns($project_id) + public function getAllSrcColumns($project_id, $role) { return $this->db ->hashtable(self::TABLE) + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') ->eq(self::TABLE.'.project_id', $project_id) + ->eq('pr.role', $role) ->getAll('src_column_id', 'src_column_id'); } diff --git a/app/Model/ProjectRoleModel.php b/app/Model/ProjectRoleModel.php index 93fe1dcc..82f22806 100644 --- a/app/Model/ProjectRoleModel.php +++ b/app/Model/ProjectRoleModel.php @@ -3,6 +3,7 @@ namespace Kanboard\Model; use Kanboard\Core\Base; +use Kanboard\Core\Security\Role; /** * Class ProjectRoleModel @@ -15,6 +16,38 @@ class ProjectRoleModel extends Base const TABLE = 'project_has_roles'; /** + * Get list of project roles + * + * @param int $project_id + * @return array + */ + public function getList($project_id) + { + $defaultRoles = $this->role->getProjectRoles(); + $customRoles = $this->db + ->hashtable(self::TABLE) + ->eq('project_id', $project_id) + ->getAll('role', 'role'); + + return $defaultRoles + $customRoles; + } + + /** + * Get a role + * + * @param int $project_id + * @param int $role_id + * @return array|null + */ + public function getById($project_id, $role_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('role_id', $role_id) + ->findOne(); + } + + /** * Get all project roles * * @param int $project_id @@ -29,6 +62,22 @@ class ProjectRoleModel extends Base } /** + * Get all project roles with restrictions + * + * @param int $project_id + * @return array + */ + public function getAllWithRestrictions($project_id) + { + $roles = $this->getAll($project_id); + $restrictions = $this->columnMoveRestrictionModel->getAll($project_id); + $restrictions = array_column_index($restrictions, 'role_id'); + array_merge_relation($roles, $restrictions, 'restrictions', 'role_id'); + + return $roles; + } + + /** * Create a new project role * * @param int $project_id @@ -73,10 +122,38 @@ class ProjectRoleModel extends Base */ public function remove($project_id, $role_id) { - return $this->db + $this->db->startTransaction(); + + $role = $this->getById($project_id, $role_id); + + $r1 = $this->db + ->table(ProjectUserRoleModel::TABLE) + ->eq('project_id', $project_id) + ->eq('role', $role['role']) + ->update(array( + 'role' => Role::PROJECT_MEMBER + )); + + $r2 = $this->db + ->table(ProjectGroupRoleModel::TABLE) + ->eq('project_id', $project_id) + ->eq('role', $role['role']) + ->update(array( + 'role' => Role::PROJECT_MEMBER + )); + + $r3 = $this->db ->table(self::TABLE) ->eq('project_id', $project_id) ->eq('role_id', $role_id) ->remove(); + + if ($r1 && $r2 && $r3) { + $this->db->closeTransaction(); + return true; + } + + $this->db->cancelTransaction(); + return false; } } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index e79ffcee..98669e6d 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -99,6 +99,7 @@ class ClassProvider implements ServiceProviderInterface 'ActionValidator', 'AuthValidator', 'CategoryValidator', + 'ColumnMoveRestrictionValidator', 'ColumnValidator', 'CommentValidator', 'CurrencyValidator', @@ -108,6 +109,7 @@ class ClassProvider implements ServiceProviderInterface 'LinkValidator', 'PasswordResetValidator', 'ProjectValidator', + 'ProjectRoleValidator', 'SubtaskValidator', 'SwimlaneValidator', 'TagValidator', diff --git a/app/Template/column/index.php b/app/Template/column/index.php index 66890ba5..8c96a350 100644 --- a/app/Template/column/index.php +++ b/app/Template/column/index.php @@ -41,9 +41,11 @@ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a> <ul> <li> + <i class="fa fa-pencil-square-o fa-fw" aria-hidden="true"></i> <?= $this->url->link(t('Edit'), 'ColumnController', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?> </li> <li> + <i class="fa fa-trash-o fa-fw" aria-hidden="true"></i> <?= $this->url->link(t('Remove'), 'ColumnController', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?> </li> </ul> diff --git a/app/Template/column/remove.php b/app/Template/column/remove.php index b231a9a7..dc0a4712 100644 --- a/app/Template/column/remove.php +++ b/app/Template/column/remove.php @@ -9,7 +9,7 @@ </p> <div class="form-actions"> - <?= $this->url->link(t('Yes'), 'ColumnController', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> + <?= $this->url->link(t('Yes'), 'ColumnController', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id']), true, 'btn btn-red') ?> <?= t('or') ?> <?= $this->url->link(t('cancel'), 'ColumnController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> </div> </div> diff --git a/app/Template/column_move_restriction/create.php b/app/Template/column_move_restriction/create.php new file mode 100644 index 00000000..69a75ce0 --- /dev/null +++ b/app/Template/column_move_restriction/create.php @@ -0,0 +1,24 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('New column restriction for the role "%s"', $role['role']) ?></h2> + </div> + <form class="popover-form" method="post" action="<?= $this->url->href('ColumnMoveRestrictionController', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('project_id', $values) ?> + <?= $this->form->hidden('role_id', $values) ?> + + <?= $this->form->label(t('Source column'), 'src_column_id') ?> + <?= $this->form->select('src_column_id', $columns, $values, $errors) ?> + + <?= $this->form->label(t('Destination column'), 'dst_column_id') ?> + <?= $this->form->select('dst_column_id', $columns, $values, $errors) ?> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'ProjectRoleController', 'show', array(), false, 'close-popover') ?> + </div> + + <p class="alert alert-info"><?= t('People belonging to this role won\'t be able to move tasks between the source and the destination column.') ?></p> + </form> +</section> diff --git a/app/Template/column_move_restriction/remove.php b/app/Template/column_move_restriction/remove.php new file mode 100644 index 00000000..1985e167 --- /dev/null +++ b/app/Template/column_move_restriction/remove.php @@ -0,0 +1,14 @@ +<div class="page-header"> + <h2><?= t('Remove a column restriction') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this column restriction: "%s" to "%s"?', $restriction['src_column_title'], $restriction['dst_column_title']) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'ColumnMoveRestrictionController', 'remove', array('project_id' => $project['id'], 'restriction_id' => $restriction['restriction_id']), true, 'btn btn-red') ?> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'ProjectRoleController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> +</div> diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php index 3be0da48..6e7fff05 100644 --- a/app/Template/project/sidebar.php +++ b/app/Template/project/sidebar.php @@ -38,6 +38,9 @@ <li <?= $this->app->checkMenuSelection('ProjectPermissionController') ?>> <?= $this->url->link(t('Permissions'), 'ProjectPermissionController', 'index', array('project_id' => $project['id'])) ?> </li> + <li <?= $this->app->checkMenuSelection('ProjectRoleController') ?>> + <?= $this->url->link(t('Custom roles'), 'ProjectRoleController', 'show', array('project_id' => $project['id'])) ?> + </li> <?php endif ?> <li <?= $this->app->checkMenuSelection('ActionController') ?>> <?= $this->url->link(t('Automatic actions'), 'ActionController', 'index', array('project_id' => $project['id'])) ?> diff --git a/app/Template/project_role/create.php b/app/Template/project_role/create.php new file mode 100644 index 00000000..8eee8f70 --- /dev/null +++ b/app/Template/project_role/create.php @@ -0,0 +1,18 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('New custom project role') ?></h2> + </div> + <form class="popover-form" method="post" action="<?= $this->url->href('ProjectRoleController', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Role'), 'role') ?> + <?= $this->form->text('role', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <div class="form-actions"> + <button type="submit" class="btn btn-blue"><?= t('Save') ?></button> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'ProjectRoleController', 'show', array(), false, 'close-popover') ?> + </div> + </form> +</section> diff --git a/app/Template/project_role/remove.php b/app/Template/project_role/remove.php new file mode 100644 index 00000000..25875e3a --- /dev/null +++ b/app/Template/project_role/remove.php @@ -0,0 +1,14 @@ +<div class="page-header"> + <h2><?= t('Remove a custom role') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this custom role: "%s"? All people assigned to this role will become project member.', $role['role']) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'ProjectRoleController', 'remove', array('project_id' => $project['id'], 'role_id' => $role['role_id']), true, 'btn btn-red') ?> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'ProjectRoleController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> +</div> diff --git a/app/Template/project_role/show.php b/app/Template/project_role/show.php new file mode 100644 index 00000000..2114a1c9 --- /dev/null +++ b/app/Template/project_role/show.php @@ -0,0 +1,58 @@ +<div class="page-header"> + <h2><?= t('Custom Project Roles') ?></h2> + <ul> + <li> + <i class="fa fa-plus fa-fw" aria-hidden="true"></i> + <?= $this->url->link(t('Add a new custom role'), 'ProjectRoleController', 'create', array('project_id' => $project['id']), false, 'popover') ?> + </li> + </ul> +</div> + +<?php if (empty($roles)): ?> + <div class="alert"><?= t('There is no custom role for this project.') ?></div> +<?php else: ?> + <?php foreach ($roles as $role): ?> + <table class="table-striped"> + <tr> + <th> + <div class="dropdown"> + <a href="#" class="dropdown-menu"><?= t('Column restrictions for the role "%s"', $role['role']) ?> <i class="fa fa-caret-down"></i></a> + <ul> + <li> + <i class="fa fa-plus fa-fw" aria-hidden="true"></i> + <?= $this->url->link(t('Add a new column restriction'), 'ColumnMoveRestrictionController', 'create', array('project_id' => $project['id'], 'role_id' => $role['role_id']), false, 'popover') ?> + </li> + <li> + <i class="fa fa-trash-o fa-fw" aria-hidden="true"></i> + <?= $this->url->link(t('Remove this role'), 'ProjectRoleController', 'confirm', array('project_id' => $project['id'], 'role_id' => $role['role_id']), false, 'popover') ?> + </li> + </ul> + </div> + </th> + <th class="column-15"> + <?= t('Actions') ?> + </th> + </tr> + <?php if (empty($role['restrictions'])): ?> + <tr> + <td colspan="2"><?= t('There is no restriction for this role.') ?></td> + </tr> + <?php else: ?> + <?php foreach ($role['restrictions'] as $restriction): ?> + <tr> + <td> + <i class="fa fa-ban fa-fw" aria-hidden="true"></i> + <?= $this->text->e($restriction['src_column_title']) ?> + <i class="fa fa-arrow-right fa-fw" aria-hidden="true"></i> + <?= $this->text->e($restriction['dst_column_title']) ?> + </td> + <td> + <i class="fa fa-trash-o fa-fw" aria-hidden="true"></i> + <?= $this->url->link(t('Remove'), 'ColumnMoveRestrictionController', 'confirm', array('project_id' => $project['id'], 'restriction_id' => $restriction['restriction_id']), false, 'popover') ?> + </td> + </tr> + <?php endforeach ?> + <?php endif ?> + </table> + <?php endforeach ?> +<?php endif ?> diff --git a/app/Validator/ColumnMoveRestrictionValidator.php b/app/Validator/ColumnMoveRestrictionValidator.php new file mode 100644 index 00000000..99769c6b --- /dev/null +++ b/app/Validator/ColumnMoveRestrictionValidator.php @@ -0,0 +1,41 @@ +<?php + +namespace Kanboard\Validator; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Class ColumnMoveRestrictionValidator + * + * @package Kanboard\Validator + * @author Frederic Guillot + */ +class ColumnMoveRestrictionValidator extends BaseValidator +{ + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('project_id', t('This field is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Required('role_id', t('This field is required')), + new Validators\Integer('role_id', t('This value must be an integer')), + new Validators\Required('src_column_id', t('This field is required')), + new Validators\Integer('src_column_id', t('This value must be an integer')), + new Validators\Required('dst_column_id', t('This field is required')), + new Validators\Integer('dst_column_id', t('This value must be an integer')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Validator/ProjectRoleValidator.php b/app/Validator/ProjectRoleValidator.php new file mode 100644 index 00000000..a4b9da15 --- /dev/null +++ b/app/Validator/ProjectRoleValidator.php @@ -0,0 +1,70 @@ +<?php + +namespace Kanboard\Validator; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Class ProjectRoleValidator + * + * @package Kanboard\Validator + * @author Frederic Guillot + */ +class ProjectRoleValidator extends BaseValidator +{ + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, $this->commonValidationRules()); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Required('role', t('This field is required')), + new Validators\MaxLength('role', t('The maximum length is %d characters', 100), 100), + new Validators\Required('project_id', t('This field is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Integer('id', t('This value must be an integer')), + ); + } +} diff --git a/app/functions.php b/app/functions.php index eaf33a52..8f0d482c 100644 --- a/app/functions.php +++ b/app/functions.php @@ -25,13 +25,13 @@ function array_merge_relation(array &$input, array &$relations, $relation, $colu * Create indexed array from a list of dict * * $input = [ - * ['k1' => 1, 'k2' => 2], ['k1' => 3, 'k2' => 4], ['k1' => 2, 'k2' => 5] + * ['k1' => 1, 'k2' => 2], ['k1' => 3, 'k2' => 4], ['k1' => 1, 'k2' => 5] * ] * * array_column_index($input, 'k1') will returns: * * [ - * 1 => [['k1' => 1, 'k2' => 2], ['k1' => 2, 'k2' => 5]], + * 1 => [['k1' => 1, 'k2' => 2], ['k1' => 1, 'k2' => 5]], * 3 => [['k1' => 3, 'k2' => 4]], * ] * diff --git a/tests/units/Model/ColumnMoveRestrictionModelTest.php b/tests/units/Model/ColumnMoveRestrictionModelTest.php index 9227012b..3bef6f99 100644 --- a/tests/units/Model/ColumnMoveRestrictionModelTest.php +++ b/tests/units/Model/ColumnMoveRestrictionModelTest.php @@ -32,6 +32,54 @@ class ColumnMoveRestrictionModelTest extends Base $this->assertTrue($columnMoveRestrictionModel->remove(1)); } + public function testGetById() + { + $projectModel = new ProjectModel($this->container); + $projectRoleModel = new ProjectRoleModel($this->container); + $columnMoveRestrictionModel = new ColumnMoveRestrictionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $projectRoleModel->create(1, 'Role A')); + $this->assertEquals(1, $columnMoveRestrictionModel->create(1, 1, 2, 3)); + + $restriction = $columnMoveRestrictionModel->getById(1, 1); + $this->assertEquals(1, $restriction['restriction_id']); + $this->assertEquals('Role A', $restriction['role']); + $this->assertEquals(1, $restriction['role_id']); + $this->assertEquals(1, $restriction['project_id']); + $this->assertEquals('Ready', $restriction['src_column_title']); + $this->assertEquals('Work in progress', $restriction['dst_column_title']); + $this->assertEquals(2, $restriction['src_column_id']); + $this->assertEquals(3, $restriction['dst_column_id']); + } + + public function testGetSrcColumns() + { + $projectModel = new ProjectModel($this->container); + $projectRoleModel = new ProjectRoleModel($this->container); + $columnMoveRestrictionModel = new ColumnMoveRestrictionModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'Test'))); + + $this->assertEquals(1, $projectRoleModel->create(1, 'Role A')); + $this->assertEquals(2, $projectRoleModel->create(1, 'Role B')); + $this->assertEquals(3, $projectRoleModel->create(2, 'Role C')); + + $this->assertEquals(1, $columnMoveRestrictionModel->create(1, 1, 2, 3)); + $this->assertEquals(2, $columnMoveRestrictionModel->create(1, 2, 3, 4)); + + $columnIds = $columnMoveRestrictionModel->getAllSrcColumns(1, 'Role A'); + $this->assertEquals(2, $columnIds[2]); + + $this->assertEmpty($columnMoveRestrictionModel->getAllSrcColumns(2, 'Role A')); + $this->assertEmpty($columnMoveRestrictionModel->getAllSrcColumns(2, 'Role C')); + + $columnIds = $columnMoveRestrictionModel->getAllSrcColumns(1, 'Role B'); + $this->assertEquals(3, $columnIds[3]); + } + public function testGetAll() { $projectModel = new ProjectModel($this->container); @@ -53,6 +101,8 @@ class ColumnMoveRestrictionModelTest extends Base $this->assertEquals(1, $restrictions[0]['restriction_id']); $this->assertEquals('Role A', $restrictions[0]['role']); + $this->assertEquals(1, $restrictions[0]['role_id']); + $this->assertEquals(1, $restrictions[0]['project_id']); $this->assertEquals('Ready', $restrictions[0]['src_column_title']); $this->assertEquals('Work in progress', $restrictions[0]['dst_column_title']); $this->assertEquals(2, $restrictions[0]['src_column_id']); diff --git a/tests/units/Model/ProjectRoleModelTest.php b/tests/units/Model/ProjectRoleModelTest.php index ce1392c2..5dd78e20 100644 --- a/tests/units/Model/ProjectRoleModelTest.php +++ b/tests/units/Model/ProjectRoleModelTest.php @@ -1,7 +1,12 @@ <?php +use Kanboard\Core\Security\Role; +use Kanboard\Model\GroupMemberModel; +use Kanboard\Model\GroupModel; +use Kanboard\Model\ProjectGroupRoleModel; use Kanboard\Model\ProjectModel; use Kanboard\Model\ProjectRoleModel; +use Kanboard\Model\ProjectUserRoleModel; require_once __DIR__.'/../Base.php'; @@ -46,14 +51,79 @@ class ProjectRoleModelTest extends Base $this->assertTrue($projectRoleModel->update(1, 1, 'Role B')); } - public function testRemove() + public function testRemoveWithUserRole() { $projectModel = new ProjectModel($this->container); $projectRoleModel = new ProjectRoleModel($this->container); + $projectUserRoleModel = new ProjectUserRoleModel($this->container); $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); $this->assertEquals(1, $projectRoleModel->create(1, 'Role A')); + $this->assertTrue($projectUserRoleModel->addUser(1, 1, 'Role A')); + $this->assertEquals('Role A', $projectUserRoleModel->getUserRole(1, 1)); + + $this->assertTrue($projectRoleModel->remove(1, 1)); + $this->assertEmpty($projectRoleModel->getAll(1)); + $this->assertEquals(Role::PROJECT_MEMBER, $projectUserRoleModel->getUserRole(1, 1)); + } + + public function testRemoveWithGroupRole() + { + $projectModel = new ProjectModel($this->container); + $projectRoleModel = new ProjectRoleModel($this->container); + $projectGroupRoleModel = new ProjectGroupRoleModel($this->container); + $groupModel = new GroupModel($this->container); + $groupMemberModel = new GroupMemberModel($this->container); + + $this->assertEquals(1, $groupModel->create('Group A')); + $this->assertTrue($groupMemberModel->addUser(1, 1)); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $projectRoleModel->create(1, 'Role A')); + $this->assertTrue($projectGroupRoleModel->addGroup(1, 1, 'Role A')); + $this->assertEquals('Role A', $projectGroupRoleModel->getUserRole(1, 1)); + $this->assertTrue($projectRoleModel->remove(1, 1)); $this->assertEmpty($projectRoleModel->getAll(1)); + $this->assertEquals(Role::PROJECT_MEMBER, $projectGroupRoleModel->getUserRole(1, 1)); + } + + public function testGetRoleListWithoutCustomRoles() + { + $projectModel = new ProjectModel($this->container); + $projectRoleModel = new ProjectRoleModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + + $roles = $projectRoleModel->getList(1); + $this->assertCount(3, $roles); + $this->assertEquals('Project Manager', $roles[Role::PROJECT_MANAGER]); + } + + public function testGetRoleListWithCustomRoles() + { + $projectModel = new ProjectModel($this->container); + $projectRoleModel = new ProjectRoleModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $projectRoleModel->create(1, 'Role A')); + + $roles = $projectRoleModel->getList(1); + $this->assertCount(4, $roles); + $this->assertEquals('Role A', $roles['Role A']); + } + + public function testGetById() + { + $projectModel = new ProjectModel($this->container); + $projectRoleModel = new ProjectRoleModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $projectRoleModel->create(1, 'Role A')); + + $role = $projectRoleModel->getById(1, 1); + $this->assertEquals(1, $role['role_id']); + $this->assertEquals(1, $role['project_id']); + $this->assertEquals('Role A', $role['role']); } } |