├── .travis.yml ├── Action ├── AssignGroup.php ├── EmailGroup.php ├── EmailGroupDue.php ├── EmailOtherAssignees.php └── EmailOtherAssigneesDue.php ├── Api └── Procedure │ └── GroupAssignTaskProcedures.php ├── Assets ├── css │ └── group_assign.css └── js │ └── group_assign.js ├── Controller ├── GroupAssignTaskCreationController.php └── GroupAssignTaskModificationController.php ├── Filter └── TaskAllAssigneeFilter.php ├── Helper ├── NewTaskHelper.php └── SizeAvatarHelperExtend.php ├── LICENSE ├── Locale ├── bg_BG │ └── translations.php ├── de_DE │ └── translations.php ├── de_DE_du │ └── translations.php ├── fr_FR │ └── translations.php ├── pt_BR │ └── translations.php └── uk_UA │ └── translations.php ├── Makefile ├── Model ├── GroupAssignCalendarModel.php ├── GroupAssignTaskDuplicationModel.php ├── GroupColorExtension.php ├── MultiselectMemberModel.php ├── MultiselectModel.php ├── NewMetaMagikSubquery.php ├── NewTaskFinderModel.php ├── NewUserNotificationFilterModel.php ├── OldMetaMagikSubquery.php ├── OldTaskFinderModel.php ├── TaskProjectMoveModel.php └── TaskRecurrenceModel.php ├── Plugin.php ├── README.md ├── Schema ├── Mysql.php ├── Postgres.php └── Sqlite.php ├── Template ├── action_creation │ └── params.php ├── board │ ├── filter.php │ ├── group.php │ └── multi.php ├── config │ └── toggle.php ├── header │ └── user_dropdown.php ├── task │ ├── changes.php │ ├── details.php │ └── multi.php ├── task_creation │ ├── show.php │ └── show_TT.php └── task_modification │ ├── show.php │ └── show_TT.php ├── Test ├── Helper │ └── NewTaskHelperTest.php ├── Model │ └── NewTaskFinderModelTest.php └── PluginTest.php └── _config.yml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | services: 5 | - postgresql 6 | - mysql 7 | 8 | php: 9 | - 7.3 10 | - 7.2 11 | 12 | env: 13 | global: 14 | - PLUGIN=Group_assign 15 | - KANBOARD_REPO=https://github.com/kanboard/kanboard.git 16 | matrix: 17 | - DB=sqlite 18 | - DB=mysql 19 | - DB=postgres 20 | 21 | matrix: 22 | fast_finish: true 23 | 24 | install: 25 | - git clone --depth 1 $KANBOARD_REPO 26 | - ln -s $TRAVIS_BUILD_DIR kanboard/plugins/$PLUGIN 27 | 28 | before_script: 29 | - cd kanboard 30 | - phpenv config-add tests/php.ini 31 | - composer install 32 | - ls -la plugins/ 33 | 34 | script: 35 | - ./vendor/bin/phpunit -c tests/units.$DB.xml plugins/$PLUGIN/Test/ 36 | -------------------------------------------------------------------------------- /Action/AssignGroup.php: -------------------------------------------------------------------------------- 1 | t('Column'), 46 | 'group_id' => t('Group'), 47 | ); 48 | } 49 | 50 | /** 51 | * Get the required parameter for the event 52 | * 53 | * @access public 54 | * @return string[] 55 | */ 56 | public function getEventRequiredParameters() 57 | { 58 | return array( 59 | 'task_id', 60 | 'task' => array( 61 | 'project_id', 62 | 'column_id', 63 | ), 64 | ); 65 | } 66 | 67 | /** 68 | * Execute the action (assign the given user) 69 | * 70 | * @access public 71 | * @param array $data Event data dictionary 72 | * @return bool True if the action was executed or false when not executed 73 | */ 74 | public function doAction(array $data) 75 | { 76 | $values = array( 77 | 'id' => $data['task_id'], 78 | 'owner_gp' => $this->getParam('group_id'), 79 | ); 80 | 81 | return $this->taskModificationModel->update($values); 82 | } 83 | 84 | /** 85 | * Check if the event data meet the action condition 86 | * 87 | * @access public 88 | * @param array $data Event data dictionary 89 | * @return bool 90 | */ 91 | public function hasRequiredCondition(array $data) 92 | { 93 | return $data['task']['column_id'] == $this->getParam('column_id'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Action/EmailGroup.php: -------------------------------------------------------------------------------- 1 | t('Column'), 31 | 'subject' => t('Email subject'), 32 | 'check_box_include_title' => t('Include Task Title and ID in subject line?'), 33 | ); 34 | } 35 | 36 | 37 | public function getEventRequiredParameters() 38 | { 39 | return array( 40 | 'task_id', 41 | 'task' => array( 42 | 'project_id', 43 | 'column_id', 44 | 'owner_id', 45 | 'owner_gp', 46 | ), 47 | ); 48 | } 49 | 50 | 51 | public function doAction(array $data) 52 | { 53 | $groupmembers = $this->groupMemberModel->getMembers($data['task']['owner_gp']); 54 | 55 | if ($this->getParam('check_box_include_title') == true) { 56 | $subject = $this->getParam('subject') . ": " . $data['task']['title'] . "(#" . $data['task_id'] . ")"; 57 | } else { 58 | $subject = $this->getParam('subject'); 59 | } 60 | 61 | if (! empty($groupmembers)) { 62 | foreach ($groupmembers as $members) { 63 | $user = $this->userModel->getById($members['id']); 64 | if (! empty($user['email'])) { 65 | $this->emailClient->send( 66 | $user['email'], 67 | $user['name'] ?: $user['username'], 68 | $subject, 69 | $this->template->render('notification/task_create', array( 70 | 'task' => $data['task'], 71 | )) 72 | ); 73 | } 74 | } 75 | return true; 76 | } 77 | 78 | return false; 79 | } 80 | 81 | 82 | 83 | public function hasRequiredCondition(array $data) 84 | { 85 | return $data['task']['column_id'] == $this->getParam('column_id'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Action/EmailGroupDue.php: -------------------------------------------------------------------------------- 1 | t('Email subject'), 45 | 'duration' => t('Duration in days'), 46 | ); 47 | } 48 | /** 49 | * Get the required parameter for the event 50 | * 51 | * @access public 52 | * @return string[] 53 | */ 54 | public function getEventRequiredParameters() 55 | { 56 | return array('tasks'); 57 | } 58 | /** 59 | * Check if the event data meet the action condition 60 | * 61 | * @access public 62 | * @param array $data Event data dictionary 63 | * @return bool 64 | */ 65 | public function hasRequiredCondition(array $data) 66 | { 67 | return count($data['tasks']) > 0; 68 | } 69 | 70 | public function doAction(array $data) 71 | { 72 | $results = array(); 73 | $max = $this->getParam('duration') * 86400; 74 | 75 | foreach ($data['tasks'] as $task) { 76 | $groupmembers = $this->groupMemberModel->getMembers($task['owner_gp']); 77 | 78 | if (! empty($groupmembers)) { 79 | foreach ($groupmembers as $members) { 80 | $user = $this->userModel->getById($members['id']); 81 | 82 | $duration = $task['date_due'] - time(); 83 | if ($task['date_due'] > 0) { 84 | if ($duration < $max) { 85 | if (! empty($user['email'])) { 86 | $results[] = $this->sendEmail($task['id'], $user); 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | return in_array(true, $results, true); 95 | } 96 | /** 97 | * Send email 98 | * 99 | * @access private 100 | * @param integer $task_id 101 | * @param array $user 102 | * @return boolean 103 | */ 104 | private function sendEmail($task_id, array $user) 105 | { 106 | $task = $this->taskFinderModel->getDetails($task_id); 107 | $this->emailClient->send( 108 | $user['email'], 109 | $user['name'] ?: $user['username'], 110 | $this->getParam('subject'), 111 | $this->template->render('notification/task_create', array('task' => $task)) 112 | ); 113 | return true; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Action/EmailOtherAssignees.php: -------------------------------------------------------------------------------- 1 | t('Column'), 30 | 'subject' => t('Email subject'), 31 | 'check_box_include_title' => t('Include Task Title and ID in subject line?'), 32 | ); 33 | } 34 | 35 | 36 | public function getEventRequiredParameters() 37 | { 38 | return array( 39 | 'task_id', 40 | 'task' => array( 41 | 'project_id', 42 | 'column_id', 43 | 'owner_id', 44 | 'owner_ms', 45 | ), 46 | ); 47 | } 48 | 49 | 50 | public function doAction(array $data) 51 | { 52 | $multimembers = $this->multiselectMemberModel->getMembers($data['task']['owner_ms']); 53 | 54 | if ($this->getParam('check_box_include_title') == true) { 55 | $subject = $this->getParam('subject') . ": " . $data['task']['title'] . "(#" . $data['task_id'] . ")"; 56 | } else { 57 | $subject = $this->getParam('subject'); 58 | } 59 | 60 | if (! empty($multimembers)) { 61 | foreach ($multimembers as $members) { 62 | $user = $this->userModel->getById($members['id']); 63 | if (! empty($user['email'])) { 64 | $this->emailClient->send( 65 | $user['email'], 66 | $user['name'] ?: $user['username'], 67 | $subject, 68 | $this->template->render('notification/task_create', array( 69 | 'task' => $data['task'], 70 | )) 71 | ); 72 | } 73 | } 74 | return true; 75 | } 76 | 77 | return false; 78 | } 79 | 80 | 81 | 82 | public function hasRequiredCondition(array $data) 83 | { 84 | return $data['task']['column_id'] == $this->getParam('column_id'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Action/EmailOtherAssigneesDue.php: -------------------------------------------------------------------------------- 1 | t('Email subject'), 45 | 'duration' => t('Duration in days'), 46 | ); 47 | } 48 | /** 49 | * Get the required parameter for the event 50 | * 51 | * @access public 52 | * @return string[] 53 | */ 54 | public function getEventRequiredParameters() 55 | { 56 | return array('tasks'); 57 | } 58 | /** 59 | * Check if the event data meet the action condition 60 | * 61 | * @access public 62 | * @param array $data Event data dictionary 63 | * @return bool 64 | */ 65 | public function hasRequiredCondition(array $data) 66 | { 67 | return count($data['tasks']) > 0; 68 | } 69 | 70 | public function doAction(array $data) 71 | { 72 | $results = array(); 73 | $max = $this->getParam('duration') * 86400; 74 | 75 | foreach ($data['tasks'] as $task) { 76 | $groupmembers = $this->multiselectMemberModel->getMembers($task['owner_ms']); 77 | 78 | if (! empty($groupmembers)) { 79 | foreach ($groupmembers as $members) { 80 | $user = $this->userModel->getById($members['id']); 81 | 82 | $duration = $task['date_due'] - time(); 83 | if ($task['date_due'] > 0) { 84 | if ($duration < $max) { 85 | if (! empty($user['email'])) { 86 | $results[] = $this->sendEmail($task['id'], $user); 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | return in_array(true, $results, true); 95 | } 96 | /** 97 | * Send email 98 | * 99 | * @access private 100 | * @param integer $task_id 101 | * @param array $user 102 | * @return boolean 103 | */ 104 | private function sendEmail($task_id, array $user) 105 | { 106 | $task = $this->taskFinderModel->getDetails($task_id); 107 | $this->emailClient->send( 108 | $user['email'], 109 | $user['name'] ?: $user['username'], 110 | $this->getParam('subject'), 111 | $this->template->render('notification/task_create', array('task' => $task)) 112 | ); 113 | return true; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Api/Procedure/GroupAssignTaskProcedures.php: -------------------------------------------------------------------------------- 1 | container)->check($this->getClassName(), 'createTaskGroupAssign', $project_id); 52 | 53 | if ($owner_id !== 0 && ! $this->projectPermissionModel->isAssignable($project_id, $owner_id)) { 54 | return false; 55 | } 56 | 57 | if ($this->userSession->isLogged()) { 58 | $creator_id = $this->userSession->getId(); 59 | } 60 | 61 | if (!empty($other_assignees)) { 62 | $ms_id = $this->multiselectModel->create(); 63 | foreach ($other_assignees as $user) { 64 | $this->multiselectMemberModel->addUser($ms_id, $user); 65 | } 66 | } 67 | 68 | 69 | $values = array( 70 | 'title' => $title, 71 | 'project_id' => $project_id, 72 | 'color_id' => $color_id, 73 | 'column_id' => $column_id, 74 | 'owner_id' => $owner_id, 75 | 'creator_id' => $creator_id, 76 | 'date_due' => $date_due, 77 | 'description' => $description, 78 | 'category_id' => $category_id, 79 | 'score' => $score, 80 | 'swimlane_id' => $swimlane_id, 81 | 'recurrence_status' => $recurrence_status, 82 | 'recurrence_trigger' => $recurrence_trigger, 83 | 'recurrence_factor' => $recurrence_factor, 84 | 'recurrence_timeframe' => $recurrence_timeframe, 85 | 'recurrence_basedate' => $recurrence_basedate, 86 | 'reference' => $reference, 87 | 'priority' => $priority, 88 | 'tags' => $tags, 89 | 'date_started' => $date_started, 90 | 'time_spent' => $time_spent, 91 | 'time_estimated' => $time_estimated, 92 | 'owner_gp' => $group_id, 93 | 'owner_ms' => (!empty($other_assignees)) ? $ms_id : 0, 94 | ); 95 | 96 | list($valid, ) = $this->taskValidator->validateCreation($values); 97 | 98 | return $valid ? $this->taskCreationModel->create($values) : false; 99 | } 100 | 101 | public function updateTaskGroupAssign( 102 | $id, 103 | $title = null, 104 | $color_id = null, 105 | $owner_id = null, 106 | $date_due = null, 107 | $description = null, 108 | $category_id = null, 109 | $score = null, 110 | $priority = null, 111 | $recurrence_status = null, 112 | $recurrence_trigger = null, 113 | $recurrence_factor = null, 114 | $recurrence_timeframe = null, 115 | $recurrence_basedate = null, 116 | $reference = null, 117 | $tags = null, 118 | $date_started = null, 119 | $time_spent = null, 120 | $time_estimated = null, 121 | $group_id = 0, 122 | array $other_assignees = array() 123 | ) 124 | { 125 | TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateTask', $id); 126 | $project_id = $this->taskFinderModel->getProjectId($id); 127 | 128 | if ($project_id === 0) { 129 | return false; 130 | } 131 | 132 | if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermissionModel->isAssignable($project_id, $owner_id)) { 133 | return false; 134 | } 135 | 136 | if (!empty($other_assignees)) { 137 | $ms_id = $this->multiselectModel->create(); 138 | foreach ($other_assignees as $user) { 139 | $this->multiselectMemberModel->addUser($ms_id, $user); 140 | } 141 | } 142 | 143 | $values = $this->filterValues(array( 144 | 'id' => $id, 145 | 'title' => $title, 146 | 'color_id' => $color_id, 147 | 'owner_id' => $owner_id, 148 | 'date_due' => $date_due, 149 | 'description' => $description, 150 | 'category_id' => $category_id, 151 | 'score' => $score, 152 | 'recurrence_status' => $recurrence_status, 153 | 'recurrence_trigger' => $recurrence_trigger, 154 | 'recurrence_factor' => $recurrence_factor, 155 | 'recurrence_timeframe' => $recurrence_timeframe, 156 | 'recurrence_basedate' => $recurrence_basedate, 157 | 'reference' => $reference, 158 | 'priority' => $priority, 159 | 'tags' => $tags, 160 | 'date_started' => $date_started, 161 | 'time_spent' => $time_spent, 162 | 'time_estimated' => $time_estimated, 163 | 'owner_gp' => $group_id, 164 | 'owner_ms' => (!empty($other_assignees)) ? $ms_id : 0, 165 | )); 166 | 167 | list($valid) = $this->taskValidator->validateApiModification($values); 168 | return $valid && $this->taskModificationModel->update($values); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Assets/css/group_assign.css: -------------------------------------------------------------------------------- 1 | .avatar-13 .avatar-letter { 2 | border-radius: 6.5px; 3 | line-height: 13px; 4 | width: 13px; 5 | font-size: 7px; 6 | } 7 | .avatar-13 img { 8 | border-radius: 6.5px; 9 | } 10 | 11 | .assigned-group { 12 | display: inline-block; 13 | margin: 3px 3px 0 0; 14 | padding: 1px 3px 1px 3px; 15 | color: #333; 16 | border-radius: 4px; 17 | } 18 | -------------------------------------------------------------------------------- /Assets/js/group_assign.js: -------------------------------------------------------------------------------- 1 | 2 | KB.on('modal.afterRender', function () { 3 | $(".group-assign-select").select2({ 4 | }); 5 | }); -------------------------------------------------------------------------------- /Controller/GroupAssignTaskCreationController.php: -------------------------------------------------------------------------------- 1 | getProject(); 31 | $swimlanesList = $this->swimlaneModel->getList($project['id'], false, true); 32 | $values += $this->prepareValues($project['is_private'], $swimlanesList); 33 | 34 | $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); 35 | $values = $this->hook->merge('controller:task-creation:form:default', $values, array('default_values' => $values)); 36 | 37 | $this->response->html($this->template->render('task_creation/show', array( 38 | 'project' => $project, 39 | 'errors' => $errors, 40 | 'values' => $values + array('project_id' => $project['id']), 41 | 'columns_list' => $this->columnModel->getList($project['id']), 42 | 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id'], true, false, $project['is_private'] == 1), 43 | 'categories_list' => $this->categoryModel->getList($project['id']), 44 | 'swimlanes_list' => $swimlanesList, 45 | ))); 46 | } 47 | 48 | /** 49 | * Validate and save a new task 50 | * 51 | * @access public 52 | */ 53 | public function save() 54 | { 55 | $project = $this->getProject(); 56 | $values = $this->request->getValues(); 57 | $values['project_id'] = $project['id']; 58 | if (isset($values['owner_ms']) && !empty($values['owner_ms'])) { 59 | $ms_id = $this->multiselectModel->create(); 60 | foreach ($values['owner_ms'] as $user) { 61 | $this->multiselectMemberModel->addUser($ms_id, $user); 62 | } 63 | unset($values['owner_ms']); 64 | $values['owner_ms'] = $ms_id; 65 | } 66 | 67 | list($valid, $errors) = $this->taskValidator->validateCreation($values); 68 | 69 | if (! $valid) { 70 | $this->flash->failure(t('Unable to create your task.')); 71 | $this->show($values, $errors); 72 | } elseif (! $this->helper->projectRole->canCreateTaskInColumn($project['id'], $values['column_id'])) { 73 | $this->flash->failure(t('You cannot create tasks in this column.')); 74 | $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true); 75 | } else { 76 | $task_id = $this->taskCreationModel->create($values); 77 | 78 | if ($task_id > 0) { 79 | $this->flash->success(t('Task created successfully.')); 80 | $this->afterSave($project, $values, $task_id); 81 | } else { 82 | $this->flash->failure(t('Unable to create this task.')); 83 | $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true); 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * Duplicate created tasks to multiple projects 90 | * 91 | * @throws PageNotFoundException 92 | */ 93 | public function duplicateProjects() 94 | { 95 | $project = $this->getProject(); 96 | $values = $this->request->getValues(); 97 | 98 | if (isset($values['project_ids'])) { 99 | foreach ($values['project_ids'] as $project_id) { 100 | $this->taskProjectDuplicationModel->duplicateToProject($values['task_id'], $project_id); 101 | } 102 | } 103 | 104 | $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true); 105 | } 106 | 107 | /** 108 | * Executed after the task is saved 109 | * 110 | * @param array $project 111 | * @param array $values 112 | * @param integer $task_id 113 | */ 114 | protected function afterSave(array $project, array &$values, $task_id) 115 | { 116 | if (isset($values['duplicate_multiple_projects']) && $values['duplicate_multiple_projects'] == 1) { 117 | $this->chooseProjects($project, $task_id); 118 | } elseif (isset($values['another_task']) && $values['another_task'] == 1) { 119 | $this->show(array( 120 | 'owner_id' => $values['owner_id'], 121 | 'color_id' => $values['color_id'], 122 | 'category_id' => isset($values['category_id']) ? $values['category_id'] : 0, 123 | 'column_id' => $values['column_id'], 124 | 'swimlane_id' => isset($values['swimlane_id']) ? $values['swimlane_id'] : 0, 125 | 'another_task' => 1, 126 | )); 127 | } else { 128 | $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true); 129 | } 130 | } 131 | 132 | /** 133 | * Prepare form values 134 | * 135 | * @access protected 136 | * @param bool $isPrivateProject 137 | * @param array $swimlanesList 138 | * @return array 139 | */ 140 | protected function prepareValues($isPrivateProject, array $swimlanesList) 141 | { 142 | $values = array( 143 | 'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanesList)), 144 | 'column_id' => $this->request->getIntegerParam('column_id'), 145 | 'color_id' => $this->colorModel->getDefaultColor(), 146 | ); 147 | 148 | if ($isPrivateProject) { 149 | $values['owner_id'] = $this->userSession->getId(); 150 | } 151 | 152 | return $values; 153 | } 154 | 155 | /** 156 | * Choose projects 157 | * 158 | * @param array $project 159 | * @param integer $task_id 160 | */ 161 | protected function chooseProjects(array $project, $task_id) 162 | { 163 | $task = $this->taskFinderModel->getById($task_id); 164 | $projects = $this->projectUserRoleModel->getActiveProjectsByUser($this->userSession->getId()); 165 | unset($projects[$project['id']]); 166 | 167 | $this->response->html($this->template->render('task_creation/duplicate_projects', array( 168 | 'project' => $project, 169 | 'task' => $task, 170 | 'projects_list' => $projects, 171 | 'values' => array('task_id' => $task['id']) 172 | ))); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Controller/GroupAssignTaskModificationController.php: -------------------------------------------------------------------------------- 1 | getTask(); 29 | $values = ['id' => $task['id'], 'owner_id' => $this->userSession->getId()]; 30 | 31 | if (! $this->helper->projectRole->canUpdateTask($task)) { 32 | throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.')); 33 | } 34 | 35 | $this->taskModificationModel->update($values); 36 | $this->redirectAfterQuickAction($task); 37 | } 38 | 39 | /** 40 | * Set the start date automatically 41 | * 42 | * @access public 43 | */ 44 | public function start() 45 | { 46 | $task = $this->getTask(); 47 | $values = ['id' => $task['id'], 'date_started' => time()]; 48 | 49 | if (! $this->helper->projectRole->canUpdateTask($task)) { 50 | throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.')); 51 | } 52 | 53 | $this->taskModificationModel->update($values); 54 | $this->redirectAfterQuickAction($task); 55 | } 56 | 57 | protected function redirectAfterQuickAction(array $task) 58 | { 59 | switch ($this->request->getStringParam('redirect')) { 60 | case 'board': 61 | $this->response->redirect($this->helper->url->to('BoardViewController', 'show', ['project_id' => $task['project_id']])); 62 | break; 63 | case 'list': 64 | $this->response->redirect($this->helper->url->to('TaskListController', 'show', ['project_id' => $task['project_id']])); 65 | break; 66 | case 'dashboard': 67 | $this->response->redirect($this->helper->url->to('DashboardController', 'show', [], 'project-tasks-'.$task['project_id'])); 68 | break; 69 | case 'dashboard-tasks': 70 | $this->response->redirect($this->helper->url->to('DashboardController', 'tasks', ['user_id' => $this->userSession->getId()])); 71 | break; 72 | default: 73 | $this->response->redirect($this->helper->url->to('TaskViewController', 'show', ['project_id' => $task['project_id'], 'task_id' => $task['id']])); 74 | } 75 | } 76 | 77 | /** 78 | * Display a form to edit a task 79 | * 80 | * @access public 81 | * @param array $values 82 | * @param array $errors 83 | * @throws \Kanboard\Core\Controller\AccessForbiddenException 84 | * @throws \Kanboard\Core\Controller\PageNotFoundException 85 | */ 86 | public function edit(array $values = array(), array $errors = array()) 87 | { 88 | $task = $this->getTask(); 89 | 90 | if (! $this->helper->projectRole->canUpdateTask($task)) { 91 | throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.')); 92 | } 93 | 94 | $project = $this->projectModel->getById($task['project_id']); 95 | 96 | if (empty($values)) { 97 | $values = $task; 98 | } 99 | 100 | $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); 101 | $values = $this->hook->merge('controller:task-modification:form:default', $values, array('default_values' => $values)); 102 | 103 | $params = array( 104 | 'project' => $project, 105 | 'values' => $values, 106 | 'errors' => $errors, 107 | 'task' => $task, 108 | 'tags' => $this->taskTagModel->getList($task['id']), 109 | 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($task['project_id']), 110 | 'categories_list' => $this->categoryModel->getList($task['project_id']), 111 | ); 112 | 113 | $this->renderTemplate($task, $params); 114 | } 115 | 116 | protected function renderTemplate(array &$task, array &$params) 117 | { 118 | if (empty($task['external_uri'])) { 119 | $this->response->html($this->template->render('task_modification/show', $params)); 120 | } else { 121 | try { 122 | $taskProvider = $this->externalTaskManager->getProvider($task['external_provider']); 123 | $params['template'] = $taskProvider->getModificationFormTemplate(); 124 | $params['external_task'] = $taskProvider->fetch($task['external_uri']); 125 | } catch (ExternalTaskAccessForbiddenException $e) { 126 | throw new AccessForbiddenException($e->getMessage()); 127 | } catch (ExternalTaskException $e) { 128 | $params['error_message'] = $e->getMessage(); 129 | } 130 | 131 | $this->response->html($this->template->render('external_task_modification/show', $params)); 132 | } 133 | } 134 | 135 | /** 136 | * Validate and update a task 137 | * 138 | * @access public 139 | */ 140 | public function update() 141 | { 142 | $previousMembers = array(); 143 | $task = $this->getTask(); 144 | $values = $this->request->getValues(); 145 | $values['id'] = $task['id']; 146 | $values['project_id'] = $task['project_id']; 147 | if (isset($values['owner_ms']) && !empty($values['owner_ms'])) { 148 | if (!empty($task['owner_ms'])) { 149 | $ms_id = $task['owner_ms']; 150 | $previousMembers = $this->multiselectMemberModel->getMembers($ms_id); 151 | $this->multiselectMemberModel->removeAllUsers($ms_id); 152 | } else { 153 | $ms_id = $this->multiselectModel->create(); 154 | } 155 | foreach ($values['owner_ms'] as $user) { 156 | if ($user !== 0) { 157 | $this->multiselectMemberModel->addUser($ms_id, $user); 158 | } 159 | } 160 | unset($values['owner_ms']); 161 | $values['owner_ms'] = $ms_id; 162 | 163 | $newMembersSet = $this->multiselectMemberModel->getMembers($values['owner_ms']); 164 | if (sort($previousMembers) !== sort($newMembersSet)) { 165 | $this->multiselectMemberModel->assigneeChanged($task, $values); 166 | } 167 | 168 | if ($values['owner_gp'] !== $task['owner_gp']) { 169 | $this->multiselectMemberModel->assigneeChanged($task, $values); 170 | } 171 | } else { 172 | $this->multiselectMemberModel->removeAllUsers($task['owner_ms']); 173 | } 174 | 175 | list($valid, $errors) = $this->taskValidator->validateModification($values); 176 | 177 | if ($valid && $this->updateTask($task, $values, $errors)) { 178 | $this->flash->success(t('Task updated successfully.')); 179 | $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); 180 | } else { 181 | $this->flash->failure(t('Unable to update your task.')); 182 | $this->edit($values, $errors); 183 | } 184 | } 185 | 186 | protected function updateTask(array &$task, array &$values, array &$errors) 187 | { 188 | if (isset($values['owner_id']) && $values['owner_id'] != $task['owner_id'] && !$this->helper->projectRole->canChangeAssignee($task)) { 189 | throw new AccessForbiddenException(t('You are not allowed to change the assignee.')); 190 | } 191 | 192 | if (! $this->helper->projectRole->canUpdateTask($task)) { 193 | throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.')); 194 | } 195 | 196 | $result = $this->taskModificationModel->update($values); 197 | 198 | if ($result && ! empty($task['external_uri'])) { 199 | try { 200 | $taskProvider = $this->externalTaskManager->getProvider($task['external_provider']); 201 | $result = $taskProvider->save($task['external_uri'], $values, $errors); 202 | } catch (ExternalTaskAccessForbiddenException $e) { 203 | throw new AccessForbiddenException($e->getMessage()); 204 | } catch (ExternalTaskException $e) { 205 | $this->logger->error($e->getMessage()); 206 | $result = false; 207 | } 208 | } 209 | 210 | return $result; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Filter/TaskAllAssigneeFilter.php: -------------------------------------------------------------------------------- 1 | db = $db; 33 | return $this; 34 | } 35 | 36 | /** 37 | * Current user id 38 | * 39 | * @access private 40 | * @var int 41 | */ 42 | private $currentUserId = 0; 43 | 44 | /** 45 | * Set current user id 46 | * 47 | * @access public 48 | * @param integer $userId 49 | * @return TaskAssigneeFilter 50 | */ 51 | public function setCurrentUserId($userId) 52 | { 53 | $this->currentUserId = $userId; 54 | return $this; 55 | } 56 | 57 | /** 58 | * Get search attribute 59 | * 60 | * @access public 61 | * @return string[] 62 | */ 63 | public function getAttributes() 64 | { 65 | return array('allassignees'); 66 | } 67 | 68 | /** 69 | * Apply filter 70 | * 71 | * @access public 72 | * @return string 73 | */ 74 | public function apply() 75 | { 76 | if (is_int($this->value) || ctype_digit($this->value)) { 77 | $this->query->beginOr(); 78 | $this->query->eq(TaskModel::TABLE.'.owner_id', $this->value); 79 | $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$this->value')"); 80 | $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$this->value')"); 81 | $this->query->closeOr(); 82 | } else { 83 | switch ($this->value) { 84 | case 'me': 85 | $this->query->beginOr(); 86 | $this->query->eq(TaskModel::TABLE.'.owner_id', $this->currentUserId); 87 | $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$this->currentUserId')"); 88 | $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$this->currentUserId')"); 89 | $this->query->closeOr(); 90 | break; 91 | case 'nobody': 92 | $this->query->eq(TaskModel::TABLE.'.owner_id', 0); 93 | $this->query->eq(TaskModel::TABLE.'.owner_gp', 0); 94 | $this->query->eq(TaskModel::TABLE.'.owner_ms', 0); 95 | break; 96 | default: 97 | $useridsarray = $this->getSubQuery()->findAllByColumn('id'); 98 | $useridstring = implode("','", $useridsarray); 99 | (!empty($useridstring)) ? $useridstring = $useridstring : $useridstring = 0; 100 | if ($useridstring == '') { 101 | $useridstring = 0; 102 | } 103 | $this->query->beginOr(); 104 | $this->query->ilike(UserModel::TABLE.'.username', '%'.$this->value.'%'); 105 | $this->query->ilike(UserModel::TABLE.'.name', '%'.$this->value.'%'); 106 | $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT id FROM `".GroupModel::TABLE."` WHERE `".GroupModel::TABLE."`.name='$this->value')"); 107 | $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id IN ('$useridstring'))"); 108 | $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id IN ('$useridstring'))"); 109 | $this->query->closeOr(); 110 | } 111 | } 112 | } 113 | public function getSubQuery() 114 | { 115 | return $this->db->table(UserModel::TABLE) 116 | ->columns( 117 | UserModel::TABLE.'.id', 118 | UserModel::TABLE.'.username', 119 | UserModel::TABLE.'.name' 120 | ) 121 | ->beginOr() 122 | ->ilike(UserModel::TABLE.'.username', '%'.$this->value.'%') 123 | ->ilike(UserModel::TABLE.'.name', '%'.$this->value.'%') 124 | ->closeOr(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Helper/NewTaskHelper.php: -------------------------------------------------------------------------------- 1 | colorModel->getList(); 22 | } 23 | 24 | public function recurrenceTriggers() 25 | { 26 | return $this->taskRecurrenceModel->getRecurrenceTriggerList(); 27 | } 28 | 29 | public function recurrenceTimeframes() 30 | { 31 | return $this->taskRecurrenceModel->getRecurrenceTimeframeList(); 32 | } 33 | 34 | public function recurrenceBasedates() 35 | { 36 | return $this->taskRecurrenceModel->getRecurrenceBasedateList(); 37 | } 38 | 39 | public function renderTitleField(array $values, array $errors) 40 | { 41 | return $this->helper->form->text( 42 | 'title', 43 | $values, 44 | $errors, 45 | array( 46 | 'autofocus', 47 | 'required', 48 | 'maxlength="200"', 49 | 'tabindex="1"', 50 | 'placeholder="'.t('Title').'"' 51 | ) 52 | ); 53 | } 54 | 55 | public function renderDescriptionField(array $values, array $errors) 56 | { 57 | return $this->helper->form->textEditor('description', $values, $errors, array('tabindex' => 2)); 58 | } 59 | 60 | public function renderDescriptionTemplateDropdown($projectId) 61 | { 62 | $templates = $this->predefinedTaskDescriptionModel->getAll($projectId); 63 | 64 | if (! empty($templates)) { 65 | $html = ''; 78 | return $html; 79 | } 80 | 81 | return ''; 82 | } 83 | 84 | public function renderTagField(array $project, array $tags = array()) 85 | { 86 | $options = $this->tagModel->getAssignableList($project['id']); 87 | 88 | $html = $this->helper->form->label(t('Tags'), 'tags[]'); 89 | $html .= ''; 90 | $html .= ''; 102 | 103 | return $html; 104 | } 105 | 106 | public function renderColorField(array $values) 107 | { 108 | $colors = $this->colorModel->getList(); 109 | $html = $this->helper->form->label(t('Color'), 'color_id'); 110 | $html .= $this->helper->form->select('color_id', $colors, $values, array(), array(), 'color-picker'); 111 | return $html; 112 | } 113 | 114 | public function renderAssigneeField(array $users, array $values, array $errors = array(), array $attributes = array()) 115 | { 116 | if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) { 117 | return ''; 118 | } 119 | 120 | $attributes = array_merge(array('tabindex="3"'), $attributes); 121 | 122 | $html = $this->helper->form->label(t('Assignee'), 'owner_id'); 123 | $html .= $this->helper->form->select('owner_id', $users, $values, $errors, $attributes); 124 | $html .= ' '; 125 | $html .= ''; 126 | $html .= ''.t('Me').''; 127 | $html .= ''; 128 | 129 | return $html; 130 | } 131 | 132 | public function renderMultiAssigneeField(array $users, array $values, array $errors = array(), array $attributes = array()) 133 | { 134 | if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) { 135 | return ''; 136 | } 137 | 138 | $attributes = array_merge(array('tabindex="4"'), $attributes); 139 | $name = 'owner_ms'; 140 | 141 | $html = $this->helper->form->label(t('Other Assignees'), $name.'[]'); 142 | 143 | $html .= ''; 171 | 172 | return $html; 173 | } 174 | 175 | public function renderGroupField(array $values, array $errors = array(), array $attributes = array()) 176 | { 177 | if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) { 178 | return ''; 179 | } 180 | $groups = $this->projectGroupRoleModel->getGroups($values['project_id']); 181 | $groupnames = array(); 182 | $groupids = array(); 183 | 184 | $groupids[] = 0; 185 | $groupnames[] = t('Unassigned'); 186 | 187 | 188 | foreach ($groups as $group) { 189 | // array_splice($groupnames, 1, 0, $group['name']); 190 | $groupnames[] = $group['name']; 191 | $groupids[] = $group['id']; 192 | } 193 | 194 | $groupvalues = array_combine($groupids, $groupnames); 195 | 196 | 197 | $attributes = array_merge(array('tabindex="4"'), $attributes); 198 | 199 | $html = $this->helper->form->label(t('Assigned Group'), 'owner_gp'); 200 | $html .= $this->helper->form->select('owner_gp', $groupvalues, $values, $errors, $attributes); 201 | $html .= ' '; 202 | 203 | return $html; 204 | } 205 | 206 | public function renderCategoryField(array $categories, array $values, array $errors = array(), array $attributes = array(), $allow_one_item = false) 207 | { 208 | $attributes = array_merge(array('tabindex="5"'), $attributes); 209 | $html = ''; 210 | 211 | if (! (! $allow_one_item && count($categories) === 1 && key($categories) == 0)) { 212 | $html .= $this->helper->form->label(t('Category'), 'category_id'); 213 | $html .= $this->helper->form->select('category_id', $categories, $values, $errors, $attributes); 214 | } 215 | 216 | return $html; 217 | } 218 | 219 | public function renderSwimlaneField(array $swimlanes, array $values, array $errors = array(), array $attributes = array()) 220 | { 221 | $attributes = array_merge(array('tabindex="6"'), $attributes); 222 | $html = ''; 223 | 224 | if (count($swimlanes) > 1) { 225 | $html .= $this->helper->form->label(t('Swimlane'), 'swimlane_id'); 226 | $html .= $this->helper->form->select('swimlane_id', $swimlanes, $values, $errors, $attributes); 227 | } 228 | 229 | return $html; 230 | } 231 | 232 | public function renderColumnField(array $columns, array $values, array $errors = array(), array $attributes = array()) 233 | { 234 | $attributes = array_merge(array('tabindex="7"'), $attributes); 235 | 236 | $html = $this->helper->form->label(t('Column'), 'column_id'); 237 | $html .= $this->helper->form->select('column_id', $columns, $values, $errors, $attributes); 238 | 239 | return $html; 240 | } 241 | 242 | public function renderPriorityField(array $project, array $values) 243 | { 244 | $range = range($project['priority_start'], $project['priority_end']); 245 | $options = array_combine($range, $range); 246 | $values += array('priority' => $project['priority_default']); 247 | 248 | $html = $this->helper->form->label(t('Priority'), 'priority'); 249 | $html .= $this->helper->form->select('priority', $options, $values, array(), array('tabindex="8"')); 250 | 251 | return $html; 252 | } 253 | 254 | public function renderScoreField(array $values, array $errors = array(), array $attributes = array()) 255 | { 256 | $attributes = array_merge(array('tabindex="13"'), $attributes); 257 | 258 | $html = $this->helper->form->label(t('Complexity'), 'score'); 259 | $html .= $this->helper->form->number('score', $values, $errors, $attributes); 260 | 261 | return $html; 262 | } 263 | 264 | public function renderReferenceField(array $values, array $errors = array(), array $attributes = array()) 265 | { 266 | $attributes = array_merge(array('tabindex="14"'), $attributes); 267 | 268 | $html = $this->helper->form->label(t('Reference'), 'reference'); 269 | $html .= $this->helper->form->text('reference', $values, $errors, $attributes, 'form-input-small'); 270 | 271 | return $html; 272 | } 273 | 274 | public function renderTimeEstimatedField(array $values, array $errors = array(), array $attributes = array()) 275 | { 276 | $attributes = array_merge(array('tabindex="11"'), $attributes); 277 | 278 | $html = $this->helper->form->label(t('Original estimate'), 'time_estimated'); 279 | $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes); 280 | $html .= ' '.t('hours'); 281 | 282 | return $html; 283 | } 284 | 285 | public function renderTimeSpentField(array $values, array $errors = array(), array $attributes = array()) 286 | { 287 | $attributes = array_merge(array('tabindex="12"'), $attributes); 288 | 289 | $html = $this->helper->form->label(t('Time spent'), 'time_spent'); 290 | $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes); 291 | $html .= ' '.t('hours'); 292 | 293 | return $html; 294 | } 295 | 296 | public function renderStartDateField(array $values, array $errors = array(), array $attributes = array()) 297 | { 298 | $attributes = array_merge(array('tabindex="10"'), $attributes); 299 | return $this->helper->form->datetime(t('Start Date'), 'date_started', $values, $errors, $attributes); 300 | } 301 | 302 | public function renderDueDateField(array $values, array $errors = array(), array $attributes = array()) 303 | { 304 | $attributes = array_merge(array('tabindex="9"'), $attributes); 305 | return $this->helper->form->datetime(t('Due Date'), 'date_due', $values, $errors, $attributes); 306 | } 307 | 308 | public function renderPriority($priority) 309 | { 310 | $html = ''; 311 | $html .= $this->helper->text->e($priority >= 0 ? 'P'.$priority : '-P'.abs($priority)); 312 | $html .= ''; 313 | 314 | return $html; 315 | } 316 | 317 | public function renderReference(array $task) 318 | { 319 | if (! empty($task['reference'])) { 320 | $reference = $this->helper->text->e($task['reference']); 321 | 322 | if (filter_var($task['reference'], FILTER_VALIDATE_URL) !== false) { 323 | return sprintf('%s', $reference, $reference); 324 | } 325 | 326 | return $reference; 327 | } 328 | 329 | return ''; 330 | } 331 | 332 | public function getProgress($task) 333 | { 334 | if (! isset($this->columns[$task['project_id']])) { 335 | $this->columns[$task['project_id']] = $this->columnModel->getList($task['project_id']); 336 | } 337 | 338 | return $this->taskModel->getProgress($task, $this->columns[$task['project_id']]); 339 | } 340 | 341 | public function getNewBoardTaskButton(array $swimlane, array $column) 342 | { 343 | $html = '
'; 344 | $providers = $this->externalTaskManager->getProviders(); 345 | 346 | if (empty($providers)) { 347 | $html .= $this->helper->modal->largeIcon( 348 | 'plus', 349 | t('Add a new task'), 350 | 'TaskCreationController', 351 | 'show', 352 | array( 353 | 'project_id' => $column['project_id'], 354 | 'column_id' => $column['id'], 355 | 'swimlane_id' => $swimlane['id'], 356 | ) 357 | ); 358 | } else { 359 | $html .= ''; 390 | } 391 | 392 | $html .= '
'; 393 | 394 | return $html; 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /Helper/SizeAvatarHelperExtend.php: -------------------------------------------------------------------------------- 1 | multiselectMemberModel->getMembers($owner_ms); 19 | $html = ""; 20 | foreach ($assignees as $assignee) { 21 | $user = $this->userModel->getById($assignee['user_id']); 22 | $html .= $this->render($assignee['user_id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css, $size); 23 | } 24 | return $html; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 creecros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Locale/bg_BG/translations.php: -------------------------------------------------------------------------------- 1 | 'Изпълнителна група', 5 | 'Assigned Group:' => 'Изпълнителна група:', 6 | 'Other Assignees' => 'Други изпълнители', 7 | 'Other Assignees:' => 'Други изпълнители:', 8 | 'Send a task by email to assigned group members' => 'Изпращане на задача по имейл до определената изпълнителна група', 9 | 'Send a task by email to the other assignees for a task.' => 'Изпращане на задача по имейл до другите изпълнители на задачата', 10 | 'Send email notification of impending due date to Group Members Assigned' => 'Изпращане на имейл известие за изтичащ краен срок до избраните членове на група', 11 | 'Send email notification of impending due date to the other assignees of a task' => 'Изпращане на имейл известие за изтичащ краен срок до другите изпълнители на задача', 12 | 'Assign the task to a specific group' => 'Присвояване на задача към специфична група', 13 | 'Action' => 'Действие', 14 | 'Action date' => 'Дата на действие', 15 | 'Add a new Kanboard task' => 'Добавяне на нова Kanboard задача', 16 | 'Add a new task' => 'Добавяне на нова задача', 17 | 'All tasks' => 'Всички задачи', 18 | 'Assignee' => 'Изпълнител', 19 | 'Assign to me' => 'Възложи на мен', 20 | 'Category' => 'Категория', 21 | 'Create another task' => 'Създай друга задача', 22 | 'Column' => 'Колона', 23 | 'Color' => 'Цвят', 24 | 'Complexity' => 'Трудност', 25 | 'Day(s)' => 'Ден(и)', 26 | 'Documentation' => 'Документация', 27 | 'Due Date' => 'Краен срок', 28 | 'Duplicate to multiple projects' => 'Дублиране към множество проекти', 29 | 'Duration in days' => 'Продължителност в дни', 30 | 'Email subject' => 'Тема на имейла', 31 | 'Enter one line per task, or leave blank to copy Task Title and create only one subtask.' => 'Въведете по един ред на задача, или оставете празно, за да копирате заглавието на задачата и да създадете само една подзадача.', 32 | 'Event' => 'Събитие', 33 | 'Existing due date' => 'Съществуващ краен срок', 34 | 'Group' => 'Група', 35 | 'Groups management' => 'Управление на групи', 36 | 'hours' => 'часове', 37 | 'Logout' => 'Изход', 38 | 'Me' => 'Аз', 39 | 'Month(s)' => 'Месец(и)', 40 | 'My dashboard' => 'Моето табло', 41 | 'My profile' => 'Моят профил', 42 | 'New assignee: %s' => 'Нов изпълнител: %s', 43 | 'New category: %s' => 'Нова категория: %s', 44 | 'New color: %s' => 'Нов цвят: %s', 45 | 'New complexity: %d' => 'Нова трудност: %d', 46 | 'New due date: ' => 'Нов краен срок: ', 47 | 'New group assigned: %s' => 'Зададена нова група: %s', 48 | 'New task' => 'Нова задача', 49 | 'New title: %s' => 'Ново заглавие: %s', 50 | 'No' => 'Не', 51 | 'not assigned' => 'не е зададено', 52 | 'Only for tasks assigned to me' => 'Само за задачи, възложени на мен', 53 | 'Only for tasks created by me' => 'Само за задачи, създадени от мен', 54 | 'Only for tasks created by me and tasks assigned to me' => 'Само за задачи, създадени от мен и възложени на мен', 55 | 'Options' => 'Опции', 56 | 'Original estimate' => 'Първоначална оценка', 57 | 'Priority' => 'Приоритет', 58 | 'Projects management' => 'Управление на проекти', 59 | 'Recurrence settings have been modified' => 'Настройките за повторение са променени', 60 | 'Reference' => 'Референция', 61 | 'Settings' => 'Настройки', 62 | 'Start Date' => 'Начална дата', 63 | 'Start date changed: ' => 'Промяна на начална дата: ', 64 | 'Swimlane' => 'Коридор', 65 | 'Tags' => 'Тагове', 66 | 'Task created successfully.' => 'Задачата е създадена успешно.', 67 | 'Task priority' => 'Приоритет на задачата', 68 | 'Task updated successfully.' => 'Задачата е актуализирана успешно.', 69 | 'Template for the task description' => 'Шаблон за описанието на задачата', 70 | 'The description has been modified:' => 'Описанието е променено:', 71 | 'The due date have been removed' => 'Крайния срок е премахнат', 72 | 'The field "%s" have been updated' => 'Полето "%s" беше актуализирано', 73 | 'The task is not assigned anymore' => 'Задачата вече не е възложена на никого', 74 | 'The task is not assigned to a group anymore' => 'Задачата вече не е възложена на група', 75 | 'The task is not assigned to multiple users anymore' => 'Задачата вече не е възложена на множество потребители', 76 | 'The task has been assigned other users' => 'Задачата е възложена на други потребители', 77 | 'There is no category now' => 'Вече няма категория', 78 | 'There is no description anymore' => 'Вече няма описание', 79 | 'Time estimated changed: %sh' => 'Променено прогнозно време: %sч', 80 | 'Time spent' => 'Отделено време', 81 | 'Time spent changed: %sh' => 'Променено отделено време: %sч', 82 | 'Title' => 'Заглавие', 83 | 'Unable to create this task.' => 'Не можете да създадете тази задача.', 84 | 'Unable to create your task.' => 'Не можете да създадете вашата задача.', 85 | 'Unable to update your task.' => 'Не можете да актуализирате вашата задача.', 86 | 'Unassigned' => 'Неприсвоен', 87 | 'Users management' => 'Управление на потребителите', 88 | 'When task is moved from first column' => 'Когато задача е преместена от първата колона', 89 | 'When task is moved to last column' => 'Когато задача е преместена в последната колона', 90 | 'When task is closed' => 'Когато задача е затворена', 91 | 'Year(s)' => 'Година(и)', 92 | 'Yes' => 'Да', 93 | 'You are not allowed to change the assignee.' => 'Не Ви е позволено да променяте изпълнителя.', 94 | 'You are not allowed to update tasks assigned to someone else.' => 'Не можете да променяте задачи с друг изпълнител.', 95 | 'You cannot create tasks in this column.' => 'Не можете да създавате задачи в тази колона.', 96 | '[DUPLICATE]' => '[ДУБЛИКАТ]', 97 | 'Enable Group Managment for Application Managers' => 'Включване на Управление на групи за Приложни управители.', 98 | 'Disable Group Managment for Application Managers' => 'Изключване на Управление на групи за Приложни управители.' 99 | ); 100 | -------------------------------------------------------------------------------- /Locale/de_DE/translations.php: -------------------------------------------------------------------------------- 1 | 'Aktion', 5 | 'Action date' => 'Datum der Aktion', 6 | 'Add a new Kanboard task' => 'Eine neue Kanboard-Aufgabe hinzufügen', 7 | 'Add a new task' => 'Eine neue Aufgabe hinzufügen', 8 | 'All tasks' => 'Alle Aufgaben', 9 | 'Assignee' => 'Zugeordneter', 10 | 'Assigned Group' => 'Zugeordnete Gruppe', 11 | 'Assigned Group:' => 'Zugeordnete Gruppe:', 12 | 'Assign to me' => 'Mir zuweisen', 13 | 'Assign the task to a specific group' => 'Aufgabe einer bestimmten Gruppe zuordnen', 14 | 'Category' => 'Kategorie', 15 | 'Create another task' => 'Andere Aufgabe erstellen', 16 | 'Column' => 'Spalte', 17 | 'Color' => 'Farbe', 18 | 'Complexity' => 'Komplexität', 19 | 'Day(s)' => 'Tag(e)', 20 | 'Documentation' => 'Dokumentation', 21 | 'Due Date' => 'Fälligkeitsdatum', 22 | 'Duplicate to multiple projects' => 'In mehrere Projekte duplizieren', 23 | 'Duration in days' => 'Dauer in Tagen', 24 | 'Email subject' => 'E-Mail-Betreff', 25 | 'Enter one line per task, or leave blank to copy Task Title and create only one subtask.' => 'Geben Sie eine Zeile pro Aufgabe ein, oder lassen Sie die Zeile leer, um den Aufgabentitel zu kopieren und nur eine Unteraufgabe zu erstellen.', 26 | 'Event' => 'Ereignis', 27 | 'Existing due date' => 'Vorhandenes Fälligkeitsdatum', 28 | 'Group' => 'Gruppe', 29 | 'Groups management' => 'Gruppen-Verwaltung', 30 | 'hours' => 'Stunden', 31 | 'Logout' => 'Abmeldung', 32 | 'Me' => 'Ich', 33 | 'Month(s)' => 'Monat(e)', 34 | 'My dashboard' => 'Mein Dashboard', 35 | 'My profile' => 'Mein Profil', 36 | 'New assignee: %s' => 'Neuer Zugewiesener: %s', 37 | 'New category: %s' => 'Neue Kategorie: %s', 38 | 'New color: %s' => 'Neue Farbe: %s', 39 | 'New complexity: %d' => 'Neue Komplexität: %d', 40 | 'New due date: ' => 'Neues Fälligkeitsdatum: ', 41 | 'New group assigned: %s' => 'Neue zugewiesene Gruppe: %s', 42 | 'New task' => 'Neue Aufgabe', 43 | 'New title: %s' => 'Neuer Titel: %s', 44 | 'No' => 'Keine', 45 | 'not assigned' => 'nicht zugewiesen', 46 | 'Only for tasks assigned to me' => 'Nur für Aufgaben, die mir zugewiesen wurden', 47 | 'Only for tasks created by me' => 'Nur für von mir erstellte Aufgaben', 48 | 'Only for tasks created by me and tasks assigned to me' => 'Nur für von mir erstellte Aufgaben und mir zugewiesene Aufgaben', 49 | 'Options' => 'Optionen', 50 | 'Original estimate' => 'Ursprüngliche Schätzung', 51 | 'Other Assignees' => 'Weitere Zuordnungen', 52 | 'Other Assignees:' => 'Weitere Zuordnungen:', 53 | 'Priority' => 'Priorität', 54 | 'Projects management' => 'Verwaltung der Projekte', 55 | 'Recurrence settings have been modified' => 'Die Wiederholungseinstellungen wurden geändert', 56 | 'Reference' => 'Referenz', 57 | 'Send a task by email to assigned group members' => 'Senden einer Aufgabe per E-Mail an zugewiesene Gruppenmitglieder', 58 | 'Send a task by email to the other assignees for a task.' => 'Aufgabe per E-Mail an die weiteren zugeordneten der Aufgabe senden', 59 | 'Send email notification of impending due date to Group Members Assigned' => 'E-Mail-Benachrichtigung über bevorstehendes Fälligkeitsdatum an die zugewiesenen Gruppenmitglieder senden', 60 | 'Send email notification of impending due date to the other assignees of a task' => 'E-Mail-Benachrichtigung über das bevorstehende Fälligkeitsdatum an die anderen Empfänger einer Aufgabe senden', 61 | 'Settings' => 'Einstellungen', 62 | 'Start Date' => 'Anfangsdatum', 63 | 'Start date changed: ' => 'Das Anfangsdatum hat sich geändert: ', 64 | 'Swimlane' => 'Swimlane', 65 | 'Tags' => 'Tags', 66 | 'Task created successfully.' => 'Aufgabe erfolgreich erstellt.', 67 | 'Task priority' => 'Priorität der Aufgabe', 68 | 'Task updated successfully.' => 'Aufgabe erfolgreich aktualisiert.', 69 | 'Template for the task description' => 'Vorlage für Aufgabenbeschreibung', 70 | 'The description has been modified:' => 'Die Beschreibung wurde geändert:', 71 | 'The due date have been removed' => 'Das Fälligkeitsdatum wurde entfernt', 72 | 'The field "%s" have been updated' => 'Das Feld "%s" wurde aktualisiert', 73 | 'The task is not assigned anymore' => 'Die Aufgabe ist nicht mehr zugewiesen', 74 | 'The task is not assigned to a group anymore' => 'Die Aufgabe ist nicht mehr einer Gruppe zugewiesen', 75 | 'The task is not assigned to multiple users anymore' => 'Die Aufgabe ist nicht mehr mehreren Benutzern zugewiesen', 76 | 'The task has been assigned other users' => 'Die Aufgabe wurde anderen Benutzern zugewiesen', 77 | 'There is no category now' => 'Es gibt derzeit keine Kategorie', 78 | 'There is no description anymore' => 'Es gibt keine Beschreibung mehr', 79 | 'Time estimated changed: %sh' => 'Geschätzte Bearbeitungsdauer geändert: %sh', 80 | 'Time spent' => 'Aufgewendete Zeit', 81 | 'Time spent changed: %sh' => 'Aufgewendete Zeit geändert: %sh', 82 | 'Title' => 'Titel', 83 | 'Unable to create this task.' => 'Diese Aufgabe kann nicht erstellt werden', 84 | 'Unable to create your task.' => 'Ihre Aufgabe kann nicht erstellt werden.', 85 | 'Unable to update your task.' => 'Diese Aufgabe kann nicht aktualisiert werden', 86 | 'Unassigned' => 'Nicht zugewiesen', 87 | 'Users management' => 'Benutzerverwaltung ', 88 | 'When task is moved from first column' => 'Wenn die Aufgabe aus der ersten Spalte heraus verschoben wird', 89 | 'When task is moved to last column' => 'Wenn die Aufgabe in die letzte Spalte verschoben wird', 90 | 'When task is closed' => 'Wenn die Aufgabe geschlossen wird', 91 | 'Year(s)' => 'Jahre', 92 | 'Yes' => 'Ja', 93 | 'You are not allowed to change the assignee.' => 'Sie dürfen die Zuweisung nicht ändern.', 94 | 'You are not allowed to update tasks assigned to someone else.' => 'Sie dürfen keine Aufgaben aktualisieren, die jemand anderem zugewiesen sind.', 95 | 'You cannot create tasks in this column.' => 'Sie können in dieser Spalte keine Aufgaben erstellen.', 96 | '[DUPLICATE]' => '[DUPLIZIERT]', 97 | ); 98 | -------------------------------------------------------------------------------- /Locale/de_DE_du/translations.php: -------------------------------------------------------------------------------- 1 | 'Zugeordnete Gruppe', 5 | 'Assigned Group:' => 'Zugeordnete Gruppe:', 6 | 'Other Assignees' => 'Weitere Zuordnungen', 7 | 'Other Assignees:' => 'Weitere Zuordnungen:', 8 | 'Send a task by email to assigned group members' => 'Senden einer Aufgabe per E-Mail an zugewiesene Gruppenmitglieder', 9 | 'Send a task by email to the other assignees for a task.' => 'Aufgabe per E-Mail an die weiteren zugeordneten der Aufgabe senden', 10 | 'Send email notification of impending due date to Group Members Assigned' => 'E-Mail-Benachrichtigung über bevorstehendes Fälligkeitsdatum an die zugewiesenen Gruppenmitglieder senden', 11 | 'Send email notification of impending due date to the other assignees of a task' => 'E-Mail-Benachrichtigung über das bevorstehende Fälligkeitsdatum an die anderen Empfänger einer Aufgabe senden', 12 | 'Assign the task to a specific group' => 'Aufgabe einer bestimmten Gruppe zuordnen' 13 | ); 14 | -------------------------------------------------------------------------------- /Locale/fr_FR/translations.php: -------------------------------------------------------------------------------- 1 | 'Groupe assigné', 5 | 'Assigned Group:' => 'Groupe assigné :', 6 | 'Other Assignees' => 'Autres assignés', 7 | 'Other Assignees:' => 'Autres assignés :', 8 | 'Send a task by email to assigned group members' => 'Envoyer une tâche par e-mail aux membres du groupe assigné', 9 | 'Send a task by email to the other assignees for a task.' => 'Envoyer une tâche par e-mail aux autres personnes assignées à la tâche', 10 | 'Send email notification of impending due date to Group Members Assigned' => 'Envoyer un e-mail de notification à l\'approche d\'une date d\'échéance aux membres du groupe assigné', 11 | 'Send email notification of impending due date to the other assignees of a task' => 'Envoyer un e-mail de notification à l\'approche d\'une date d\'échéance aux autres personnes assignées à la tâche', 12 | 'Assign the task to a specific group' => 'Assigner la tâche à un groupe spécifique' 13 | ); 14 | -------------------------------------------------------------------------------- /Locale/pt_BR/translations.php: -------------------------------------------------------------------------------- 1 | 'Ação', 5 | 'Action date' => 'Data de ação', 6 | 'Add a new Kanboard task' => 'Adicionar uma nova tarefa do Kanboard', 7 | 'Add a new task' => 'Adicionar uma nova tarefa', 8 | 'All tasks' => 'Todas as tarefas', 9 | 'Assignee' => 'Responsável', 10 | 'Assigned Group' => 'Grupo Designado', 11 | 'Assigned Group:' => 'Grupo Designado:', 12 | 'Assign to me' => 'Atribuir para mim', 13 | 'Assign the task to a specific group' => 'Atribuir uma tarefa para um grupo específico', 14 | 'Category' => 'Categoria', 15 | 'Create another task' => 'Criar outra tarefa', 16 | 'Column' => 'Coluna', 17 | 'Color' => 'Cor', 18 | 'Complexity' => 'Complexidade', 19 | 'Day(s)' => 'Dia(s)', 20 | 'Documentation' => 'Documentação', 21 | 'Due Date' => 'Data de Vencimento', 22 | 'Duplicate to multiple projects' => 'Duplicar para múltiplos projetos', 23 | 'Duration in days' => 'Duração em dias', 24 | 'Email subject' => 'Assunto do email', 25 | 'Enter one line per task, or leave blank to copy Task Title and create only one subtask.' => 'Digite uma linha por tarefa, ou deixe em branco para copiar o Título da Tarefa e criar apenas uma subtarefa.', 26 | 'Event' => 'Evento', 27 | 'Existing due date' => 'Data de vencimeto existente', 28 | 'Group' => 'Grupo', 29 | 'Groups management' => 'Gestão de grupos', 30 | 'hours' => 'horas', 31 | 'Logout' => 'Sair', 32 | 'Me' => 'Eu', 33 | 'Month(s)' => 'Mês(es)', 34 | 'My dashboard' => 'Meu painel', 35 | 'My profile' => 'Meu perfil', 36 | 'New assignee: %s' => 'Novo responsável: %s', 37 | 'New category: %s' => 'Nova categoria: %s', 38 | 'New color: %s' => 'Nova cor: %s', 39 | 'New complexity: %d' => 'Nova complexidade: %d', 40 | 'New due date: ' => 'Nova data de vencimento: ', 41 | 'New group assigned: %s' => 'Novo grupo atribuido: %s', 42 | 'New task' => 'Nova tarefa', 43 | 'New title: %s' => 'Novo título: %s', 44 | 'No' => 'Não', 45 | 'not assigned' => 'não atribuído', 46 | 'Only for tasks assigned to me' => 'Apenas tarefas atribuídas a mim', 47 | 'Only for tasks created by me' => 'Apenas tarefas criadas por mim', 48 | 'Only for tasks created by me and tasks assigned to me' => 'Apenas tarefas criadas por mim ou tarefas atribuídas a mim', 49 | 'Options' => 'Opções', 50 | 'Original estimate' => 'Estimativa original', 51 | 'Other Assignees' => 'Outros Responsáveis', 52 | 'Other Assignees:' => 'Outros Responsáveis:', 53 | 'Priority' => 'Prioridade', 54 | 'Projects management' => 'Gestão de projetos', 55 | 'Recurrence settings have been modified' => 'Configurações de recorrência foram modificadas', 56 | 'Reference' => 'Referência', 57 | 'Send a task by email to assigned group members' => 'Enviar uma tarefa por email para membros do grupo designado', 58 | 'Send a task by email to the other assignees for a task.' => 'Enviar uma tarefa por email para os outros responsáveis por uma tarefa', 59 | 'Send email notification of impending due date to Group Members Assigned' => 'Enviar email de notificação de data de vencimento iminente para os membros de grupos designados', 60 | 'Send email notification of impending due date to the other assignees of a task' => 'Enviar email de notificação de data de vencimento iminente para os outros designados por uma tarefa', 61 | 'Settings' => 'Definições', 62 | 'Start Date' => 'Data de Início', 63 | 'Start date changed: ' => 'Data de início alterada: ', 64 | 'Swimlane' => 'Raia', 65 | 'Tags' => 'Etiquetas', 66 | 'Task created successfully.' => 'Tarefa criada com sucesso.', 67 | 'Task priority' => 'Prioridade da tarefa', 68 | 'Task updated successfully.' => 'Tarefa atualizada com sucesso', 69 | 'Template for the task description' => 'Modelo para a descrição da tarefa', 70 | 'The description has been modified:' => 'A descrição foi modificada:', 71 | 'The due date have been removed' => 'A data de vencimento foi removida', 72 | 'The field "%s" have been updated' => 'O campo "%s" foi atualizado', 73 | 'The task is not assigned anymore' => 'A tarefa não está mais atribuída', 74 | 'The task is not assigned to a group anymore' => 'A tarefa não está mais atribuída para um grupo', 75 | 'The task is not assigned to multiple users anymore' => 'A tarefa não está mais atribuída para múltiplos usuários', 76 | 'The task has been assigned other users' => 'A tarefa foi atribuída para outros usuários', 77 | 'There is no category now' => 'Não há categoria agora', 78 | 'There is no description anymore' => 'Não há descrição agora', 79 | 'Time estimated changed: %sh' => 'Tempo estimado alterado: %sh', 80 | 'Time spent' => 'Tempo gasto', 81 | 'Time spent changed: %sh' => 'Tempo gasto alterado: %sh', 82 | 'Title' => 'Título', 83 | 'Unable to create this task.' => 'Incapaz de criar esta tarefa', 84 | 'Unable to create your task.' => 'Incapaz de criar a sua tarefa.', 85 | 'Unable to update your task.' => 'Incapaz de atualizar sua tarefa', 86 | 'Unassigned' => 'Não atribuido', 87 | 'Users management' => 'Gestão de usuários', 88 | 'When task is moved from first column' => 'Quando a tarefa é movida da primeira coluna', 89 | 'When task is moved to last column' => 'Quando a tarefa é movida para a última coluna', 90 | 'When task is closed' => 'Quando a tarefa é fechada', 91 | 'Year(s)' => 'Ano(s)', 92 | 'Yes' => 'Sim', 93 | 'You are not allowed to change the assignee.' => 'Você não tem permissão para mudar o responsável.', 94 | 'You are not allowed to update tasks assigned to someone else.' => 'Você não tem permissão para atualizar tarefas designadas para outras pessoas.', 95 | 'You cannot create tasks in this column.' => 'Você não pode criar tarefas nesta coluna.', 96 | '[DUPLICATE]' => '[DUPLICADO]', 97 | 'Enable Group Managment for Application Managers' => 'Ativar gerenciamento de grupo para gestores da aplicação.', 98 | 'Disable Group Managment for Application Managers' => 'Desativar gerenciamento de grupo para gestores da aplicação.', 99 | ); 100 | -------------------------------------------------------------------------------- /Locale/uk_UA/translations.php: -------------------------------------------------------------------------------- 1 | 'Призначена група', 5 | 'Assigned Group:' => 'Призначена група:', 6 | 'Other Assignees' => 'Інші відповідальні', 7 | 'Other Assignees:' => 'Інші відповідальні:', 8 | 'Send a task by email to assigned group members' => 'Надіслати завдання електронною поштою призначеним учасникам групи', 9 | 'Send a task by email to the other assignees for a task.' => 'Надіслати завдання електронною поштою іншим виконавцям завдання.', 10 | 'Send email notification of impending due date to Group Members Assigned' => 'Надіслати сповіщення електронною поштою про наближення терміну виконання призначеним членам групи', 11 | 'Send email notification of impending due date to the other assignees of a task' => 'Надіслати сповіщення електронною поштою про наближення терміну виконання іншим виконавцям завдання', 12 | 'Assign the task to a specific group' => 'Доручити завдання певній групі', 13 | 'Action' => 'Дія', 14 | 'Action date' => 'Дата дії', 15 | 'Add a new Kanboard task' => 'Додати нове завдання Kanboard', 16 | 'Add a new task' => 'Додати нове завдання', 17 | 'All tasks' => 'Всі завдання', 18 | 'Assignee' => 'Відповідальний', 19 | 'Assign to me' => 'Призначити мені', 20 | 'Category' => 'Категорія', 21 | 'Create another task' => 'Створити інше завдання', 22 | 'Column' => 'Колонка', 23 | 'Color' => 'Колір', 24 | 'Complexity' => 'Складність', 25 | 'Day(s)' => 'День(дні)', 26 | 'Documentation' => 'Документація', 27 | 'Due Date' => 'Термін виконання', 28 | 'Duplicate to multiple projects' => 'Дублювати для кількох проектів', 29 | 'Duration in days' => 'Тривалість в днях', 30 | 'Email subject' => 'Тема електронного листа', 31 | 'Enter one line per task, or leave blank to copy Task Title and create only one subtask.' => 'Введіть один рядок для кожного завдання або залиште поле порожнім, щоб скопіювати назву завдання та створити лише одне підзавдання.', 32 | 'Event' => 'Подія', 33 | 'Existing due date' => 'Наявний термін виконання', 34 | 'Group' => 'Група', 35 | 'Groups management' => 'Управління групами', 36 | 'hours' => 'години', 37 | 'Logout' => 'Вийти', 38 | 'Me' => 'Я', 39 | 'Month(s)' => 'Місяць(и)', 40 | 'My dashboard' => 'Мій профіль', 41 | 'My profile' => 'Мій профіль', 42 | 'New assignee: %s' => 'Новий відповідальний: %s', 43 | 'New category: %s' => 'Нова категорія: %s', 44 | 'New color: %s' => 'Новий колір: %s', 45 | 'New complexity: %d' => 'Нова складність: %d', 46 | 'New due date: ' => 'Новий термін виконання: ', 47 | 'New group assigned: %s' => 'Нова призначена група: %s', 48 | 'New task' => 'Нове завдання', 49 | 'New title: %s' => 'Нова назва: %s', 50 | 'No' => 'Ні', 51 | 'not assigned' => 'не призначено', 52 | 'Only for tasks assigned to me' => 'Тільки для завдань, призначених мені', 53 | 'Only for tasks created by me' => 'Тільки для завдань, створених мною', 54 | 'Only for tasks created by me and tasks assigned to me' => 'Тільки для завдань, створених мною, і завдань, призначених мені', 55 | 'Options' => 'Опції', 56 | 'Original estimate' => 'Початкова оцінка', 57 | 'Priority' => 'Пріоритет', 58 | 'Projects management' => 'Управління проєктами', 59 | 'Recurrence settings have been modified' => 'Налаштування повторення змінено', 60 | 'Reference' => 'Зовнішній ID', 61 | 'Settings' => 'Налаштування', 62 | 'Start Date' => 'Дата початку', 63 | 'Start date changed: ' => 'Дата початку змінена: ', 64 | 'Swimlane' => 'Доріжка', 65 | 'Tags' => 'Мітки', 66 | 'Task created successfully.' => 'Завдання створено успішно.', 67 | 'Task priority' => 'Пріоритет завдання', 68 | 'Task updated successfully.' => 'Завдання оновлено успішно.', 69 | 'Template for the task description' => 'Шаблон для опису завдання', 70 | 'The description has been modified:' => 'Опис змінено:', 71 | 'The due date have been removed' => 'Термін виконання видалено', 72 | 'The field "%s" have been updated' => 'Поле "%s" оновлено', 73 | 'The task is not assigned anymore' => 'Завдання більше не ставиться', 74 | 'The task is not assigned to a group anymore' => 'Завдання більше не призначається групі', 75 | 'The task is not assigned to multiple users anymore' => 'Завдання більше не призначається кільком користувачам', 76 | 'The task has been assigned other users' => 'Завдання призначено іншим користувачам', 77 | 'There is no category now' => 'Категорії зараз немає', 78 | 'There is no description anymore' => 'Опису більше немає', 79 | 'Time estimated changed: %sh' => 'Приблизний час змінено: %sг', 80 | 'Time spent' => 'Витрачений час', 81 | 'Time spent changed: %sh' => 'Витрачений час змінено: %sг', 82 | 'Title' => 'Назва', 83 | 'Unable to create this task.' => 'Не вдалося створити це завдання.', 84 | 'Unable to create your task.' => 'Не вдалося створити ваше завдання.', 85 | 'Unable to update your task.' => 'Не вдалося оновити ваше завдання.', 86 | 'Unassigned' => 'Непризначено', 87 | 'Users management' => 'Керування користувачами', 88 | 'When task is moved from first column' => 'Коли завдання переміщується з першого стовпця', 89 | 'When task is moved to last column' => 'Коли завдання переміщується в останній стовпець', 90 | 'When task is closed' => 'Коли завдання закрите', 91 | 'Year(s)' => 'рік(роки)', 92 | 'Yes' => 'Так ', 93 | 'You are not allowed to change the assignee.' => 'Ви не маєте права змінювати відповідального.', 94 | 'You are not allowed to update tasks assigned to someone else.' => 'Ви не маєте права оновлювати завдання, призначені комусь іншому.', 95 | 'You cannot create tasks in this column.' => 'Ви не можете створювати завдання в цьому стовпці.', 96 | '[DUPLICATE]' => '[ДУБЛІКАТ]', 97 | 'Enable Group Managment for Application Managers' => 'Увімкнути групове керування для менеджерів програм', 98 | 'Disable Group Managment for Application Managers' => 'Вимкнути групове керування для менеджерів програм' 99 | ); 100 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | extract = $(shell grep -A2 $(1) Plugin.php | tail -n1 | tr -d " ;'" | sed "s/return//") 5 | 6 | plugin = $(call extract, getPluginName) 7 | version = $(call extract, getPluginVersion) 8 | 9 | all: 10 | @echo "Build archive for plugin ${plugin} version=${version}" 11 | @git archive HEAD --prefix=${plugin}/ --format=zip -o ${plugin}-${version}.zip -------------------------------------------------------------------------------- /Model/GroupAssignCalendarModel.php: -------------------------------------------------------------------------------- 1 | db->table(MultiselectMemberModel::TABLE) 37 | ->eq('user_id', $user_id) 38 | ->findAllByColumn('group_id'); 39 | 40 | $getGr_Ids = $this->db->table(GroupMemberModel::TABLE) 41 | ->eq('user_id', $user_id) 42 | ->findAllByColumn('group_id'); 43 | 44 | $tasks = $this->db->table(self::TABLE) 45 | ->beginOr() 46 | ->in('owner_gp', $getGr_Ids) 47 | ->in('owner_ms', $getMS_Ids) 48 | ->closeOr() 49 | ->gte('date_due', strtotime($start)) 50 | ->lte('date_due', strtotime($end)) 51 | ->neq('is_active', 0) 52 | ->findAll(); 53 | 54 | $events = array(); 55 | 56 | foreach ($tasks as $task) { 57 | $startDate = new DateTime(); 58 | $startDate->setTimestamp($task['date_started']); 59 | 60 | $endDate = new DateTime(); 61 | $endDate->setTimestamp($task['date_due']); 62 | 63 | if ($startDate == 0) { 64 | $startDate = $endDate; 65 | } 66 | 67 | $allDay = $startDate == $endDate && $endDate->format('Hi') == '0000'; 68 | $format = $allDay ? 'Y-m-d' : 'Y-m-d\TH:i:s'; 69 | 70 | $events[] = array( 71 | 'timezoneParam' => $this->timezoneModel->getCurrentTimezone(), 72 | 'id' => $task['id'], 73 | 'title' => t('#%d', $task['id']).' '.$task['title'], 74 | 'backgroundColor' => $this->colorModel->getBackgroundColor('dark_grey'), 75 | 'borderColor' => $this->colorModel->getBorderColor($task['color_id']), 76 | 'textColor' => 'white', 77 | 'url' => $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), 78 | 'start' => $startDate->format($format), 79 | 'end' => $endDate->format($format), 80 | 'editable' => $allDay, 81 | 'allday' => $allDay, 82 | ); 83 | } 84 | 85 | return $events; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Model/GroupAssignTaskDuplicationModel.php: -------------------------------------------------------------------------------- 1 | copyFields($task_id); 68 | $values['title'] = t('[DUPLICATE]').' '.$values['title']; 69 | 70 | $new_task_id = $this->save($task_id, $values); 71 | 72 | if ($new_task_id !== false) { 73 | $this->tagDuplicationModel->duplicateTaskTags($task_id, $new_task_id); 74 | $this->taskLinkModel->create($new_task_id, $task_id, 4); 75 | } 76 | 77 | return $new_task_id; 78 | } 79 | 80 | /** 81 | * Check if the assignee and the category are available in the destination project 82 | * 83 | * @access public 84 | * @param array $values 85 | * @return array 86 | */ 87 | public function checkDestinationProjectValues(array &$values) 88 | { 89 | // Check if the assigned user is allowed for the destination project 90 | if ($values['owner_id'] > 0 && ! $this->projectPermissionModel->isUserAllowed($values['project_id'], $values['owner_id'])) { 91 | $values['owner_id'] = 0; 92 | } 93 | 94 | // Check if the category exists for the destination project 95 | if ($values['category_id'] > 0) { 96 | $values['category_id'] = $this->categoryModel->getIdByName( 97 | $values['project_id'], 98 | $this->categoryModel->getNameById($values['category_id']) 99 | ); 100 | } 101 | 102 | // Check if the swimlane exists for the destination project 103 | $values['swimlane_id'] = $this->swimlaneModel->getIdByName( 104 | $values['project_id'], 105 | $this->swimlaneModel->getNameById($values['swimlane_id']) 106 | ); 107 | 108 | if ($values['swimlane_id'] == 0) { 109 | $values['swimlane_id'] = $this->swimlaneModel->getFirstActiveSwimlaneId($values['project_id']); 110 | } 111 | 112 | // Check if the column exists for the destination project 113 | if ($values['column_id'] > 0) { 114 | $values['column_id'] = $this->columnModel->getColumnIdByTitle( 115 | $values['project_id'], 116 | $this->columnModel->getColumnTitleById($values['column_id']) 117 | ); 118 | 119 | $values['column_id'] = $values['column_id'] ?: $this->columnModel->getFirstColumnId($values['project_id']); 120 | } 121 | 122 | // Check if priority exists for destination project 123 | $values['priority'] = $this->projectTaskPriorityModel->getPriorityForProject( 124 | $values['project_id'], 125 | empty($values['priority']) ? 0 : $values['priority'] 126 | ); 127 | 128 | return $values; 129 | } 130 | 131 | /** 132 | * Duplicate fields for the new task 133 | * 134 | * @access protected 135 | * @param integer $task_id Task id 136 | * @return array 137 | */ 138 | protected function copyFields($task_id) 139 | { 140 | $task = $this->taskFinderModel->getById($task_id); 141 | $values = array(); 142 | 143 | foreach ($this->fieldsToDuplicate as $field) { 144 | $values[$field] = $task[$field]; 145 | } 146 | 147 | $ms_id = $this->multiselectModel->create(); 148 | $users_in_ms = $this->multiselectMemberModel->getMembers($values['owner_ms']); 149 | $values['owner_ms'] = $ms_id; 150 | foreach ($users_in_ms as $user) { 151 | $this->multiselectMemberModel->addUser($ms_id, $user['id']); 152 | } 153 | 154 | return $values; 155 | } 156 | 157 | /** 158 | * Create the new task and duplicate subtasks 159 | * 160 | * @access protected 161 | * @param integer $task_id Task id 162 | * @param array $values Form values 163 | * @return boolean|integer 164 | */ 165 | protected function save($task_id, array $values) 166 | { 167 | $new_task_id = $this->taskCreationModel->create($values); 168 | 169 | if ($new_task_id !== false) { 170 | $this->subtaskModel->duplicate($task_id, $new_task_id); 171 | } 172 | 173 | return $new_task_id; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Model/GroupColorExtension.php: -------------------------------------------------------------------------------- 1 | 130) { 28 | return 'black'; 29 | } else { 30 | return 'white'; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Model/MultiselectMemberModel.php: -------------------------------------------------------------------------------- 1 | db->table(self::TABLE) 36 | ->join(UserModel::TABLE, 'id', 'user_id') 37 | ->eq('group_id', $group_id); 38 | } 39 | 40 | /** 41 | * Get all users 42 | * 43 | * @access public 44 | * @param integer $group_id 45 | * @return array 46 | */ 47 | public function getMembers($group_id) 48 | { 49 | return $this->getQuery($group_id)->findAll(); 50 | } 51 | 52 | /** 53 | * Get all not members 54 | * 55 | * @access public 56 | * @param integer $group_id 57 | * @return array 58 | */ 59 | public function getNotMembers($group_id) 60 | { 61 | $subquery = $this->db->table(self::TABLE) 62 | ->columns('user_id') 63 | ->eq('group_id', $group_id); 64 | 65 | return $this->db->table(UserModel::TABLE) 66 | ->notInSubquery('id', $subquery) 67 | ->eq('is_active', 1) 68 | ->findAll(); 69 | } 70 | 71 | /** 72 | * Add user to a group 73 | * 74 | * @access public 75 | * @param integer $group_id 76 | * @param integer $user_id 77 | * @return boolean 78 | */ 79 | public function addUser($group_id, $user_id) 80 | { 81 | return $this->db->table(self::TABLE)->insert(array( 82 | 'group_id' => $group_id, 83 | 'user_id' => $user_id, 84 | )); 85 | } 86 | 87 | /** 88 | * Remove user from a group 89 | * 90 | * @access public 91 | * @param integer $group_id 92 | * @param integer $user_id 93 | * @return boolean 94 | */ 95 | public function removeUser($group_id, $user_id) 96 | { 97 | return $this->db->table(self::TABLE) 98 | ->eq('group_id', $group_id) 99 | ->eq('user_id', $user_id) 100 | ->remove(); 101 | } 102 | 103 | /** 104 | * Remove all users from a group 105 | * 106 | * @access public 107 | * @param integer $group_id 108 | * @param integer $user_id 109 | * @return boolean 110 | */ 111 | public function removeAllUsers($group_id) 112 | { 113 | return $this->db->table(self::TABLE) 114 | ->eq('group_id', $group_id) 115 | ->remove(); 116 | } 117 | 118 | /** 119 | * Check if a user is member 120 | * 121 | * @access public 122 | * @param integer $group_id 123 | * @param integer $user_id 124 | * @return boolean 125 | */ 126 | public function isMember($group_id, $user_id) 127 | { 128 | return $this->db->table(self::TABLE) 129 | ->eq('group_id', $group_id) 130 | ->eq('user_id', $user_id) 131 | ->exists(); 132 | } 133 | 134 | /** 135 | * Get all groups for a given user 136 | * 137 | * @access public 138 | * @param integer $user_id 139 | * @return array 140 | */ 141 | public function getGroups($user_id) 142 | { 143 | return $this->db->table(self::TABLE) 144 | ->columns(MultiselectModel::TABLE.'.id', MultiselectModel::TABLE.'.external_id') 145 | ->join(MultiselectModel::TABLE, 'id', 'group_id') 146 | ->eq(self::TABLE.'.user_id', $user_id) 147 | ->asc(MultiselectModel::TABLE.'.id') 148 | ->findAll(); 149 | } 150 | 151 | /** 152 | * Fire Assignee Change 153 | * 154 | * @access protected 155 | * @param array $task 156 | * @param array $changes 157 | */ 158 | public function assigneeChanged(array $task, array $changes) 159 | { 160 | $events = array(); 161 | $events[] = TaskModel::EVENT_ASSIGNEE_CHANGE; 162 | 163 | if (! empty($events)) { 164 | $this->queueManager->push( 165 | $this->taskEventJob 166 | ->withParams($task['id'], $events, $changes, array(), $task) 167 | ); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Model/MultiselectModel.php: -------------------------------------------------------------------------------- 1 | db->table(self::TABLE) 32 | ->columns('id', 'external_id') 33 | ->subquery('SELECT COUNT(*) FROM '.MultiselectMemberModel::TABLE.' WHERE group_id='.self::TABLE.'.id', 'nb_users'); 34 | } 35 | 36 | /** 37 | * Get a specific group by id 38 | * 39 | * @access public 40 | * @param integer $group_id 41 | * @return array 42 | */ 43 | public function getById($group_id) 44 | { 45 | return $this->db->table(self::TABLE)->eq('id', $group_id)->findOne(); 46 | } 47 | 48 | /** 49 | * Get a specific group by externalID 50 | * 51 | * @access public 52 | * @param string $external_id 53 | * @return array 54 | */ 55 | public function getByExternalId($external_id) 56 | { 57 | return $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOne(); 58 | } 59 | 60 | /** 61 | * Get specific groups by externalIDs 62 | * 63 | * @access public 64 | * @param string[] $external_ids 65 | * @return array 66 | */ 67 | public function getByExternalIds(array $external_ids) 68 | { 69 | if (empty($external_ids)) { 70 | return []; 71 | } 72 | 73 | return $this->db->table(self::TABLE)->in('external_id', $external_ids)->findAll(); 74 | } 75 | 76 | /** 77 | * Get all groups 78 | * 79 | * @access public 80 | * @return array 81 | */ 82 | public function getAll() 83 | { 84 | return $this->getQuery()->asc('id')->findAll(); 85 | } 86 | 87 | /** 88 | * Remove a group 89 | * 90 | * @access public 91 | * @param integer $group_id 92 | * @return boolean 93 | */ 94 | public function remove($group_id) 95 | { 96 | return $this->db->table(self::TABLE)->eq('id', $group_id)->remove(); 97 | } 98 | 99 | /** 100 | * Create a new group 101 | * 102 | * @access public 103 | * @param string $external_id 104 | * @return integer|boolean 105 | */ 106 | public function create($external_id = '') 107 | { 108 | return $this->db->table(self::TABLE)->persist(array( 109 | 'external_id' => $external_id, 110 | )); 111 | } 112 | 113 | /** 114 | * Update existing group 115 | * 116 | * @access public 117 | * @param array $values 118 | * @return boolean 119 | */ 120 | public function update(array $values) 121 | { 122 | return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); 123 | } 124 | 125 | /** 126 | * Get groupId from externalGroupId and create the group if not found 127 | * 128 | * @access public 129 | * @param string $name 130 | * @param string $external_id 131 | * @return bool|integer 132 | */ 133 | public function getOrCreateExternalGroupId($name, $external_id) 134 | { 135 | $group_id = $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOneColumn('id'); 136 | 137 | if (empty($group_id)) { 138 | $group_id = $this->create($external_id); 139 | } 140 | 141 | return $group_id; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Model/NewMetaMagikSubquery.php: -------------------------------------------------------------------------------- 1 | subquery('(SELECT COUNT(*) FROM '.self::METADATA_TABLE.' WHERE task_id=tasks.id)', 'nb_metadata'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Model/NewUserNotificationFilterModel.php: -------------------------------------------------------------------------------- 1 | t('All tasks'), 45 | self::FILTER_ASSIGNEE => t('Only for tasks assigned to me'), 46 | self::FILTER_CREATOR => t('Only for tasks created by me'), 47 | self::FILTER_BOTH => t('Only for tasks created by me and tasks assigned to me'), 48 | ); 49 | } 50 | 51 | /** 52 | * Get user selected filter 53 | * 54 | * @access public 55 | * @param integer $user_id 56 | * @return integer 57 | */ 58 | public function getSelectedFilter($user_id) 59 | { 60 | return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter'); 61 | } 62 | 63 | /** 64 | * Save selected filter for a user 65 | * 66 | * @access public 67 | * @param integer $user_id 68 | * @param string $filter 69 | * @return boolean 70 | */ 71 | public function saveFilter($user_id, $filter) 72 | { 73 | return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array( 74 | 'notifications_filter' => $filter, 75 | )); 76 | } 77 | 78 | /** 79 | * Get user selected projects 80 | * 81 | * @access public 82 | * @param integer $user_id 83 | * @return array 84 | */ 85 | public function getSelectedProjects($user_id) 86 | { 87 | return $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id'); 88 | } 89 | 90 | /** 91 | * Save selected projects for a user 92 | * 93 | * @access public 94 | * @param integer $user_id 95 | * @param integer[] $project_ids 96 | * @return boolean 97 | */ 98 | public function saveSelectedProjects($user_id, array $project_ids) 99 | { 100 | $results = array(); 101 | $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove(); 102 | 103 | foreach ($project_ids as $project_id) { 104 | $results[] = $this->db->table(self::PROJECT_TABLE)->insert(array( 105 | 'user_id' => $user_id, 106 | 'project_id' => $project_id, 107 | )); 108 | } 109 | 110 | return !in_array(false, $results, true); 111 | } 112 | 113 | /** 114 | * Return true if the user should receive notification 115 | * 116 | * @access public 117 | * @param array $user 118 | * @param array $event_data 119 | * @return boolean 120 | */ 121 | public function shouldReceiveNotification(array $user, array $event_data) 122 | { 123 | $filters = array( 124 | 'filterNone', 125 | 'filterAssignee', 126 | 'filterCreator', 127 | 'filterBoth', 128 | ); 129 | 130 | foreach ($filters as $filter) { 131 | if ($this->$filter($user, $event_data)) { 132 | return $this->filterProject($user, $event_data); 133 | } 134 | } 135 | 136 | return false; 137 | } 138 | 139 | /** 140 | * Return true if the user will receive all notifications 141 | * 142 | * @access public 143 | * @param array $user 144 | * @return boolean 145 | */ 146 | public function filterNone(array $user) 147 | { 148 | return $user['notifications_filter'] == self::FILTER_NONE; 149 | } 150 | 151 | /** 152 | * Return true if the user is the assignee and selected the filter "assignee" 153 | * 154 | * @access public 155 | * @param array $user 156 | * @param array $event_data 157 | * @return boolean 158 | */ 159 | public function filterAssignee(array $user, array $event_data) 160 | { 161 | if (!isset($event_data['task']['owner_ms'])) { 162 | $event_data['task']['owner_ms'] = 0; 163 | } 164 | if (!isset($event_data['task']['owner_gp'])) { 165 | $event_data['task']['owner_gp'] = 0; 166 | } 167 | return $user['notifications_filter'] == self::FILTER_ASSIGNEE && 168 | ($event_data['task']['owner_id'] == $user['id'] || 169 | $this->multiselectMemberModel->isMember($event_data['task']['owner_ms'], $user['id']) || 170 | $this->groupMemberModel->isMember($event_data['task']['owner_gp'], $user['id'])); 171 | } 172 | 173 | /** 174 | * Return true if the user is the creator and enabled the filter "creator" 175 | * 176 | * @access public 177 | * @param array $user 178 | * @param array $event_data 179 | * @return boolean 180 | */ 181 | public function filterCreator(array $user, array $event_data) 182 | { 183 | return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id']; 184 | } 185 | 186 | /** 187 | * Return true if the user is the assignee or the creator and selected the filter "both" 188 | * 189 | * @access public 190 | * @param array $user 191 | * @param array $event_data 192 | * @return boolean 193 | */ 194 | public function filterBoth(array $user, array $event_data) 195 | { 196 | if (!isset($event_data['task']['owner_ms'])) { 197 | $event_data['task']['owner_ms'] = 0; 198 | } 199 | if (!isset($event_data['task']['owner_gp'])) { 200 | $event_data['task']['owner_gp'] = 0; 201 | } 202 | return $user['notifications_filter'] == self::FILTER_BOTH && 203 | ($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id'] || 204 | $this->multiselectMemberModel->isMember($event_data['task']['owner_ms'], $user['id']) || 205 | $this->groupMemberModel->isMember($event_data['task']['owner_gp'], $user['id'])); 206 | } 207 | 208 | /** 209 | * Return true if the user want to receive notification for the selected project 210 | * 211 | * @access public 212 | * @param array $user 213 | * @param array $event_data 214 | * @return boolean 215 | */ 216 | public function filterProject(array $user, array $event_data) 217 | { 218 | $projects = $this->getSelectedProjects($user['id']); 219 | 220 | if (! empty($projects)) { 221 | return in_array($event_data['task']['project_id'], $projects); 222 | } 223 | 224 | return true; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Model/OldMetaMagikSubquery.php: -------------------------------------------------------------------------------- 1 | subquery('(SELECT COUNT(*) FROM '.self::METADATA_TABLE.' WHERE task_id=tasks.id)', 'nb_metadata'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Model/OldTaskFinderModel.php: -------------------------------------------------------------------------------- 1 | db 96 | ->table(TaskModel::TABLE) 97 | ->columns( 98 | TaskModel::TABLE.'.id', 99 | TaskModel::TABLE.'.title', 100 | TaskModel::TABLE.'.date_due', 101 | TaskModel::TABLE.'.date_started', 102 | TaskModel::TABLE.'.project_id', 103 | TaskModel::TABLE.'.color_id', 104 | TaskModel::TABLE.'.priority', 105 | TaskModel::TABLE.'.time_spent', 106 | TaskModel::TABLE.'.time_estimated', 107 | ProjectModel::TABLE.'.name AS project_name', 108 | ColumnModel::TABLE.'.title AS column_name', 109 | UserModel::TABLE.'.username AS assignee_username', 110 | UserModel::TABLE.'.name AS assignee_name' 111 | ) 112 | ->eq(TaskModel::TABLE.'.is_active', $is_active) 113 | ->in(ProjectModel::TABLE.'.id', $project_ids) 114 | ->join(ProjectModel::TABLE, 'id', 'project_id') 115 | ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE) 116 | ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE); 117 | } 118 | 119 | /** 120 | * Get query for assigned user tasks 121 | * 122 | * @access public 123 | * @param integer $user_id User id 124 | * @return \PicoDb\Table 125 | */ 126 | public function getUserQuery($user_id) 127 | { 128 | return $this->getExtendedQuery() 129 | ->beginOr() 130 | ->eq(TaskModel::TABLE.'.owner_id', $user_id) 131 | ->addCondition(TaskModel::TABLE.".id IN (SELECT task_id FROM ".SubtaskModel::TABLE." WHERE ".SubtaskModel::TABLE.".user_id='$user_id')") 132 | ->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$user_id')") 133 | ->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$user_id')") 134 | ->closeOr() 135 | ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN) 136 | ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE) 137 | ->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0); 138 | } 139 | 140 | /** 141 | * Extended query 142 | * 143 | * @access public 144 | * @return \PicoDb\Table 145 | */ 146 | public function getExtendedQuery() 147 | { 148 | return $this->db 149 | ->table(TaskModel::TABLE) 150 | ->columns( 151 | '(SELECT COUNT(*) FROM '.CommentModel::TABLE.' WHERE task_id=tasks.id) AS nb_comments', 152 | '(SELECT COUNT(*) FROM '.TaskFileModel::TABLE.' WHERE task_id=tasks.id) AS nb_files', 153 | '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id) AS nb_subtasks', 154 | '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks', 155 | '(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links', 156 | '(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links', 157 | '(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id AND '.TaskLinkModel::TABLE.'.link_id = 9) AS is_milestone', 158 | TaskModel::TABLE.'.id', 159 | TaskModel::TABLE.'.reference', 160 | TaskModel::TABLE.'.title', 161 | TaskModel::TABLE.'.description', 162 | TaskModel::TABLE.'.date_creation', 163 | TaskModel::TABLE.'.date_modification', 164 | TaskModel::TABLE.'.date_completed', 165 | TaskModel::TABLE.'.date_started', 166 | TaskModel::TABLE.'.date_due', 167 | TaskModel::TABLE.'.color_id', 168 | TaskModel::TABLE.'.project_id', 169 | TaskModel::TABLE.'.column_id', 170 | TaskModel::TABLE.'.swimlane_id', 171 | TaskModel::TABLE.'.owner_id', 172 | TaskModel::TABLE.'.creator_id', 173 | TaskModel::TABLE.'.position', 174 | TaskModel::TABLE.'.is_active', 175 | TaskModel::TABLE.'.score', 176 | TaskModel::TABLE.'.category_id', 177 | TaskModel::TABLE.'.priority', 178 | TaskModel::TABLE.'.date_moved', 179 | TaskModel::TABLE.'.recurrence_status', 180 | TaskModel::TABLE.'.recurrence_trigger', 181 | TaskModel::TABLE.'.recurrence_factor', 182 | TaskModel::TABLE.'.recurrence_timeframe', 183 | TaskModel::TABLE.'.recurrence_basedate', 184 | TaskModel::TABLE.'.recurrence_parent', 185 | TaskModel::TABLE.'.recurrence_child', 186 | TaskModel::TABLE.'.time_estimated', 187 | TaskModel::TABLE.'.time_spent', 188 | UserModel::TABLE.'.username AS assignee_username', 189 | UserModel::TABLE.'.name AS assignee_name', 190 | UserModel::TABLE.'.email AS assignee_email', 191 | UserModel::TABLE.'.avatar_path AS assignee_avatar_path', 192 | CategoryModel::TABLE.'.name AS category_name', 193 | CategoryModel::TABLE.'.description AS category_description', 194 | ColumnModel::TABLE.'.title AS column_name', 195 | ColumnModel::TABLE.'.position AS column_position', 196 | SwimlaneModel::TABLE.'.name AS swimlane_name', 197 | ProjectModel::TABLE.'.name AS project_name', 198 | TaskModel::TABLE.'.owner_ms', 199 | GroupModel::TABLE.'.name AS assigned_groupname' 200 | ) 201 | ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE) 202 | ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id') 203 | ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE) 204 | ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE) 205 | ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE) 206 | ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE) 207 | ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE) 208 | ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE); 209 | } 210 | 211 | /** 212 | * Get all tasks for a given project and status 213 | * 214 | * @access public 215 | * @param integer $project_id Project id 216 | * @param integer $status_id Status id 217 | * @return array 218 | */ 219 | public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN) 220 | { 221 | return $this->db 222 | ->table(TaskModel::TABLE) 223 | ->eq(TaskModel::TABLE.'.project_id', $project_id) 224 | ->eq(TaskModel::TABLE.'.is_active', $status_id) 225 | ->asc(TaskModel::TABLE.'.id') 226 | ->findAll(); 227 | } 228 | 229 | /** 230 | * Get all tasks for a given project and status 231 | * 232 | * @access public 233 | * @param integer $project_id 234 | * @param array $status 235 | * @return array 236 | */ 237 | public function getAllIds($project_id, array $status = array(TaskModel::STATUS_OPEN)) 238 | { 239 | return $this->db 240 | ->table(TaskModel::TABLE) 241 | ->eq(TaskModel::TABLE.'.project_id', $project_id) 242 | ->in(TaskModel::TABLE.'.is_active', $status) 243 | ->asc(TaskModel::TABLE.'.id') 244 | ->findAllByColumn(TaskModel::TABLE.'.id'); 245 | } 246 | 247 | /** 248 | * Get overdue tasks query 249 | * 250 | * @access public 251 | * @return \PicoDb\Table 252 | */ 253 | public function getOverdueTasksQuery() 254 | { 255 | return $this->db->table(TaskModel::TABLE) 256 | ->columns( 257 | TaskModel::TABLE.'.id', 258 | TaskModel::TABLE.'.title', 259 | TaskModel::TABLE.'.date_due', 260 | TaskModel::TABLE.'.project_id', 261 | TaskModel::TABLE.'.creator_id', 262 | TaskModel::TABLE.'.owner_id', 263 | ProjectModel::TABLE.'.name AS project_name', 264 | UserModel::TABLE.'.username AS assignee_username', 265 | UserModel::TABLE.'.name AS assignee_name' 266 | ) 267 | ->join(ProjectModel::TABLE, 'id', 'project_id') 268 | ->join(UserModel::TABLE, 'id', 'owner_id') 269 | ->eq(ProjectModel::TABLE.'.is_active', 1) 270 | ->eq(TaskModel::TABLE.'.is_active', 1) 271 | ->neq(TaskModel::TABLE.'.date_due', 0) 272 | ->lte(TaskModel::TABLE.'.date_due', time()); 273 | } 274 | 275 | /** 276 | * Get a list of overdue tasks for all projects 277 | * 278 | * @access public 279 | * @return array 280 | */ 281 | public function getOverdueTasks() 282 | { 283 | return $this->getOverdueTasksQuery()->findAll(); 284 | } 285 | 286 | /** 287 | * Get a list of overdue tasks by project 288 | * 289 | * @access public 290 | * @param integer $project_id 291 | * @return array 292 | */ 293 | public function getOverdueTasksByProject($project_id) 294 | { 295 | return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.project_id', $project_id)->findAll(); 296 | } 297 | 298 | /** 299 | * Get a list of overdue tasks by user 300 | * 301 | * @access public 302 | * @param integer $user_id 303 | * @return array 304 | */ 305 | public function getOverdueTasksByUser($user_id) 306 | { 307 | return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.owner_id', $user_id)->findAll(); 308 | } 309 | 310 | /** 311 | * Get project id for a given task 312 | * 313 | * @access public 314 | * @param integer $task_id Task id 315 | * @return integer 316 | */ 317 | public function getProjectId($task_id) 318 | { 319 | return (int) $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0; 320 | } 321 | 322 | /** 323 | * Fetch a task by the id 324 | * 325 | * @access public 326 | * @param integer $task_id Task id 327 | * @return array 328 | */ 329 | public function getById($task_id) 330 | { 331 | return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOne(); 332 | } 333 | 334 | /** 335 | * Fetch a task by the reference (external id) 336 | * 337 | * @access public 338 | * @param integer $project_id Project id 339 | * @param string $reference Task reference 340 | * @return array 341 | */ 342 | public function getByReference($project_id, $reference) 343 | { 344 | return $this->db->table(TaskModel::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne(); 345 | } 346 | 347 | /** 348 | * Get task details (fetch more information from other tables) 349 | * 350 | * @access public 351 | * @param integer $task_id Task id 352 | * @return array 353 | */ 354 | public function getDetails($task_id) 355 | { 356 | return $this->db->table(TaskModel::TABLE) 357 | ->columns( 358 | TaskModel::TABLE.'.*', 359 | CategoryModel::TABLE.'.name AS category_name', 360 | SwimlaneModel::TABLE.'.name AS swimlane_name', 361 | ProjectModel::TABLE.'.name AS project_name', 362 | ColumnModel::TABLE.'.title AS column_title', 363 | UserModel::TABLE.'.username AS assignee_username', 364 | UserModel::TABLE.'.name AS assignee_name', 365 | 'uc.username AS creator_username', 366 | 'uc.name AS creator_name', 367 | CategoryModel::TABLE.'.description AS category_description', 368 | ColumnModel::TABLE.'.position AS column_position', 369 | GroupModel::TABLE.'.name AS assigned_groupname' 370 | ) 371 | ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE) 372 | ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id') 373 | ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE) 374 | ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE) 375 | ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE) 376 | ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE) 377 | ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE) 378 | ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE) 379 | ->eq(TaskModel::TABLE.'.id', $task_id) 380 | ->findOne(); 381 | } 382 | 383 | /** 384 | * Get iCal query 385 | * 386 | * @access public 387 | * @return \PicoDb\Table 388 | */ 389 | public function getICalQuery() 390 | { 391 | return $this->db->table(TaskModel::TABLE) 392 | ->left(UserModel::TABLE, 'ua', 'id', TaskModel::TABLE, 'owner_id') 393 | ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id') 394 | ->columns( 395 | TaskModel::TABLE.'.*', 396 | 'ua.email AS assignee_email', 397 | 'ua.name AS assignee_name', 398 | 'ua.username AS assignee_username', 399 | 'uc.email AS creator_email', 400 | 'uc.name AS creator_name', 401 | 'uc.username AS creator_username' 402 | ); 403 | } 404 | 405 | /** 406 | * Count all tasks for a given project and status 407 | * 408 | * @access public 409 | * @param integer $project_id Project id 410 | * @param array $status List of status id 411 | * @return integer 412 | */ 413 | public function countByProjectId($project_id, array $status = array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED)) 414 | { 415 | return $this->db 416 | ->table(TaskModel::TABLE) 417 | ->eq('project_id', $project_id) 418 | ->in('is_active', $status) 419 | ->count(); 420 | } 421 | 422 | /** 423 | * Count the number of tasks for a given column and status 424 | * 425 | * @access public 426 | * @param integer $project_id Project id 427 | * @param integer $column_id Column id 428 | * @param array $status 429 | * @return int 430 | */ 431 | public function countByColumnId($project_id, $column_id, array $status = array(TaskModel::STATUS_OPEN)) 432 | { 433 | return $this->db 434 | ->table(TaskModel::TABLE) 435 | ->eq('project_id', $project_id) 436 | ->eq('column_id', $column_id) 437 | ->in('is_active', $status) 438 | ->count(); 439 | } 440 | 441 | /** 442 | * Count the number of tasks for a given column and swimlane 443 | * 444 | * @access public 445 | * @param integer $project_id Project id 446 | * @param integer $column_id Column id 447 | * @param integer $swimlane_id Swimlane id 448 | * @return integer 449 | */ 450 | public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id) 451 | { 452 | return $this->db 453 | ->table(TaskModel::TABLE) 454 | ->eq('project_id', $project_id) 455 | ->eq('column_id', $column_id) 456 | ->eq('swimlane_id', $swimlane_id) 457 | ->eq('is_active', 1) 458 | ->count(); 459 | } 460 | 461 | /** 462 | * Return true if the task exists 463 | * 464 | * @access public 465 | * @param integer $task_id Task id 466 | * @return boolean 467 | */ 468 | public function exists($task_id) 469 | { 470 | return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->exists(); 471 | } 472 | 473 | /** 474 | * Get project token 475 | * 476 | * @access public 477 | * @param integer $task_id 478 | * @return string 479 | */ 480 | public function getProjectToken($task_id) 481 | { 482 | return $this->db 483 | ->table(TaskModel::TABLE) 484 | ->eq(TaskModel::TABLE.'.id', $task_id) 485 | ->join(ProjectModel::TABLE, 'id', 'project_id') 486 | ->findOneColumn(ProjectModel::TABLE.'.token'); 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /Model/TaskProjectMoveModel.php: -------------------------------------------------------------------------------- 1 | taskFinderModel->getById($task_id); 36 | $values = $this->prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, $task); 37 | 38 | $this->checkDestinationProjectValues($values); 39 | $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id); 40 | 41 | // Check if the group is allowed for the destination project and unassign if not 42 | $group_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_gp'); 43 | if ($group_id > 0) { 44 | $group_in_project = $this->db 45 | ->table(ProjectGroupRoleModel::TABLE) 46 | ->eq('project_id', $project_id) 47 | ->eq('group_id', $group_id) 48 | ->exists(); 49 | if (!$group_in_project) { 50 | $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(['owner_gp' => 0]); 51 | } 52 | } 53 | 54 | // Check if the other assignees are allowed for the destination project and remove from ms group if not 55 | $ms_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_ms'); 56 | if ($ms_id > 0) { 57 | $users_in_ms = $this->multiselectMemberModel->getMembers($ms_id); 58 | foreach ($users_in_ms as $user) { 59 | if (! $this->projectPermissionModel->isAssignable($project_id, $user['id'])) { 60 | $this->multiselectMemberModel->removeUser($ms_id, $user['id']); 61 | } 62 | } 63 | } 64 | 65 | 66 | if ($this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update($values)) { 67 | $this->queueManager->push($this->taskEventJob->withParams($task_id, array(TaskModel::EVENT_MOVE_PROJECT), $values)); 68 | } 69 | 70 | return true; 71 | } 72 | 73 | /** 74 | * Prepare new task values 75 | * 76 | * @access protected 77 | * @param integer $project_id 78 | * @param integer $swimlane_id 79 | * @param integer $column_id 80 | * @param integer $category_id 81 | * @param integer $owner_id 82 | * @param array $task 83 | * @return array 84 | */ 85 | protected function prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, array $task) 86 | { 87 | $values = array(); 88 | $values['is_active'] = 1; 89 | $values['project_id'] = $project_id; 90 | $values['column_id'] = $column_id !== null ? $column_id : $task['column_id']; 91 | $values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1; 92 | $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id']; 93 | $values['category_id'] = $category_id !== null ? $category_id : $task['category_id']; 94 | $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id']; 95 | $values['priority'] = $task['priority']; 96 | return $values; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Model/TaskRecurrenceModel.php: -------------------------------------------------------------------------------- 1 | t('No'), 33 | TaskModel::RECURRING_STATUS_PENDING => t('Yes'), 34 | ); 35 | } 36 | 37 | /** 38 | * Return the list recurrence triggers 39 | * 40 | * @access public 41 | * @return array 42 | */ 43 | public function getRecurrenceTriggerList() 44 | { 45 | return array( 46 | TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'), 47 | TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'), 48 | TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'), 49 | ); 50 | } 51 | 52 | /** 53 | * Return the list options to calculate recurrence due date 54 | * 55 | * @access public 56 | * @return array 57 | */ 58 | public function getRecurrenceBasedateList() 59 | { 60 | return array( 61 | TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'), 62 | TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'), 63 | ); 64 | } 65 | 66 | /** 67 | * Return the list recurrence timeframes 68 | * 69 | * @access public 70 | * @return array 71 | */ 72 | public function getRecurrenceTimeframeList() 73 | { 74 | return array( 75 | TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'), 76 | TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'), 77 | TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'), 78 | ); 79 | } 80 | 81 | /** 82 | * Duplicate recurring task 83 | * 84 | * @access public 85 | * @param integer $task_id Task id 86 | * @return boolean|integer Recurrence task id 87 | */ 88 | public function duplicateRecurringTask($task_id) 89 | { 90 | $values = $this->copyFields($task_id); 91 | 92 | if ($values['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) { 93 | $values['recurrence_parent'] = $task_id; 94 | $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']); 95 | $this->calculateRecurringTaskDueDate($values); 96 | 97 | $recurring_task_id = $this->save($task_id, $values); 98 | 99 | if ($recurring_task_id !== false) { 100 | $this->tagDuplicationModel->duplicateTaskTags($task_id, $recurring_task_id); 101 | 102 | $parent_update = $this->db 103 | ->table(TaskModel::TABLE) 104 | ->eq('id', $task_id) 105 | ->update(array( 106 | 'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED, 107 | 'recurrence_child' => $recurring_task_id, 108 | )); 109 | 110 | if ($parent_update) { 111 | return $recurring_task_id; 112 | } 113 | } 114 | } 115 | 116 | return false; 117 | } 118 | 119 | /** 120 | * Calculate new due date for new recurrence task 121 | * 122 | * @access public 123 | * @param array $values Task fields 124 | */ 125 | public function calculateRecurringTaskDueDate(array &$values) 126 | { 127 | if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) { 128 | if ($values['recurrence_basedate'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) { 129 | $values['date_due'] = time(); 130 | } 131 | 132 | $factor = abs($values['recurrence_factor']); 133 | $subtract = $values['recurrence_factor'] < 0; 134 | 135 | switch ($values['recurrence_timeframe']) { 136 | case TaskModel::RECURRING_TIMEFRAME_MONTHS: 137 | $interval = 'P' . $factor . 'M'; 138 | break; 139 | case TaskModel::RECURRING_TIMEFRAME_YEARS: 140 | $interval = 'P' . $factor . 'Y'; 141 | break; 142 | default: 143 | $interval = 'P' . $factor . 'D'; 144 | } 145 | 146 | $date_due = new DateTime(); 147 | $date_due->setTimestamp($values['date_due']); 148 | 149 | $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval)); 150 | 151 | $values['date_due'] = $date_due->getTimestamp(); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | template->setTemplateOverride('task/changes', 'group_assign:task/changes'); 39 | 40 | //Notifications 41 | $this->container['userNotificationFilterModel'] = $this->container->factory(function ($c) { 42 | return new NewUserNotificationFilterModel($c); 43 | }); 44 | 45 | //Helpers 46 | $this->helper->register('newTaskHelper', '\Kanboard\Plugin\Group_assign\Helper\NewTaskHelper'); 47 | $this->helper->register('sizeAvatarHelperExtend', '\Kanboard\Plugin\Group_assign\Helper\SizeAvatarHelperExtend'); 48 | 49 | //Models and backward compatibility 50 | $load_new_classes = true; 51 | $applications_version = str_replace('v', '', APP_VERSION); 52 | 53 | if (strpos(APP_VERSION, 'master') !== false || strpos(APP_VERSION, 'main') !== false) { 54 | if (file_exists('ChangeLog')) { 55 | $applications_version = trim(file_get_contents('ChangeLog', false, null, 8, 6), ' '); 56 | $clean_appversion = preg_replace('/\s+/', '', $applications_version); 57 | $load_new_classes = version_compare($clean_appversion, '1.2.6', '<'); 58 | } 59 | } else { 60 | $load_new_classes = version_compare($applications_version, '1.2.5', '>'); 61 | } 62 | 63 | if ($load_new_classes) { 64 | if (file_exists('plugins/MetaMagik')) { 65 | $this->container['taskFinderModel'] = $this->container->factory(function ($c) { 66 | return new NewMetaMagikSubquery($c); 67 | }); 68 | } else { 69 | $this->container['taskFinderModel'] = $this->container->factory(function ($c) { 70 | return new NewTaskFinderModel($c); 71 | }); 72 | } 73 | $this->container['taskDuplicationModel'] = $this->container->factory(function ($c) { 74 | return new GroupAssignTaskDuplicationModel($c); 75 | }); 76 | $this->container['taskProjectMoveModel '] = $this->container->factory(function ($c) { 77 | return new TaskProjectMoveModel($c); 78 | }); 79 | $this->container['taskRecurrenceModel '] = $this->container->factory(function ($c) { 80 | return new TaskRecurrenceModel($c); 81 | }); 82 | } else { 83 | if (file_exists('plugins/MetaMagik')) { 84 | $this->container['taskFinderModel'] = $this->container->factory(function ($c) { 85 | return new OldMetaMagikSubquery($c); 86 | }); 87 | } else { 88 | $this->container['taskFinderModel'] = $this->container->factory(function ($c) { 89 | return new OldTaskFinderModel($c); 90 | }); 91 | } 92 | $this->container['taskDuplicationModel'] = $this->container->factory(function ($c) { 93 | return new GroupAssignTaskDuplicationModel($c); 94 | }); 95 | $this->container['taskProjectMoveModel '] = $this->container->factory(function ($c) { 96 | return new TaskProjectMoveModel($c); 97 | }); 98 | $this->container['taskRecurrenceModel '] = $this->container->factory(function ($c) { 99 | return new TaskRecurrenceModel($c); 100 | }); 101 | } 102 | 103 | $this->hook->on('model:task:project_duplication:aftersave', function($hook_values) { 104 | $destination = $hook_values['destination_task_id']; 105 | $source = $hook_values['source_task_id']; 106 | $project_Id = $this->taskFinderModel->getProjectId($destination); 107 | 108 | if ($destination !== false) { 109 | // Check if the group is allowed for the destination project 110 | $group_id = $this->db->table(TaskModel::TABLE)->eq('id', $source)->findOneColumn('owner_gp'); 111 | if ($group_id > 0) { 112 | $group_in_project = $this->db 113 | ->table(ProjectGroupRoleModel::TABLE) 114 | ->eq('project_id', $project_Id) 115 | ->eq('group_id', $group_id) 116 | ->exists(); 117 | if ($group_in_project) { 118 | $this->db->table(TaskModel::TABLE)->eq('id', $hook_values['destination_task_id'])->update(['owner_gp' => $group_id]); 119 | } 120 | } 121 | 122 | // Check if the other assignees are allowed for the destination project 123 | $ms_id = $this->db->table(TaskModel::TABLE)->eq('id', $source)->findOneColumn('owner_ms'); 124 | if ($ms_id > 0) { 125 | $users_in_ms = $this->multiselectMemberModel->getMembers($ms_id); 126 | $new_ms_id = $this->multiselectModel->create(); 127 | $this->db->table(TaskModel::TABLE)->eq('id', $hook_values['destination_task_id'])->update(['owner_ms' => $new_ms_id]); 128 | foreach ($users_in_ms as $user) { 129 | if ($this->projectPermissionModel->isAssignable($project_Id, $user['id'])) { 130 | $this->multiselectMemberModel->addUser($new_ms_id, $user['id']); 131 | } 132 | } 133 | } 134 | } 135 | }); 136 | 137 | //Task - Template - details.php 138 | $this->template->hook->attach('template:task:details:third-column', 'group_assign:task/details'); 139 | $this->template->hook->attach('template:task:details:third-column', 'group_assign:task/multi'); 140 | 141 | //Forms - task_creation.php and task_modification.php 142 | if (file_exists('plugins/TemplateTitle')) { 143 | $this->template->setTemplateOverride('task_creation/show', 'group_assign:task_creation/show_TT'); 144 | $this->template->setTemplateOverride('task_modification/show', 'group_assign:task_modification/show_TT'); 145 | } else { 146 | $this->template->setTemplateOverride('task_creation/show', 'group_assign:task_creation/show'); 147 | $this->template->setTemplateOverride('task_modification/show', 'group_assign:task_modification/show'); 148 | } 149 | //Board 150 | $this->template->hook->attach('template:board:private:task:before-title', 'group_assign:board/group'); 151 | 152 | if ($this->configModel->get('boardcustomizer_compactlayout', '') == 'enable') { 153 | $this->template->hook->attach('template:board:private:task:before-avatar', 'group_assign:board/multi'); 154 | } else { 155 | $this->template->hook->attach('template:board:private:task:before-title', 'group_assign:board/multi'); 156 | } 157 | $groupmodel = $this->projectGroupRoleModel; 158 | $this->template->hook->attachCallable('template:app:filters-helper:after', 'group_assign:board/filter', function ($array = array()) use ($groupmodel) { 159 | if (!empty($array) && $array['id'] >= 1) { 160 | return ['grouplist' => array_column($groupmodel->getGroups($array['id']), 'name')]; 161 | } else { 162 | return ['grouplist' => array()]; 163 | } 164 | }); 165 | 166 | //Filter 167 | $this->container->extend('taskLexer', function ($taskLexer, $c) { 168 | $taskLexer->withFilter(TaskAllAssigneeFilter::getInstance()->setDatabase($c['db']) 169 | ->setCurrentUserId($c['userSession']->getId())); 170 | return $taskLexer; 171 | }); 172 | 173 | //Actions 174 | $this->actionManager->register(new EmailGroup($this->container)); 175 | $this->actionManager->register(new EmailGroupDue($this->container)); 176 | $this->actionManager->register(new EmailOtherAssignees($this->container)); 177 | $this->actionManager->register(new EmailOtherAssigneesDue($this->container)); 178 | $this->actionManager->register(new AssignGroup($this->container)); 179 | 180 | //Params 181 | $this->template->setTemplateOverride('action_creation/params', 'group_assign:action_creation/params'); 182 | 183 | //CSS 184 | $this->hook->on('template:layout:css', array('template' => 'plugins/Group_assign/Assets/css/group_assign.css')); 185 | 186 | //JS 187 | $this->hook->on('template:layout:js', array('template' => 'plugins/Group_assign/Assets/js/group_assign.js')); 188 | 189 | //Calendar Events 190 | $container = $this->container; 191 | 192 | $this->hook->on('controller:calendar:user:events', function ($user_id, $start, $end) use ($container) { 193 | $model = new GroupAssignCalendarModel($container); 194 | return $model->getUserCalendarEvents($user_id, $start, $end); // Return new events 195 | }); 196 | 197 | //Roles 198 | $this->template->hook->attach('template:config:application', 'group_assign:config/toggle'); 199 | 200 | if ($this->configModel->get('enable_am_group_management', '2') == 1) { 201 | $this->applicationAccessMap->add('GroupListController', '*', Role::APP_MANAGER); 202 | $this->applicationAccessMap->add('GroupCreationController', '*', Role::APP_MANAGER); 203 | $this->template->setTemplateOverride('header/user_dropdown', 'group_assign:header/user_dropdown'); 204 | } 205 | 206 | //API 207 | $this->api->getProcedureHandler()->withClassAndMethod('createTaskGroupAssign', new GroupAssignTaskProcedures($this->container), 'createTaskGroupAssign'); 208 | $this->api->getProcedureHandler()->withClassAndMethod('updateTaskGroupAssign', new GroupAssignTaskProcedures($this->container), 'updateTaskGroupAssign'); 209 | } 210 | 211 | public function onStartup() 212 | { 213 | Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale'); 214 | } 215 | 216 | public function getClasses() 217 | { 218 | return [ 219 | 'Plugin\Group_assign\Model' => [ 220 | 'MultiselectMemberModel', 'MultiselectModel', 'GroupColorExtension', 'TaskProjectMoveModel', 'TaskRecurrenceModel', 221 | ], 222 | ]; 223 | } 224 | 225 | public function getPluginName() 226 | { 227 | return 'Group_assign'; 228 | } 229 | public function getPluginDescription() 230 | { 231 | return t('Add group assignment to tasks'); 232 | } 233 | public function getPluginAuthor() 234 | { 235 | return 'Craig Crosby'; 236 | } 237 | public function getPluginVersion() 238 | { 239 | return '1.8.2'; 240 | } 241 | public function getPluginHomepage() 242 | { 243 | return 'https://github.com/creecros/group_assign'; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Checkout our latest project 2 | [![](https://raw.githubusercontent.com/docpht/docpht/master/public/assets/img/logo.png)](https://github.com/docpht/docpht) 3 | 4 | - With [DocPHT](https://github.com/docpht/docpht) you can take notes and quickly document anything and without the use of any database. 5 | ----------- 6 | [![Latest release](https://img.shields.io/github/release/creecros/Group_assign.svg)](https://github.com/creecros/Group_assign/releases) 7 | [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/creecros/Group_assign/blob/master/LICENSE) 8 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/creecros/Group_assign/graphs/contributors) 9 | [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)]() 10 | [![Downloads](https://img.shields.io/github/downloads/creecros/Group_assign/total.svg)](https://github.com/creecros/Group_assign/releases) 11 | 12 | Donate to help keep this project maintained. 13 | 14 | Donate with PayPal button 15 | 16 | :star: If you use it, you should star it on Github! 17 | It's the least you can do for all the work put into it! 18 | 19 | 20 | # Group_assign 21 | Assign Tasks to Groups or from Multi-Select of Users with permissions from the project 22 | 23 | # Requirements 24 | Kanboard v1.1.0 or Higher 25 | 26 | Group_assign Versions 1.7.7 and below will support PHP version below 7 27 | 28 | # Features and usage 29 | * A task can have an assigned group or selection of users 30 | * Can only assign groups or other assigness to a task that have permissions in the Project. 31 | * If a user is in a group that a task is assigned to, it will show up on their dashboard. 32 | * If a user is in other assignees multiselect that a task is assigned to, it will show up on their dashboard. 33 | * If a user is in a group that a task is assigned to, it will show up in their calendar. 34 | * If a user is in other assignees multiselect that a task is assigned to, it will show up in their calendar. 35 | * If a group is assigned or a user is assigneed in other assignees, it will be appear on the task in detail view, board view, creation, modification. 36 | * Includes 5 Automatic Actions to utilize the Assigned Group 37 | * Email Assigned Group on Task Modification, Creation, Close, or Movement 38 | * Email Assigned Group of impending Task Due Date 39 | * Email Other Assignees on Task Modification, Creation, Close, or Movement 40 | * Email Other Assignees of impending Task Due Date 41 | * Assign task to a group on creation or movement 42 | * using ``allassignees:me`` (``assignee:me`` for pre 1.7.3 versions) in filter will find tasks assigned to groups that the user is in or assignee in other assignees is in. 43 | * using ``allassignees:GroupName`` (``assignee:GroupName`` for pre 1.7.3 versions) in filter will find tasks assigned to a group by NAME of the group. 44 | * using ``allassignees:GroupID`` (``assignee:GroupID`` for pre 1.7.3 versions) in filter will find tasks assigned to a group by ID number of group. 45 | * using ``allassignees:Username`` or ``allassignees:Name`` will find all tasks assigned to that user regardless of how they have been assigneed, whether in the group or in Other Assignees or Assignee. 46 | * User assigneed via a group or multiselect will now recieve notifications 47 | * Changing assigned group or any multiselect users will now trigger `EVENT_ASSIGNEE_CHANGE` 48 | * Duplicating Tasks will include assigned groups and other users. 49 | * Duplicating to another project or moving to another project will check permissions of assignees, and remove those without permission. 50 | * Task Reccurences will include group assigned and other assignees in the recurrence. 51 | * Setting included to enable group managment for Application Managers 52 | * Found in `Settings > Application settings` 53 | 54 | # Future enhancments 55 | Find bugs or missing functionality, please report it. 56 | 57 | - [x] Add a few basic automatic actions that utilize Groups assigned 58 | - [x] Add relationship for ``allassignees:Username`` or ``allassignees:Name`` in the table lookup 59 | - [x] Add an event for assigned group change. 60 | - [x] Incorporate into notifications 61 | - [x] Address Task Duplication 62 | - [x] Task Recurrence 63 | 64 | # Manual Installation 65 | 66 | - Find the release you wish to install: https://github.com/creecros/Group_assign/releases 67 | - Download the provided zip, not the source zip, i.e. `Group_assign-x.x.x.zip` 68 | - Unzip contents to the plugins folder 69 | 70 | In the event that you use the master repo, ensure that the directory of the plugin is named `Group_assign`, or else the plugin will not work. 71 | 72 | # Screenshots 73 | 74 | ## Task Details: 75 | ![image](https://user-images.githubusercontent.com/26339368/49951197-64546680-fec7-11e8-9473-82820b1a4f7e.png) 76 | 77 | ## Task Creation/Modification: 78 | ![image](https://user-images.githubusercontent.com/26339368/38753761-692db008-3f2d-11e8-8ce2-59d88ddf39b1.png) 79 | ![image](https://user-images.githubusercontent.com/26339368/49557918-3c696f80-f8d7-11e8-91b8-7cef11c6eec0.png) 80 | 81 | ## Board View: 82 | ![image](https://user-images.githubusercontent.com/26339368/49951135-3a9b3f80-fec7-11e8-9bf6-3a777c09c675.png) 83 | 84 | ## Users Calendar View 85 | 86 | - Tasks that a user is assigned too but not main assignee will show up in calendar, with Dark Grey Background and Task color Border, to differentiate that they are not the main assignee. 87 | 88 | ![image](https://user-images.githubusercontent.com/26339368/49655821-b7cb3e00-fa09-11e8-9608-952abbf146fa.png) 89 | 90 | 91 | ## Automatic Actions: 92 | ![image](https://user-images.githubusercontent.com/26339368/38754253-0a0fd2de-3f2f-11e8-9dde-2036de011a6b.png) 93 | 94 | ![image](https://user-images.githubusercontent.com/26339368/38754279-2285d0d4-3f2f-11e8-88c2-0ed91e452f90.png) 95 | 96 | ![image](https://user-images.githubusercontent.com/26339368/38754288-310df2c6-3f2f-11e8-9993-39e96b55076c.png) 97 | 98 | -------------------------------------------------------------------------------- /Schema/Mysql.php: -------------------------------------------------------------------------------- 1 | exec('ALTER TABLE multiselect CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); 12 | $pdo->exec('ALTER TABLE multiselect_has_users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); 13 | } 14 | 15 | function version_2(PDO $pdo) 16 | { 17 | $pdo->exec("ALTER TABLE `tasks` ADD COLUMN `owner_ms` INT DEFAULT '0'"); 18 | 19 | $pdo->exec(" 20 | CREATE TABLE `multiselect` ( 21 | id INT NOT NULL AUTO_INCREMENT, 22 | external_id VARCHAR(255) DEFAULT '', 23 | PRIMARY KEY(id) 24 | ) ENGINE=InnoDB CHARSET=utf8 25 | "); 26 | $pdo->exec(" 27 | CREATE TABLE multiselect_has_users ( 28 | group_id INT NOT NULL, 29 | user_id INT NOT NULL, 30 | FOREIGN KEY(group_id) REFERENCES `multiselect`(id) ON DELETE CASCADE, 31 | FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, 32 | UNIQUE(group_id, user_id) 33 | ) ENGINE=InnoDB CHARSET=utf8 34 | "); 35 | } 36 | 37 | function version_1(PDO $pdo) 38 | { 39 | $pdo->exec("ALTER TABLE `tasks` ADD COLUMN `owner_gp` INT DEFAULT '0'"); 40 | } 41 | -------------------------------------------------------------------------------- /Schema/Postgres.php: -------------------------------------------------------------------------------- 1 | exec("ALTER TABLE tasks ADD COLUMN owner_ms INT DEFAULT '0'"); 12 | 13 | $pdo->exec(" 14 | CREATE TABLE multiselect ( 15 | id SERIAL PRIMARY KEY, 16 | external_id VARCHAR(255) DEFAULT '' 17 | ) 18 | "); 19 | 20 | $pdo->exec(" 21 | CREATE TABLE multiselect_has_users ( 22 | group_id INTEGER NOT NULL, 23 | user_id INTEGER NOT NULL, 24 | FOREIGN KEY(group_id) REFERENCES multiselect(id) ON DELETE CASCADE, 25 | FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, 26 | UNIQUE(group_id, user_id) 27 | ) 28 | "); 29 | } 30 | 31 | function version_1(PDO $pdo) 32 | { 33 | $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_gp INT DEFAULT '0'"); 34 | } 35 | -------------------------------------------------------------------------------- /Schema/Sqlite.php: -------------------------------------------------------------------------------- 1 | exec("ALTER TABLE tasks ADD COLUMN owner_ms INTEGER DEFAULT '0'"); 12 | 13 | $pdo->exec(" 14 | CREATE TABLE multiselect ( 15 | id INTEGER PRIMARY KEY, 16 | external_id TEXT DEFAULT '' 17 | ) 18 | "); 19 | 20 | $pdo->exec(" 21 | CREATE TABLE multiselect_has_users ( 22 | group_id INTEGER NOT NULL, 23 | user_id INTEGER NOT NULL, 24 | FOREIGN KEY(group_id) REFERENCES multiselect(id) ON DELETE CASCADE, 25 | FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, 26 | UNIQUE(group_id, user_id) 27 | ) 28 | "); 29 | } 30 | 31 | function version_1(PDO $pdo) 32 | { 33 | $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_gp INTEGER DEFAULT '0'"); 34 | } 35 | -------------------------------------------------------------------------------- /Template/action_creation/params.php: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | form->csrf() ?> 7 | 8 | form->hidden('event_name', $values) ?> 9 | form->hidden('action_name', $values) ?> 10 | 11 | form->label(t('Action'), 'action_name') ?> 12 | form->select('action_name', $available_actions, $values, array(), array('disabled')) ?> 13 | 14 | form->label(t('Event'), 'event_name') ?> 15 | form->select('event_name', $events, $values, array(), array('disabled')) ?> 16 | 17 | $param_desc): ?> 18 | text->contains($param_name, 'column_id')): ?> 19 | form->label($param_desc, $param_name) ?> 20 | form->select('params['.$param_name.']', $columns_list, $values) ?> 21 | text->contains($param_name, 'user_id')): ?> 22 | form->label($param_desc, $param_name) ?> 23 | form->select('params['.$param_name.']', $users_list, $values) ?> 24 | text->contains($param_name, 'group_id')): ?> 25 | model->projectGroupRoleModel->getGroups($values['project_id']); ?> 26 | 27 | 28 | 29 | 30 | 31 | form->label($param_desc, $param_name) ?> 32 | form->select('params['.$param_name.']', $groupvalues, $values) ?> 33 | text->contains($param_name, 'check_box')): ?> 34 | form->label(t('Options'), $param_name) ?> 35 | form->checkbox('params['.$param_name.']', $param_desc, 1) ?> 36 | text->contains($param_name, 'project_id')): ?> 37 | form->label($param_desc, $param_name) ?> 38 | form->select('params['.$param_name.']', $projects_list, $values) ?> 39 | text->contains($param_name, 'color_id')): ?> 40 | form->label($param_desc, $param_name) ?> 41 | form->select('params['.$param_name.']', $colors_list, $values) ?> 42 | text->contains($param_name, 'category_id')): ?> 43 | form->label($param_desc, $param_name) ?> 44 | form->select('params['.$param_name.']', $categories_list, $values) ?> 45 | text->contains($param_name, 'link_id')): ?> 46 | form->label($param_desc, $param_name) ?> 47 | form->select('params['.$param_name.']', $links_list, $values) ?> 48 | 49 | form->label($param_desc, $param_name) ?> 50 | form->select('params['.$param_name.']', $priorities_list, $values) ?> 51 | text->contains($param_name, 'duration')): ?> 52 | form->label($param_desc, $param_name) ?> 53 | form->number('params['.$param_name.']', $values) ?> 54 | text->contains($param_name, 'swimlane_id')): ?> 55 | form->label($param_desc, $param_name) ?> 56 | form->select('params['.$param_name.']', $swimlane_list, $values) ?> 57 | 58 | form->label(ucfirst($param_name), $param_name) ?> 59 | form->select('params['.$param_name.']', $param_desc, $values) ?> 60 | text->contains($param_name, 'multitasktitles')): ?> 61 | form->label($param_desc, $param_name) ?> 62 | form->textarea('params['.$param_name.']', $values) ?> 63 |
64 | 65 |
66 | 67 | form->label($param_desc, $param_name) ?> 68 | form->text('params['.$param_name.']', $values) ?> 69 | 70 | 71 | 72 | modal->submitButtons() ?> 73 |
74 | -------------------------------------------------------------------------------- /Template/board/filter.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 12 | -------------------------------------------------------------------------------- /Template/board/group.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | text->e($task['assigned_groupname'] ?: $task['owner_gp']) ?> 5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /Template/board/multi.php: -------------------------------------------------------------------------------- 1 | 0 && count($this->task->multiselectMemberModel->getMembers($task['owner_ms'])) > 0) : ?> 2 | 3 | user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_compactlayout")) { 4 | /* compact card layout */ 5 | ?> 6 | 7 | 8 | helper->sizeAvatarHelperExtend->sizeMultiple($task['owner_ms'], 'avatar-inline avatar-bdyn', $this->task->configModel->get('b_av_size', '20')) ?> 9 | 10 | 13 | 14 | 15 | helper->sizeAvatarHelperExtend->sizeMultiple($task['owner_ms'], 'avatar-inline avatar-bdyn', 13) ?> 16 |
17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Template/config/toggle.php: -------------------------------------------------------------------------------- 1 |
2 | form->radio('enable_am_group_management', t('Enable Group Managment for Application Managers'), 1, isset($values['enable_am_group_management'])&& $values['enable_am_group_management']==1) ?> 3 | form->radio('enable_am_group_management', t('Disable Group Managment for Application Managers'), 2, isset($values['enable_am_group_management'])&& $values['enable_am_group_management']==2) ?> 4 |
5 | -------------------------------------------------------------------------------- /Template/header/user_dropdown.php: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /Template/task/changes.php: -------------------------------------------------------------------------------- 1 | 2 | 83 | 84 | 85 |

86 | 87 |
text->markdown($task['description'], true) ?>
88 | 89 |
text->markdown($task['description']) ?>
90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Template/task/details.php: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 | 5 | text->e($task['assigned_groupname'] ?: $task['owner_gp']) ?> 6 | 7 | 8 | 9 | 10 |
  • 11 | -------------------------------------------------------------------------------- /Template/task/multi.php: -------------------------------------------------------------------------------- 1 | 0 && count($this->task->multiselectMemberModel->getMembers($task['owner_ms'])) > 0) : ?> 2 |
  • 3 | 4 |
  • 5 | helper->sizeAvatarHelperExtend->sizeMultiple($task['owner_ms'], 'avatar-inline', 20) ?> 6 | 7 | -------------------------------------------------------------------------------- /Template/task_creation/show.php: -------------------------------------------------------------------------------- 1 | 4 |
    5 | form->csrf() ?> 6 | 7 |
    8 |
    9 | task->renderTitleField($values, $errors) ?> 10 | task->renderDescriptionField($values, $errors) ?> 11 | task->renderDescriptionTemplateDropdown($project['id']) ?> 12 | task->renderTagField($project) ?> 13 | 14 | hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?> 15 |
    16 | 17 |
    18 | task->renderColorField($values) ?> 19 | task->renderAssigneeField($users_list, $values, $errors) ?> 20 | helper->newTaskHelper->renderGroupField($values, $errors) ?> 21 | helper->newTaskHelper->renderMultiAssigneeField($users_list, $values) ?> 22 | task->renderCategoryField($categories_list, $values, $errors) ?> 23 | task->renderSwimlaneField($swimlanes_list, $values, $errors) ?> 24 | task->renderColumnField($columns_list, $values, $errors) ?> 25 | task->renderPriorityField($project, $values) ?> 26 | 27 | hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> 28 |
    29 | 30 |
    31 | task->renderDueDateField($values, $errors) ?> 32 | task->renderStartDateField($values, $errors) ?> 33 | task->renderTimeEstimatedField($values, $errors) ?> 34 | task->renderTimeSpentField($values, $errors) ?> 35 | task->renderScoreField($values, $errors) ?> 36 | task->renderReferenceField($values, $errors) ?> 37 | 38 | hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?> 39 |
    40 | 41 |
    42 | 43 | hook->render('template:task:form:bottom-before-buttons', array('values' => $values, 'errors' => $errors)) ?> 44 | 45 | 46 | form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> 47 | form->checkbox('duplicate_multiple_projects', t('Duplicate to multiple projects'), 1) ?> 48 | 49 | 50 | modal->submitButtons() ?> 51 |
    52 |
    53 |
    54 | -------------------------------------------------------------------------------- /Template/task_creation/show_TT.php: -------------------------------------------------------------------------------- 1 | 4 |
    5 | form->csrf() ?> 6 | 7 |
    8 |
    9 | task->renderTitleField($values, $errors) ?> 10 | task->renderDescriptionField($values, $errors) ?> 11 | helper->templateTitleHelper->renderDescriptionAndTitleTemplateDropdown($project['id']) ?> 12 | task->renderTagField($project) ?> 13 | 14 | hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?> 15 |
    16 | 17 |
    18 | task->renderColorField($values) ?> 19 | task->renderAssigneeField($users_list, $values, $errors) ?> 20 | helper->newTaskHelper->renderGroupField($values, $errors) ?> 21 | helper->newTaskHelper->renderMultiAssigneeField($users_list, $values) ?> 22 | task->renderCategoryField($categories_list, $values, $errors) ?> 23 | task->renderSwimlaneField($swimlanes_list, $values, $errors) ?> 24 | task->renderColumnField($columns_list, $values, $errors) ?> 25 | task->renderPriorityField($project, $values) ?> 26 | 27 | hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> 28 |
    29 | 30 |
    31 | task->renderDueDateField($values, $errors) ?> 32 | task->renderStartDateField($values, $errors) ?> 33 | task->renderTimeEstimatedField($values, $errors) ?> 34 | task->renderTimeSpentField($values, $errors) ?> 35 | task->renderScoreField($values, $errors) ?> 36 | task->renderReferenceField($values, $errors) ?> 37 | 38 | hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?> 39 |
    40 | 41 |
    42 | 43 | hook->render('template:task:form:bottom-before-buttons', array('values' => $values, 'errors' => $errors)) ?> 44 | 45 | 46 | form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> 47 | form->checkbox('duplicate_multiple_projects', t('Duplicate to multiple projects'), 1) ?> 48 | 49 | 50 | modal->submitButtons() ?> 51 |
    52 |
    53 |
    54 | -------------------------------------------------------------------------------- /Template/task_modification/show.php: -------------------------------------------------------------------------------- 1 | 4 |
    5 | form->csrf() ?> 6 | 7 |
    8 |
    9 | task->renderTitleField($values, $errors) ?> 10 | task->renderDescriptionField($values, $errors) ?> 11 | task->renderDescriptionTemplateDropdown($project['id']) ?> 12 | task->renderTagField($project, $tags) ?> 13 | 14 | hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?> 15 |
    16 | 17 |
    18 | task->renderColorField($values) ?> 19 | task->renderAssigneeField($users_list, $values, $errors) ?> 20 | helper->newTaskHelper->renderGroupField($values, $errors) ?> 21 | helper->newTaskHelper->renderMultiAssigneeField($users_list, $values) ?> 22 | task->renderCategoryField($categories_list, $values, $errors) ?> 23 | task->renderPriorityField($project, $values) ?> 24 | 25 | hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> 26 |
    27 | 28 |
    29 | task->renderDueDateField($values, $errors) ?> 30 | task->renderStartDateField($values, $errors) ?> 31 | task->renderTimeEstimatedField($values, $errors) ?> 32 | task->renderTimeSpentField($values, $errors) ?> 33 | task->renderScoreField($values, $errors) ?> 34 | task->renderReferenceField($values, $errors) ?> 35 | 36 | hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?> 37 |
    38 | 39 |
    40 | 41 | hook->render('template:task:form:bottom-before-buttons', array('values' => $values, 'errors' => $errors)) ?> 42 | 43 | modal->submitButtons() ?> 44 |
    45 |
    46 |
    47 | -------------------------------------------------------------------------------- /Template/task_modification/show_TT.php: -------------------------------------------------------------------------------- 1 | 4 |
    5 | form->csrf() ?> 6 | 7 |
    8 |
    9 | task->renderTitleField($values, $errors) ?> 10 | task->renderDescriptionField($values, $errors) ?> 11 | helper->templateTitleHelper->renderDescriptionAndTitleTemplateDropdown($project['id']) ?> 12 | task->renderTagField($project, $tags) ?> 13 | 14 | hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?> 15 |
    16 | 17 |
    18 | task->renderColorField($values) ?> 19 | task->renderAssigneeField($users_list, $values, $errors) ?> 20 | helper->newTaskHelper->renderGroupField($values, $errors) ?> 21 | helper->newTaskHelper->renderMultiAssigneeField($users_list, $values) ?> 22 | task->renderCategoryField($categories_list, $values, $errors) ?> 23 | task->renderPriorityField($project, $values) ?> 24 | 25 | hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> 26 |
    27 | 28 |
    29 | task->renderDueDateField($values, $errors) ?> 30 | task->renderStartDateField($values, $errors) ?> 31 | task->renderTimeEstimatedField($values, $errors) ?> 32 | task->renderTimeSpentField($values, $errors) ?> 33 | task->renderScoreField($values, $errors) ?> 34 | task->renderReferenceField($values, $errors) ?> 35 | 36 | hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?> 37 |
    38 | 39 |
    40 | 41 | hook->render('template:task:form:bottom-before-buttons', array('values' => $values, 'errors' => $errors)) ?> 42 | 43 | modal->submitButtons() ?> 44 |
    45 |
    46 |
    47 | -------------------------------------------------------------------------------- /Test/Helper/NewTaskHelperTest.php: -------------------------------------------------------------------------------- 1 | container); 14 | $plugin->scan(); 15 | } 16 | public function testSelectPriority() 17 | { 18 | $helper = new NewTaskHelper($this->container); 19 | $this->assertNotEmpty($helper->renderPriorityField(array('priority_end' => '1', 'priority_start' => '5', 'priority_default' => '2'), array())); 20 | $this->assertNotEmpty($helper->renderPriorityField(array('priority_end' => '3', 'priority_start' => '1', 'priority_default' => '2'), array())); 21 | } 22 | public function testFormatPriority() 23 | { 24 | $helper = new NewTaskHelper($this->container); 25 | $this->assertEquals( 26 | 'P2', 27 | $helper->renderPriority(2) 28 | ); 29 | $this->assertEquals( 30 | '-P6', 31 | $helper->renderPriority(-6) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Test/Model/NewTaskFinderModelTest.php: -------------------------------------------------------------------------------- 1 | container); 19 | $plugin->scan(); 20 | } 21 | 22 | public function testGetDetails() 23 | { 24 | $taskCreationModel = new TaskCreationModel($this->container); 25 | $taskFinderModel = new NewTaskFinderModel($this->container); 26 | $projectModel = new ProjectModel($this->container); 27 | $categoryModel = new \Kanboard\Model\CategoryModel($this->container); 28 | $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); 29 | $this->assertEquals(1, $categoryModel->create(array('project_id' => 1, 'name' => 'C1'))); 30 | $this->assertEquals(1, $taskCreationModel->create(array( 31 | 'project_id' => 1, 32 | 'title' => 'Task #1', 33 | 'reference' => 'test', 34 | 'description' => 'desc', 35 | 'owner_id' => 1, 36 | 'category_id' => 1, 37 | ))); 38 | $task = $taskFinderModel->getDetails(1); 39 | $this->assertEquals(1, $task['id']); 40 | $this->assertEquals('test', $task['reference']); 41 | $this->assertEquals('Task #1', $task['title']); 42 | $this->assertEquals('desc', $task['description']); 43 | $this->assertEquals(time(), $task['date_creation'], 'Delta', 1); 44 | $this->assertEquals(time(), $task['date_modification'], 'Delta', 1); 45 | $this->assertEquals(time(), $task['date_moved'], 'Delta', 1); 46 | $this->assertEquals(0, $task['date_completed']); 47 | $this->assertEquals(0, $task['date_due']); 48 | $this->assertEquals(0, $task['date_started']); 49 | $this->assertEquals(0, $task['time_estimated']); 50 | $this->assertEquals(0, $task['time_spent']); 51 | $this->assertEquals('yellow', $task['color_id']); 52 | $this->assertEquals(1, $task['project_id']); 53 | $this->assertEquals(1, $task['column_id']); 54 | $this->assertEquals(1, $task['owner_id']); 55 | $this->assertEquals(0, $task['creator_id']); 56 | $this->assertEquals(1, $task['position']); 57 | $this->assertEquals(TaskModel::STATUS_OPEN, $task['is_active']); 58 | $this->assertEquals(0, $task['score']); 59 | $this->assertEquals(1, $task['category_id']); 60 | $this->assertEquals(0, $task['priority']); 61 | $this->assertEquals(1, $task['swimlane_id']); 62 | $this->assertEquals(TaskModel::RECURRING_STATUS_NONE, $task['recurrence_status']); 63 | $this->assertEquals(TaskModel::RECURRING_TRIGGER_FIRST_COLUMN, $task['recurrence_trigger']); 64 | $this->assertEquals(0, $task['recurrence_factor']); 65 | $this->assertEquals(TaskModel::RECURRING_TIMEFRAME_DAYS, $task['recurrence_timeframe']); 66 | $this->assertEquals(TaskModel::RECURRING_BASEDATE_DUEDATE, $task['recurrence_basedate']); 67 | $this->assertEquals(0, $task['recurrence_parent']); 68 | $this->assertEquals(0, $task['recurrence_child']); 69 | $this->assertEquals('C1', $task['category_name']); 70 | $this->assertEquals('Default swimlane', $task['swimlane_name']); 71 | $this->assertEquals('Project #1', $task['project_name']); 72 | $this->assertEquals('Backlog', $task['column_title']); 73 | $this->assertEquals('admin', $task['assignee_username']); 74 | $this->assertEquals('', $task['assignee_name']); 75 | $this->assertEquals('', $task['creator_username']); 76 | $this->assertEquals('', $task['creator_name']); 77 | } 78 | public function testGetTasksForDashboardWithHiddenColumn() 79 | { 80 | $taskCreationModel = new TaskCreationModel($this->container); 81 | $taskFinderModel = new NewTaskFinderModel($this->container); 82 | $projectModel = new ProjectModel($this->container); 83 | $columnModel = new ColumnModel($this->container); 84 | $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); 85 | $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1))); 86 | $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1))); 87 | $tasks = $taskFinderModel->getUserQuery(1)->findAll(); 88 | $this->assertCount(2, $tasks); 89 | $this->assertTrue($columnModel->update(2, 'Test', 0, '', 1)); 90 | $tasks = $taskFinderModel->getUserQuery(1)->findAll(); 91 | $this->assertCount(1, $tasks); 92 | $this->assertEquals('Task #1', $tasks[0]['title']); 93 | $this->assertEquals(1, $tasks[0]['column_id']); 94 | $this->assertTrue($columnModel->update(2, 'Test', 0, '', 0)); 95 | $tasks = $taskFinderModel->getUserQuery(1)->findAll(); 96 | $this->assertCount(2, $tasks); 97 | } 98 | public function testGetOverdueTasks() 99 | { 100 | $taskCreationModel = new TaskCreationModel($this->container); 101 | $taskFinderModel = new NewTaskFinderModel($this->container); 102 | $projectModel = new ProjectModel($this->container); 103 | $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); 104 | $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day')))); 105 | $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); 106 | $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => 0))); 107 | $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1))); 108 | $tasks = $taskFinderModel->getOverdueTasks(); 109 | $this->assertNotEmpty($tasks); 110 | $this->assertTrue(is_array($tasks)); 111 | $this->assertCount(1, $tasks); 112 | $this->assertEquals('Task #1', $tasks[0]['title']); 113 | } 114 | public function testGetOverdueTasksByProject() 115 | { 116 | $taskCreationModel = new TaskCreationModel($this->container); 117 | $taskFinderModel = new NewTaskFinderModel($this->container); 118 | $projectModel = new ProjectModel($this->container); 119 | $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); 120 | $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); 121 | $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day')))); 122 | $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'date_due' => strtotime('-1 day')))); 123 | $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); 124 | $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0))); 125 | $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1))); 126 | $tasks = $taskFinderModel->getOverdueTasksByProject(1); 127 | $this->assertNotEmpty($tasks); 128 | $this->assertTrue(is_array($tasks)); 129 | $this->assertCount(1, $tasks); 130 | $this->assertEquals('Task #1', $tasks[0]['title']); 131 | } 132 | public function testGetOverdueTasksByUser() 133 | { 134 | $taskCreationModel = new TaskCreationModel($this->container); 135 | $taskFinderModel = new NewTaskFinderModel($this->container); 136 | $projectModel = new ProjectModel($this->container); 137 | $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); 138 | $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); 139 | $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'owner_id' => 1, 'date_due' => strtotime('-1 day')))); 140 | $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'owner_id' => 1, 'date_due' => strtotime('-1 day')))); 141 | $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day')))); 142 | $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0))); 143 | $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1))); 144 | $tasks = $taskFinderModel->getOverdueTasksByUser(1); 145 | $this->assertNotEmpty($tasks); 146 | $this->assertTrue(is_array($tasks)); 147 | $this->assertCount(2, $tasks); 148 | $this->assertEquals(1, $tasks[0]['id']); 149 | $this->assertEquals('Task #1', $tasks[0]['title']); 150 | $this->assertEquals(1, $tasks[0]['owner_id']); 151 | $this->assertEquals(1, $tasks[0]['project_id']); 152 | $this->assertEquals('Project #1', $tasks[0]['project_name']); 153 | $this->assertEquals('admin', $tasks[0]['assignee_username']); 154 | $this->assertEquals('', $tasks[0]['assignee_name']); 155 | $this->assertEquals('Task #2', $tasks[1]['title']); 156 | } 157 | public function testCountByProject() 158 | { 159 | $taskCreationModel = new TaskCreationModel($this->container); 160 | $taskFinderModel = new NewTaskFinderModel($this->container); 161 | $projectModel = new ProjectModel($this->container); 162 | $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); 163 | $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); 164 | $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1))); 165 | $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2))); 166 | $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 2))); 167 | $this->assertEquals(1, $taskFinderModel->countByProjectId(1)); 168 | $this->assertEquals(2, $taskFinderModel->countByProjectId(2)); 169 | } 170 | public function testGetProjectToken() 171 | { 172 | $taskCreationModel = new TaskCreationModel($this->container); 173 | $taskFinderModel = new NewTaskFinderModel($this->container); 174 | $projectModel = new ProjectModel($this->container); 175 | $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); 176 | $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2'))); 177 | $this->assertTrue($projectModel->enablePublicAccess(1)); 178 | $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1))); 179 | $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2))); 180 | $project = $projectModel->getById(1); 181 | $this->assertEquals($project['token'], $taskFinderModel->getProjectToken(1)); 182 | $this->assertEmpty($taskFinderModel->getProjectToken(2)); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Test/PluginTest.php: -------------------------------------------------------------------------------- 1 | container); 12 | $this->assertSame(null, $plugin->initialize()); 13 | $this->assertNotEmpty($plugin->getPluginName()); 14 | $this->assertNotEmpty($plugin->getPluginDescription()); 15 | $this->assertNotEmpty($plugin->getPluginAuthor()); 16 | $this->assertNotEmpty($plugin->getPluginVersion()); 17 | $this->assertNotEmpty($plugin->getPluginHomepage()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | plugins: 3 | - jemoji 4 | --------------------------------------------------------------------------------