summaryrefslogtreecommitdiff
path: root/plugins/Group_assign
diff options
context:
space:
mode:
authorDzial Techniczny WMW Projekt s.c <techniczna@wmwprojekt.pl>2019-12-10 11:34:53 +0100
committerDzial Techniczny WMW Projekt s.c <techniczna@wmwprojekt.pl>2019-12-10 11:34:53 +0100
commitb8fa0246803dab40cf57d40b45984c53046f2d55 (patch)
treedc92b167c7542137c385614a1d558e57669a4339 /plugins/Group_assign
parent2a43146236fd8fb16f84398d85720ad84aa0a0b1 (diff)
Plugins directory and local modifications
Diffstat (limited to 'plugins/Group_assign')
-rw-r--r--plugins/Group_assign/.travis.yml30
-rw-r--r--plugins/Group_assign/Action/AssignGroup.php95
-rw-r--r--plugins/Group_assign/Action/EmailGroup.php80
-rw-r--r--plugins/Group_assign/Action/EmailGroupDue.php116
-rw-r--r--plugins/Group_assign/Action/EmailOtherAssignees.php80
-rw-r--r--plugins/Group_assign/Action/EmailOtherAssigneesDue.php116
-rw-r--r--plugins/Group_assign/Assets/css/group_assign.css17
-rw-r--r--plugins/Group_assign/Assets/js/group_assign.js5
-rw-r--r--plugins/Group_assign/Controller/GroupAssignTaskCreationController.php175
-rw-r--r--plugins/Group_assign/Controller/GroupAssignTaskModificationController.php208
-rw-r--r--plugins/Group_assign/Filter/TaskAllAssigneeFilter.php122
-rw-r--r--plugins/Group_assign/Helper/NewTaskHelper.php389
-rw-r--r--plugins/Group_assign/Helper/SmallAvatarHelperExtend.php35
-rw-r--r--plugins/Group_assign/LICENSE21
-rw-r--r--plugins/Group_assign/Locale/de_DE/translations.php13
-rw-r--r--plugins/Group_assign/Locale/pt_BR/translations.php97
-rw-r--r--plugins/Group_assign/Model/GroupAssignCalendarModel.php89
-rw-r--r--plugins/Group_assign/Model/GroupAssignTaskDuplicationModel.php175
-rw-r--r--plugins/Group_assign/Model/GroupColorExtension.php28
-rw-r--r--plugins/Group_assign/Model/MultiselectMemberModel.php169
-rw-r--r--plugins/Group_assign/Model/MultiselectModel.php143
-rw-r--r--plugins/Group_assign/Model/NewMetaMagikSubquery.php28
-rw-r--r--plugins/Group_assign/Model/NewTaskFinderModel.php491
-rw-r--r--plugins/Group_assign/Model/NewUserNotificationFilterModel.php218
-rw-r--r--plugins/Group_assign/Model/OldMetaMagikSubquery.php28
-rw-r--r--plugins/Group_assign/Model/OldTaskFinderModel.php489
-rw-r--r--plugins/Group_assign/Model/TaskProjectDuplicationModel.php94
-rw-r--r--plugins/Group_assign/Model/TaskProjectMoveModel.php96
-rw-r--r--plugins/Group_assign/Model/TaskRecurrenceModel.php154
-rw-r--r--plugins/Group_assign/Plugin.php200
-rw-r--r--plugins/Group_assign/README.md95
-rw-r--r--plugins/Group_assign/Schema/Mysql.php34
-rw-r--r--plugins/Group_assign/Schema/Postgres.php34
-rw-r--r--plugins/Group_assign/Schema/Sqlite.php34
-rw-r--r--plugins/Group_assign/Template/action_creation/params.php73
-rw-r--r--plugins/Group_assign/Template/board/filter.php12
-rw-r--r--plugins/Group_assign/Template/board/group.php7
-rw-r--r--plugins/Group_assign/Template/board/multi.php5
-rw-r--r--plugins/Group_assign/Template/config/toggle.php4
-rw-r--r--plugins/Group_assign/Template/header/user_dropdown.php46
-rw-r--r--plugins/Group_assign/Template/task/changes.php92
-rw-r--r--plugins/Group_assign/Template/task/details.php10
-rw-r--r--plugins/Group_assign/Template/task/multi.php6
-rw-r--r--plugins/Group_assign/Template/task_creation/show.php50
-rw-r--r--plugins/Group_assign/Template/task_modification/show.php43
-rw-r--r--plugins/Group_assign/Test/Helper/NewTaskHelperTest.php34
-rw-r--r--plugins/Group_assign/Test/Model/NewTaskFinderModelTest.php184
-rw-r--r--plugins/Group_assign/Test/PluginTest.php19
-rw-r--r--plugins/Group_assign/_config.yml3
49 files changed, 4786 insertions, 0 deletions
diff --git a/plugins/Group_assign/.travis.yml b/plugins/Group_assign/.travis.yml
new file mode 100644
index 00000000..6e29a9fc
--- /dev/null
+++ b/plugins/Group_assign/.travis.yml
@@ -0,0 +1,30 @@
+language: php
+sudo: false
+
+php:
+ - 7.2
+
+env:
+ global:
+ - PLUGIN=Group_assign
+ - KANBOARD_REPO=https://github.com/kanboard/kanboard.git
+ matrix:
+ - DB=sqlite
+ - DB=mysql
+ - DB=postgres
+
+matrix:
+ fast_finish: true
+
+install:
+ - git clone --depth 1 $KANBOARD_REPO
+ - ln -s $TRAVIS_BUILD_DIR kanboard/plugins/$PLUGIN
+
+before_script:
+ - cd kanboard
+ - phpenv config-add tests/php.ini
+ - composer install
+ - ls -la plugins/
+
+script:
+ - phpunit -c tests/units.$DB.xml plugins/$PLUGIN/Test/
diff --git a/plugins/Group_assign/Action/AssignGroup.php b/plugins/Group_assign/Action/AssignGroup.php
new file mode 100644
index 00000000..4a63696b
--- /dev/null
+++ b/plugins/Group_assign/Action/AssignGroup.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Action\Base;
+
+class AssignGroup extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign the task to a specific group');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_CREATE_UPDATE,
+ TaskModel::EVENT_MOVE_COLUMN,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'column_id' => t('Column'),
+ 'group_id' => t('Group'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'task' => array(
+ 'project_id',
+ 'column_id',
+ ),
+ );
+ }
+
+ /**
+ * Execute the action (assign the given user)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $values = array(
+ 'id' => $data['task_id'],
+ 'owner_gp' => $this->getParam('group_id'),
+ );
+
+ return $this->taskModificationModel->update($values);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['task']['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/plugins/Group_assign/Action/EmailGroup.php b/plugins/Group_assign/Action/EmailGroup.php
new file mode 100644
index 00000000..6d12bc73
--- /dev/null
+++ b/plugins/Group_assign/Action/EmailGroup.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\Base;
+
+class EmailGroup extends Base
+{
+
+ public function getDescription()
+ {
+ return t('Send a task by email to assigned group members');
+ }
+
+
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_MOVE_COLUMN,
+ TaskModel::EVENT_CLOSE,
+ TaskModel::EVENT_CREATE,
+ );
+ }
+
+
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'column_id' => t('Column'),
+ 'subject' => t('Email subject'),
+ );
+ }
+
+
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'task' => array(
+ 'project_id',
+ 'column_id',
+ 'owner_id',
+ 'owner_gp',
+ ),
+ );
+ }
+
+
+ public function doAction(array $data)
+ {
+ $groupmembers = $this->groupMemberModel->getMembers($data['task']['owner_gp']);
+
+ if (! empty($groupmembers)) {
+ foreach ($groupmembers as $members) {
+ $user = $this->userModel->getById($members['id']);
+ if (! empty($user['email'])) {
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array(
+ 'task' => $data['task'],
+ ))
+ );
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['task']['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/plugins/Group_assign/Action/EmailGroupDue.php b/plugins/Group_assign/Action/EmailGroupDue.php
new file mode 100644
index 00000000..8f1a66a7
--- /dev/null
+++ b/plugins/Group_assign/Action/EmailGroupDue.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\Base;
+
+/**
+ * Email a task notification of impending due date
+ */
+class EmailGroupDue extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Send email notification of impending due date to Group Members Assigned');
+ }
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_DAILY_CRONJOB,
+ );
+ }
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'subject' => t('Email subject'),
+ 'duration' => t('Duration in days'),
+ );
+ }
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+
+ }
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+
+ public function doAction(array $data)
+ {
+ $results = array();
+ $max = $this->getParam('duration') * 86400;
+
+ foreach ($data['tasks'] as $task) {
+ $groupmembers = $this->groupMemberModel->getMembers($task['owner_gp']);
+
+ if (! empty($groupmembers)) {
+ foreach ($groupmembers as $members) {
+ $user = $this->userModel->getById($members['id']);
+
+ $duration = $task['date_due'] - time();
+ if ($task['date_due'] > 0) {
+ if ($duration < $max) {
+ if (! empty($user['email'])) {
+ $results[] = $this->sendEmail($task['id'], $user);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+ /**
+ * Send email
+ *
+ * @access private
+ * @param integer $task_id
+ * @param array $user
+ * @return boolean
+ */
+ private function sendEmail($task_id, array $user)
+ {
+ $task = $this->taskFinderModel->getDetails($task_id);
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array('task' => $task))
+ );
+ return true;
+ }
+}
diff --git a/plugins/Group_assign/Action/EmailOtherAssignees.php b/plugins/Group_assign/Action/EmailOtherAssignees.php
new file mode 100644
index 00000000..f9ab091c
--- /dev/null
+++ b/plugins/Group_assign/Action/EmailOtherAssignees.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\Base;
+
+class EmailOtherAssignees extends Base
+{
+
+ public function getDescription()
+ {
+ return t('Send a task by email to the other assignees for a task.');
+ }
+
+
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_MOVE_COLUMN,
+ TaskModel::EVENT_CLOSE,
+ TaskModel::EVENT_CREATE,
+ );
+ }
+
+
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'column_id' => t('Column'),
+ 'subject' => t('Email subject'),
+ );
+ }
+
+
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'task' => array(
+ 'project_id',
+ 'column_id',
+ 'owner_id',
+ 'owner_ms',
+ ),
+ );
+ }
+
+
+ public function doAction(array $data)
+ {
+ $multimembers = $this->multiselectMemberModel->getMembers($data['task']['owner_ms']);
+
+ if (! empty($multimembers)) {
+ foreach ($multimembers as $members) {
+ $user = $this->userModel->getById($members['id']);
+ if (! empty($user['email'])) {
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array(
+ 'task' => $data['task'],
+ ))
+ );
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['task']['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/plugins/Group_assign/Action/EmailOtherAssigneesDue.php b/plugins/Group_assign/Action/EmailOtherAssigneesDue.php
new file mode 100644
index 00000000..abd15003
--- /dev/null
+++ b/plugins/Group_assign/Action/EmailOtherAssigneesDue.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\Base;
+
+/**
+ * Email a task notification of impending due date
+ */
+class EmailOtherAssigneesDue extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Send email notification of impending due date to the other assignees of a task');
+ }
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_DAILY_CRONJOB,
+ );
+ }
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'subject' => t('Email subject'),
+ 'duration' => t('Duration in days'),
+ );
+ }
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+
+ }
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+
+ public function doAction(array $data)
+ {
+ $results = array();
+ $max = $this->getParam('duration') * 86400;
+
+ foreach ($data['tasks'] as $task) {
+ $groupmembers = $this->multiselectMemberModel->getMembers($task['owner_ms']);
+
+ if (! empty($groupmembers)) {
+ foreach ($groupmembers as $members) {
+ $user = $this->userModel->getById($members['id']);
+
+ $duration = $task['date_due'] - time();
+ if ($task['date_due'] > 0) {
+ if ($duration < $max) {
+ if (! empty($user['email'])) {
+ $results[] = $this->sendEmail($task['id'], $user);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+ /**
+ * Send email
+ *
+ * @access private
+ * @param integer $task_id
+ * @param array $user
+ * @return boolean
+ */
+ private function sendEmail($task_id, array $user)
+ {
+ $task = $this->taskFinderModel->getDetails($task_id);
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array('task' => $task))
+ );
+ return true;
+ }
+}
diff --git a/plugins/Group_assign/Assets/css/group_assign.css b/plugins/Group_assign/Assets/css/group_assign.css
new file mode 100644
index 00000000..e5693f83
--- /dev/null
+++ b/plugins/Group_assign/Assets/css/group_assign.css
@@ -0,0 +1,17 @@
+.avatar-13 .avatar-letter {
+ border-radius: 6.5px;
+ line-height: 13px;
+ width: 13px;
+ font-size: 7px;
+}
+.avatar-13 img {
+ border-radius: 6.5px;
+}
+
+.assigned-group {
+ display: inline-block;
+ margin: 3px 3px 0 0;
+ padding: 1px 3px 1px 3px;
+ color: #333;
+ border-radius: 4px;
+}
diff --git a/plugins/Group_assign/Assets/js/group_assign.js b/plugins/Group_assign/Assets/js/group_assign.js
new file mode 100644
index 00000000..daf47e89
--- /dev/null
+++ b/plugins/Group_assign/Assets/js/group_assign.js
@@ -0,0 +1,5 @@
+
+ KB.on('modal.afterRender', function () {
+ $(".group-assign-select").select2({
+ });
+ }); \ No newline at end of file
diff --git a/plugins/Group_assign/Controller/GroupAssignTaskCreationController.php b/plugins/Group_assign/Controller/GroupAssignTaskCreationController.php
new file mode 100644
index 00000000..f7dadbee
--- /dev/null
+++ b/plugins/Group_assign/Controller/GroupAssignTaskCreationController.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Controller;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Controller\BaseController;
+use Kanboard\Core\Controller\PageNotFoundException;
+
+
+class GroupAssignTaskCreationController extends BaseController
+{
+ /**
+ * Display a form to create a new task
+ *
+ * @access public
+ * @param array $values
+ * @param array $errors
+ * @throws PageNotFoundException
+ */
+ public function show(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+ $swimlanesList = $this->swimlaneModel->getList($project['id'], false, true);
+ $values += $this->prepareValues($project['is_private'], $swimlanesList);
+
+ $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values));
+ $values = $this->hook->merge('controller:task-creation:form:default', $values, array('default_values' => $values));
+
+ $this->response->html($this->template->render('task_creation/show', array(
+ 'project' => $project,
+ 'errors' => $errors,
+ 'values' => $values + array('project_id' => $project['id']),
+ 'columns_list' => $this->columnModel->getList($project['id']),
+ 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id'], true, false, $project['is_private'] == 1),
+ 'categories_list' => $this->categoryModel->getList($project['id']),
+ 'swimlanes_list' => $swimlanesList,
+ )));
+ }
+
+ /**
+ * Validate and save a new task
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+ $values['project_id'] = $project['id'];
+ if (isset($values['owner_ms']) && !empty($values['owner_ms'])) {
+ $ms_id = $this->multiselectModel->create();
+ foreach ($values['owner_ms'] as $user) {
+ $this->multiselectMemberModel->addUser($ms_id, $user);
+ }
+ unset($values['owner_ms']);
+ $values['owner_ms'] = $ms_id;
+ }
+
+ list($valid, $errors) = $this->taskValidator->validateCreation($values);
+
+ if (! $valid) {
+ $this->flash->failure(t('Unable to create your task.'));
+ $this->show($values, $errors);
+ } else if (! $this->helper->projectRole->canCreateTaskInColumn($project['id'], $values['column_id'])) {
+ $this->flash->failure(t('You cannot create tasks in this column.'));
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ } else {
+ $task_id = $this->taskCreationModel->create($values);
+
+ if ($task_id > 0) {
+ $this->flash->success(t('Task created successfully.'));
+ $this->afterSave($project, $values, $task_id);
+ } else {
+ $this->flash->failure(t('Unable to create this task.'));
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ }
+ }
+ }
+
+ /**
+ * Duplicate created tasks to multiple projects
+ *
+ * @throws PageNotFoundException
+ */
+ public function duplicateProjects()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ if (isset($values['project_ids'])) {
+ foreach ($values['project_ids'] as $project_id) {
+ $this->taskProjectDuplicationModel->duplicateToProject($values['task_id'], $project_id);
+ }
+ }
+
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ }
+
+ /**
+ * Executed after the task is saved
+ *
+ * @param array $project
+ * @param array $values
+ * @param integer $task_id
+ */
+ protected function afterSave(array $project, array &$values, $task_id)
+ {
+ if (isset($values['duplicate_multiple_projects']) && $values['duplicate_multiple_projects'] == 1) {
+ $this->chooseProjects($project, $task_id);
+ } elseif (isset($values['another_task']) && $values['another_task'] == 1) {
+ $this->show(array(
+ 'owner_id' => $values['owner_id'],
+ 'color_id' => $values['color_id'],
+ 'category_id' => isset($values['category_id']) ? $values['category_id'] : 0,
+ 'column_id' => $values['column_id'],
+ 'swimlane_id' => isset($values['swimlane_id']) ? $values['swimlane_id'] : 0,
+ 'another_task' => 1,
+ ));
+ } else {
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ }
+ }
+
+ /**
+ * Prepare form values
+ *
+ * @access protected
+ * @param bool $isPrivateProject
+ * @param array $swimlanesList
+ * @return array
+ */
+ protected function prepareValues($isPrivateProject, array $swimlanesList)
+ {
+ $values = array(
+ 'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanesList)),
+ 'column_id' => $this->request->getIntegerParam('column_id'),
+ 'color_id' => $this->colorModel->getDefaultColor(),
+ );
+
+ if ($isPrivateProject) {
+ $values['owner_id'] = $this->userSession->getId();
+ }
+
+ return $values;
+ }
+
+ /**
+ * Choose projects
+ *
+ * @param array $project
+ * @param integer $task_id
+ */
+ protected function chooseProjects(array $project, $task_id)
+ {
+ $task = $this->taskFinderModel->getById($task_id);
+ $projects = $this->projectUserRoleModel->getActiveProjectsByUser($this->userSession->getId());
+ unset($projects[$project['id']]);
+
+ $this->response->html($this->template->render('task_creation/duplicate_projects', array(
+ 'project' => $project,
+ 'task' => $task,
+ 'projects_list' => $projects,
+ 'values' => array('task_id' => $task['id'])
+ )));
+ }
+}
diff --git a/plugins/Group_assign/Controller/GroupAssignTaskModificationController.php b/plugins/Group_assign/Controller/GroupAssignTaskModificationController.php
new file mode 100644
index 00000000..b975c3d9
--- /dev/null
+++ b/plugins/Group_assign/Controller/GroupAssignTaskModificationController.php
@@ -0,0 +1,208 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Controller;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Controller\BaseController;
+use Kanboard\Core\Controller\PageNotFoundException;
+
+
+/**
+ * Group Assign Task Modification controller
+ *
+ * @package Kanboard\Plugin\Group_assign\
+ * @author Craig Crosby
+ */
+class GroupAssignTaskModificationController extends BaseController
+{
+ public function assignToMe()
+ {
+ $task = $this->getTask();
+ $values = ['id' => $task['id'], 'owner_id' => $this->userSession->getId()];
+
+ if (! $this->helper->projectRole->canUpdateTask($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.'));
+ }
+
+ $this->taskModificationModel->update($values);
+ $this->redirectAfterQuickAction($task);
+ }
+
+ /**
+ * Set the start date automatically
+ *
+ * @access public
+ */
+ public function start()
+ {
+ $task = $this->getTask();
+ $values = ['id' => $task['id'], 'date_started' => time()];
+
+ if (! $this->helper->projectRole->canUpdateTask($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.'));
+ }
+
+ $this->taskModificationModel->update($values);
+ $this->redirectAfterQuickAction($task);
+ }
+
+ protected function redirectAfterQuickAction(array $task)
+ {
+ switch ($this->request->getStringParam('redirect')) {
+ case 'board':
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', ['project_id' => $task['project_id']]));
+ break;
+ case 'list':
+ $this->response->redirect($this->helper->url->to('TaskListController', 'show', ['project_id' => $task['project_id']]));
+ break;
+ case 'dashboard':
+ $this->response->redirect($this->helper->url->to('DashboardController', 'show', [], 'project-tasks-'.$task['project_id']));
+ break;
+ case 'dashboard-tasks':
+ $this->response->redirect($this->helper->url->to('DashboardController', 'tasks', ['user_id' => $this->userSession->getId()]));
+ break;
+ default:
+ $this->response->redirect($this->helper->url->to('TaskViewController', 'show', ['project_id' => $task['project_id'], 'task_id' => $task['id']]));
+ }
+ }
+
+ /**
+ * Display a form to edit a task
+ *
+ * @access public
+ * @param array $values
+ * @param array $errors
+ * @throws \Kanboard\Core\Controller\AccessForbiddenException
+ * @throws \Kanboard\Core\Controller\PageNotFoundException
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+
+ if (! $this->helper->projectRole->canUpdateTask($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.'));
+ }
+
+ $project = $this->projectModel->getById($task['project_id']);
+
+ if (empty($values)) {
+ $values = $task;
+ }
+
+ $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values));
+ $values = $this->hook->merge('controller:task-modification:form:default', $values, array('default_values' => $values));
+
+ $params = array(
+ 'project' => $project,
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'tags' => $this->taskTagModel->getList($task['id']),
+ 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($task['project_id']),
+ 'categories_list' => $this->categoryModel->getList($task['project_id']),
+ );
+
+ $this->renderTemplate($task, $params);
+ }
+
+ protected function renderTemplate(array &$task, array &$params)
+ {
+ if (empty($task['external_uri'])) {
+ $this->response->html($this->template->render('task_modification/show', $params));
+ } else {
+
+ try {
+ $taskProvider = $this->externalTaskManager->getProvider($task['external_provider']);
+ $params['template'] = $taskProvider->getModificationFormTemplate();
+ $params['external_task'] = $taskProvider->fetch($task['external_uri']);
+ } catch (ExternalTaskAccessForbiddenException $e) {
+ throw new AccessForbiddenException($e->getMessage());
+ } catch (ExternalTaskException $e) {
+ $params['error_message'] = $e->getMessage();
+ }
+
+ $this->response->html($this->template->render('external_task_modification/show', $params));
+ }
+ }
+
+ /**
+ * Validate and update a task
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $previousMembers = array();
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+ $values['id'] = $task['id'];
+ $values['project_id'] = $task['project_id'];
+ if (isset($values['owner_ms']) && !empty($values['owner_ms'])) {
+ if (!empty($task['owner_ms'])) {
+ $ms_id = $task['owner_ms'];
+ $previousMembers = $this->multiselectMemberModel->getMembers($ms_id);
+ $this->multiselectMemberModel->removeAllUsers($ms_id);
+ } else {
+ $ms_id = $this->multiselectModel->create();
+ }
+ foreach ($values['owner_ms'] as $user) {
+ if ($user !== 0) { $this->multiselectMemberModel->addUser($ms_id, $user); }
+ }
+ unset($values['owner_ms']);
+ $values['owner_ms'] = $ms_id;
+
+ $newMembersSet = $this->multiselectMemberModel->getMembers($values['owner_ms']);
+ if (sort($previousMembers) !== sort($newMembersSet)) { $this->multiselectMemberModel->assigneeChanged($task, $values); }
+
+ if ($values['owner_gp'] !== $task['owner_gp']) { $this->multiselectMemberModel->assigneeChanged($task, $values); }
+ } else {
+ $this->multiselectMemberModel->removeAllUsers($task['owner_ms']);
+ }
+
+ list($valid, $errors) = $this->taskValidator->validateModification($values);
+
+ if ($valid && $this->updateTask($task, $values, $errors)) {
+ $this->flash->success(t('Task updated successfully.'));
+ $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
+ } else {
+ $this->flash->failure(t('Unable to update your task.'));
+ $this->edit($values, $errors);
+ }
+ }
+
+ protected function updateTask(array &$task, array &$values, array &$errors)
+ {
+ if (isset($values['owner_id']) && $values['owner_id'] != $task['owner_id'] && !$this->helper->projectRole->canChangeAssignee($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to change the assignee.'));
+ }
+
+ if (! $this->helper->projectRole->canUpdateTask($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.'));
+ }
+
+ $result = $this->taskModificationModel->update($values);
+
+ if ($result && ! empty($task['external_uri'])) {
+ try {
+ $taskProvider = $this->externalTaskManager->getProvider($task['external_provider']);
+ $result = $taskProvider->save($task['external_uri'], $values, $errors);
+ } catch (ExternalTaskAccessForbiddenException $e) {
+ throw new AccessForbiddenException($e->getMessage());
+ } catch (ExternalTaskException $e) {
+ $this->logger->error($e->getMessage());
+ $result = false;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/Group_assign/Filter/TaskAllAssigneeFilter.php b/plugins/Group_assign/Filter/TaskAllAssigneeFilter.php
new file mode 100644
index 00000000..e7ffc06f
--- /dev/null
+++ b/plugins/Group_assign/Filter/TaskAllAssigneeFilter.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Filter;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Filter\BaseFilter;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Model\GroupModel;
+use PicoDb\Database;
+
+
+class TaskAllAssigneeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return TaskAssigneeFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('allassignees');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->beginOr();
+ $this->query->eq(TaskModel::TABLE.'.owner_id', $this->value);
+ $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$this->value')");
+ $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$this->value')");
+ $this->query->closeOr();
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $this->query->beginOr();
+ $this->query->eq(TaskModel::TABLE.'.owner_id', $this->currentUserId);
+ $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$this->currentUserId')");
+ $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$this->currentUserId')");
+ $this->query->closeOr();
+ break;
+ case 'nobody':
+ $this->query->eq(TaskModel::TABLE.'.owner_id', 0);
+ break;
+ default:
+ $useridsarray = $this->getSubQuery()->findAllByColumn('id');
+ $useridstring = implode("','", $useridsarray);
+ $this->query->beginOr();
+ $this->query->ilike(UserModel::TABLE.'.username', '%'.$this->value.'%');
+ $this->query->ilike(UserModel::TABLE.'.name', '%'.$this->value.'%');
+ $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT id FROM ".GroupModel::TABLE." WHERE ".GroupModel::TABLE.".name='$this->value')");
+ $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id IN ('$useridstring'))");
+ $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id IN ('$useridstring'))");
+ $this->query->closeOr();
+ }
+ }
+ }
+ public function getSubQuery()
+ {
+ return $this->db->table(UserModel::TABLE)
+ ->columns(
+ UserModel::TABLE.'.id',
+ UserModel::TABLE.'.username',
+ UserModel::TABLE.'.name'
+ )
+ ->beginOr()
+ ->ilike(UserModel::TABLE.'.username', '%'.$this->value.'%')
+ ->ilike(UserModel::TABLE.'.name', '%'.$this->value.'%')
+ ->closeOr();
+ }
+
+}
diff --git a/plugins/Group_assign/Helper/NewTaskHelper.php b/plugins/Group_assign/Helper/NewTaskHelper.php
new file mode 100644
index 00000000..b15262a1
--- /dev/null
+++ b/plugins/Group_assign/Helper/NewTaskHelper.php
@@ -0,0 +1,389 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Helper;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Core\Base;
+
+
+class NewTaskHelper extends Base
+{
+ /**
+ * Local cache for project columns
+ *
+ * @access private
+ * @var array
+ */
+ private $columns = array();
+
+ public function getColors()
+ {
+ return $this->colorModel->getList();
+ }
+
+ public function recurrenceTriggers()
+ {
+ return $this->taskRecurrenceModel->getRecurrenceTriggerList();
+ }
+
+ public function recurrenceTimeframes()
+ {
+ return $this->taskRecurrenceModel->getRecurrenceTimeframeList();
+ }
+
+ public function recurrenceBasedates()
+ {
+ return $this->taskRecurrenceModel->getRecurrenceBasedateList();
+ }
+
+ public function renderTitleField(array $values, array $errors)
+ {
+ return $this->helper->form->text(
+ 'title',
+ $values,
+ $errors,
+ array(
+ 'autofocus',
+ 'required',
+ 'maxlength="200"',
+ 'tabindex="1"',
+ 'placeholder="'.t('Title').'"'
+ )
+ );
+ }
+
+ public function renderDescriptionField(array $values, array $errors)
+ {
+ return $this->helper->form->textEditor('description', $values, $errors, array('tabindex' => 2));
+ }
+
+ public function renderDescriptionTemplateDropdown($projectId)
+ {
+ $templates = $this->predefinedTaskDescriptionModel->getAll($projectId);
+
+ if (! empty($templates)) {
+ $html = '<div class="dropdown dropdown-smaller">';
+ $html .= '<a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-floppy-o fa-fw" aria-hidden="true"></i>'.t('Template for the task description').' <i class="fa fa-caret-down" aria-hidden="true"></i></a>';
+ $html .= '<ul>';
+
+ foreach ($templates as $template) {
+ $html .= '<li>';
+ $html .= '<a href="#" data-template-target="textarea[name=description]" data-template="'.$this->helper->text->e($template['description']).'" class="js-template">';
+ $html .= $this->helper->text->e($template['title']);
+ $html .= '</a>';
+ $html .= '</li>';
+ }
+
+ $html .= '</ul></div>';
+ return $html;
+ }
+
+ return '';
+ }
+
+ public function renderTagField(array $project, array $tags = array())
+ {
+ $options = $this->tagModel->getAssignableList($project['id']);
+
+ $html = $this->helper->form->label(t('Tags'), 'tags[]');
+ $html .= '<input type="hidden" name="tags[]" value="">';
+ $html .= '<select name="tags[]" id="form-tags" class="tag-autocomplete" multiple>';
+
+ foreach ($options as $tag) {
+ $html .= sprintf(
+ '<option value="%s" %s>%s</option>',
+ $this->helper->text->e($tag),
+ in_array($tag, $tags) ? 'selected="selected"' : '',
+ $this->helper->text->e($tag)
+ );
+ }
+
+ $html .= '</select>';
+
+ return $html;
+ }
+
+ public function renderColorField(array $values)
+ {
+ $colors = $this->colorModel->getList();
+ $html = $this->helper->form->label(t('Color'), 'color_id');
+ $html .= $this->helper->form->select('color_id', $colors, $values, array(), array(), 'color-picker');
+ return $html;
+ }
+
+ public function renderAssigneeField(array $users, array $values, array $errors = array(), array $attributes = array())
+ {
+ if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) {
+ return '';
+ }
+
+ $attributes = array_merge(array('tabindex="3"'), $attributes);
+
+ $html = $this->helper->form->label(t('Assignee'), 'owner_id');
+ $html .= $this->helper->form->select('owner_id', $users, $values, $errors, $attributes);
+ $html .= '&nbsp;';
+ $html .= '<small>';
+ $html .= '<a href="#" class="assign-me" data-target-id="form-owner_id" data-current-id="'.$this->userSession->getId().'" title="'.t('Assign to me').'">'.t('Me').'</a>';
+ $html .= '</small>';
+
+ return $html;
+ }
+
+ public function renderMultiAssigneeField(array $users, array $values, array $errors = array(), array $attributes = array())
+ {
+ if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) {
+ return '';
+ }
+
+ $attributes = array_merge(array('tabindex="4"'), $attributes);
+ $name = 'owner_ms';
+
+ $html = $this->helper->form->label(t('Other Assignees'), $name.'[]');
+
+ $html .= '<select class="group-assign-select" multiple="multiple" size="3" name="'.$name.'[]" id="form-'.$name.'" '.implode(' ', $attributes).'>';
+
+ foreach ($users as $id => $value) {
+ if($id !== 0){
+ $html .= '<option value="'.$this->helper->text->e($id).'"';
+ if (isset($values->$name)) {
+ $multiusers = $this->multiselectMemberModel->getMembers($values->$name);
+ foreach ($multiusers as $member) {
+ if ($member['user_id'] == $id){ $html .= ' selected="selected"'; break; }
+ }
+ }
+ if (isset($values[$name])) {
+ $multiusers = $this->multiselectMemberModel->getMembers($values[$name]);
+ foreach ($multiusers as $member) {
+ if ($member['user_id'] == $id){ $html .= ' selected="selected"'; break; }
+ }
+ }
+
+ $html .= '>'.$this->helper->text->e($value).'</option>';
+ }
+ }
+ $html .= '</select>';
+
+ return $html;
+ }
+
+ public function renderGroupField(array $values, array $errors = array(), array $attributes = array())
+ {
+ if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) {
+ return '';
+ }
+ $groups = $this->projectGroupRoleModel->getGroups($values['project_id']);
+ $groupnames = array();
+ $groupids = array();
+
+ $groupids[] = 0;
+ $groupnames[] = t('Unassigned');
+
+
+ foreach ($groups as $group) {
+ // array_splice($groupnames, 1, 0, $group['name']);
+ $groupnames[] = $group['name'];
+ $groupids[] = $group['id'];
+ }
+
+ $groupvalues = array_combine($groupids, $groupnames);
+
+
+ $attributes = array_merge(array('tabindex="4"'), $attributes);
+
+ $html = $this->helper->form->label(t('Assigned Group'), 'owner_gp');
+ $html .= $this->helper->form->select('owner_gp', $groupvalues, $values, $errors, $attributes);
+ $html .= '&nbsp;';
+
+ return $html;
+ }
+
+ public function renderCategoryField(array $categories, array $values, array $errors = array(), array $attributes = array(), $allow_one_item = false)
+ {
+ $attributes = array_merge(array('tabindex="5"'), $attributes);
+ $html = '';
+
+ if (! (! $allow_one_item && count($categories) === 1 && key($categories) == 0)) {
+ $html .= $this->helper->form->label(t('Category'), 'category_id');
+ $html .= $this->helper->form->select('category_id', $categories, $values, $errors, $attributes);
+ }
+
+ return $html;
+ }
+
+ public function renderSwimlaneField(array $swimlanes, array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="6"'), $attributes);
+ $html = '';
+
+ if (count($swimlanes) > 1) {
+ $html .= $this->helper->form->label(t('Swimlane'), 'swimlane_id');
+ $html .= $this->helper->form->select('swimlane_id', $swimlanes, $values, $errors, $attributes);
+ }
+
+ return $html;
+ }
+
+ public function renderColumnField(array $columns, array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="7"'), $attributes);
+
+ $html = $this->helper->form->label(t('Column'), 'column_id');
+ $html .= $this->helper->form->select('column_id', $columns, $values, $errors, $attributes);
+
+ return $html;
+ }
+
+ public function renderPriorityField(array $project, array $values)
+ {
+ $range = range($project['priority_start'], $project['priority_end']);
+ $options = array_combine($range, $range);
+ $values += array('priority' => $project['priority_default']);
+
+ $html = $this->helper->form->label(t('Priority'), 'priority');
+ $html .= $this->helper->form->select('priority', $options, $values, array(), array('tabindex="8"'));
+
+ return $html;
+ }
+
+ public function renderScoreField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="13"'), $attributes);
+
+ $html = $this->helper->form->label(t('Complexity'), 'score');
+ $html .= $this->helper->form->number('score', $values, $errors, $attributes);
+
+ return $html;
+ }
+
+ public function renderReferenceField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="14"'), $attributes);
+
+ $html = $this->helper->form->label(t('Reference'), 'reference');
+ $html .= $this->helper->form->text('reference', $values, $errors, $attributes, 'form-input-small');
+
+ return $html;
+ }
+
+ public function renderTimeEstimatedField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="11"'), $attributes);
+
+ $html = $this->helper->form->label(t('Original estimate'), 'time_estimated');
+ $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes);
+ $html .= ' '.t('hours');
+
+ return $html;
+ }
+
+ public function renderTimeSpentField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="12"'), $attributes);
+
+ $html = $this->helper->form->label(t('Time spent'), 'time_spent');
+ $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes);
+ $html .= ' '.t('hours');
+
+ return $html;
+ }
+
+ public function renderStartDateField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="10"'), $attributes);
+ return $this->helper->form->datetime(t('Start Date'), 'date_started', $values, $errors, $attributes);
+ }
+
+ public function renderDueDateField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="9"'), $attributes);
+ return $this->helper->form->datetime(t('Due Date'), 'date_due', $values, $errors, $attributes);
+ }
+
+ public function renderPriority($priority)
+ {
+ $html = '<span class="task-priority" title="'.t('Task priority').'">';
+ $html .= $this->helper->text->e($priority >= 0 ? 'P'.$priority : '-P'.abs($priority));
+ $html .= '</span>';
+
+ return $html;
+ }
+
+ public function renderReference(array $task)
+ {
+ if (! empty($task['reference'])) {
+ $reference = $this->helper->text->e($task['reference']);
+
+ if (filter_var($task['reference'], FILTER_VALIDATE_URL) !== false) {
+ return sprintf('<a href="%s" target=_blank">%s</a>', $reference, $reference);
+ }
+
+ return $reference;
+ }
+
+ return '';
+ }
+
+ public function getProgress($task)
+ {
+ if (! isset($this->columns[$task['project_id']])) {
+ $this->columns[$task['project_id']] = $this->columnModel->getList($task['project_id']);
+ }
+
+ return $this->taskModel->getProgress($task, $this->columns[$task['project_id']]);
+ }
+
+ public function getNewBoardTaskButton(array $swimlane, array $column)
+ {
+ $html = '<div class="board-add-icon">';
+ $providers = $this->externalTaskManager->getProviders();
+
+ if (empty($providers)) {
+ $html .= $this->helper->modal->largeIcon(
+ 'plus',
+ t('Add a new task'),
+ 'TaskCreationController',
+ 'show', array(
+ 'project_id' => $column['project_id'],
+ 'column_id' => $column['id'],
+ 'swimlane_id' => $swimlane['id'],
+ )
+ );
+ } else {
+ $html .= '<div class="dropdown">';
+ $html .= '<a href="#" class="dropdown-menu"><i class="fa fa-plus" aria-hidden="true"></i></a><ul>';
+
+ $link = $this->helper->modal->large(
+ 'plus',
+ t('Add a new Kanboard task'),
+ 'TaskCreationController',
+ 'show', array(
+ 'project_id' => $column['project_id'],
+ 'column_id' => $column['id'],
+ 'swimlane_id' => $swimlane['id'],
+ )
+ );
+
+ $html .= '<li>'.$link.'</li>';
+
+ foreach ($providers as $provider) {
+ $link = $this->helper->url->link(
+ $provider->getMenuAddLabel(),
+ 'ExternalTaskCreationController',
+ 'step1',
+ array('project_id' => $column['project_id'], 'swimlane_id' => $swimlane['id'], 'column_id' => $column['id'], 'provider_name' => $provider->getName()),
+ false,
+ 'js-modal-large'
+ );
+
+ $html .= '<li>'.$provider->getIcon().' '.$link.'</li>';
+ }
+
+ $html .= '</ul></div>';
+ }
+
+ $html .= '</div>';
+
+ return $html;
+ }
+}
diff --git a/plugins/Group_assign/Helper/SmallAvatarHelperExtend.php b/plugins/Group_assign/Helper/SmallAvatarHelperExtend.php
new file mode 100644
index 00000000..1ee35c98
--- /dev/null
+++ b/plugins/Group_assign/Helper/SmallAvatarHelperExtend.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Helper;
+
+use Kanboard\Helper\AvatarHelper;
+use Kanboard\Core\Base;
+/**
+ * Avatar Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class SmallAvatarHelperExtend extends AvatarHelper
+{
+
+ public function smallMultiple($owner_ms, $css = '') {
+ $assignees = $this->multiselectMemberModel->getMembers($owner_ms);
+ $html = "";
+ foreach ($assignees as $assignee) {
+ $user = $this->userModel->getById($assignee['user_id']);
+ $html .= $this->render($assignee['user_id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css, 20);
+ }
+ return $html;
+ }
+
+ public function miniMultiple($owner_ms, $css = '') {
+ $assignees = $this->multiselectMemberModel->getMembers($owner_ms);
+ $html = "";
+ foreach ($assignees as $assignee) {
+ $user = $this->userModel->getById($assignee['user_id']);
+ $html .= $this->render($assignee['user_id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css, 13);
+ }
+ return $html;
+ }
+ }
diff --git a/plugins/Group_assign/LICENSE b/plugins/Group_assign/LICENSE
new file mode 100644
index 00000000..b2e46df8
--- /dev/null
+++ b/plugins/Group_assign/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 creecros
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/Group_assign/Locale/de_DE/translations.php b/plugins/Group_assign/Locale/de_DE/translations.php
new file mode 100644
index 00000000..eb5128ba
--- /dev/null
+++ b/plugins/Group_assign/Locale/de_DE/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Assigned Group' => 'Zugeordnete Gruppe',
+ 'Assigned Group:' => 'Zugeordnete Gruppe:',
+ 'Other Assignees' => 'Weitere Zuordnungen',
+ 'Other Assignees:' => 'Weitere Zuordnungen:',
+ 'Send a task by email to assigned group members' => 'Senden einer Aufgabe per E-Mail an zugewiesene Gruppenmitglieder',
+ 'Send a task by email to the other assignees for a task.' => 'Aufgabe per E-Mail an die weiteren zugeordneten der Aufgabe senden',
+ 'Send email notification of impending due date to Group Members Assigned' => 'E-Mail-Benachrichtigung über bevorstehendes Fälligkeitsdatum an die zugewiesenen Gruppenmitglieder senden',
+ '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',
+ 'Assign the task to a specific group' => 'Aufgabe einer bestimmten Gruppe zuordnen'
+);
diff --git a/plugins/Group_assign/Locale/pt_BR/translations.php b/plugins/Group_assign/Locale/pt_BR/translations.php
new file mode 100644
index 00000000..fbbdf207
--- /dev/null
+++ b/plugins/Group_assign/Locale/pt_BR/translations.php
@@ -0,0 +1,97 @@
+<?php
+
+return array(
+ 'Action' => 'Ação',
+ 'Action date' => 'Data de ação',
+ 'Add a new Kanboard task' => 'Adicionar uma nova tarefa do Kanboard',
+ 'Add a new task' => 'Adicionar uma nova tarefa',
+ 'All tasks' => 'Todas as tarefas',
+ 'Assignee' => 'Responsável',
+ 'Assigned Group' => 'Grupo Designado',
+ 'Assigned Group:' => 'Grupo Designado:',
+ 'Assign to me' => 'Atribuir para mim',
+ 'Assign the task to a specific group' => 'Atribuir uma tarefa para um grupo específico',
+ 'Category' => 'Categoria',
+ 'Create another task' => 'Criar outra tarefa',
+ 'Column' => 'Coluna',
+ 'Color' => 'Cor',
+ 'Complexity' => 'Complexidade',
+ 'Day(s)' => 'Dia(s)',
+ 'Documentation' => 'Documentação',
+ 'Due Date' => 'Data de Vencimento',
+ 'Duplicate to multiple projects' => 'Duplicar para múltiplos projetos',
+ 'Duration in days' => 'Duração em dias',
+ 'Email subject' => 'Assunto do email',
+ '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.',
+ 'Event' => 'Evento',
+ 'Existing due date' => 'Data de vencimeto existente',
+ 'Group' => 'Grupo',
+ 'Groups management' => 'Gestão de grupos',
+ 'hours' => 'horas',
+ 'Logout' => 'Sair',
+ 'Me' => 'Eu',
+ 'Month(s)' => 'Mês(es)',
+ 'My dashboard' => 'Meu painel',
+ 'My profile' => 'Meu perfil',
+ 'New assignee: %s' => 'Novo responsável: %s',
+ 'New category: %s' => 'Nova categoria: %s',
+ 'New color: %s' => 'Nova cor: %s',
+ 'New complexity: %d' => 'Nova complexidade: %d',
+ 'New due date: ' => 'Nova data de vencimento: ',
+ 'New group assigned: %s' => 'Novo grupo atribuido: %s',
+ 'New task' => 'Nova tarefa',
+ 'New title: %s' => 'Novo título: %s',
+ 'No' => 'Não',
+ 'not assigned' => 'não atribuído',
+ 'Only for tasks assigned to me' => 'Apenas tarefas atribuídas a mim',
+ 'Only for tasks created by me' => 'Apenas tarefas criadas por mim',
+ 'Only for tasks created by me and tasks assigned to me' => 'Apenas tarefas criadas por mim ou tarefas atribuídas a mim',
+ 'Options' => 'Opções',
+ 'Original estimate' => 'Estimativa original',
+ 'Other Assignees' => 'Outros Responsáveis',
+ 'Other Assignees:' => 'Outros Responsáveis:',
+ 'Priority' => 'Prioridade',
+ 'Projects management' => 'Gestão de projetos',
+ 'Recurrence settings have been modified' => 'Configurações de recorrência foram modificadas',
+ 'Reference' => 'Referência',
+ 'Send a task by email to assigned group members' => 'Enviar uma tarefa por email para membros do grupo designado',
+ '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',
+ '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',
+ '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',
+ 'Settings' => 'Definições',
+ 'Start Date' => 'Data de Início',
+ 'Start date changed: ' => 'Data de início alterada: ',
+ 'Swimlane' => 'Raia',
+ 'Tags' => 'Etiquetas',
+ 'Task created successfully.' => 'Tarefa criada com sucesso.',
+ 'Task priority' => 'Prioridade da tarefa',
+ 'Task updated successfully.' => 'Tarefa atualizada com sucesso',
+ 'Template for the task description' => 'Modelo para a descrição da tarefa',
+ 'The description has been modified:' => 'A descrição foi modificada:',
+ 'The due date have been removed' => 'A data de vencimento foi removida',
+ 'The field "%s" have been updated' => 'O campo "%s" foi atualizado',
+ 'The task is not assigned anymore' => 'A tarefa não está mais atribuída',
+ 'The task is not assigned to a group anymore' => 'A tarefa não está mais atribuída para um grupo',
+ 'The task is not assigned to multiple users anymore' => 'A tarefa não está mais atribuída para múltiplos usuários',
+ 'The task has been assigned other users' => 'A tarefa foi atribuída para outros usuários',
+ 'There is no category now' => 'Não há categoria agora',
+ 'There is no description anymore' => 'Não há descrição agora',
+ 'Time estimated changed: %sh' => 'Tempo estimado alterado: %sh',
+ 'Time spent' => 'Tempo gasto',
+ 'Time spent changed: %sh' => 'Tempo gasto alterado: %sh',
+ 'Title' => 'Título',
+ 'Unable to create this task.' => 'Incapaz de criar esta tarefa',
+ 'Unable to create your task.' => 'Incapaz de criar a sua tarefa.',
+ 'Unable to update your task.' => 'Incapaz de atualizar sua tarefa',
+ 'Unassigned' => 'Não atribuido',
+ 'Users management' => 'Gestão de usuários',
+ 'When task is moved from first column' => 'Quando a tarefa é movida da primeira coluna',
+ 'When task is moved to last column' => 'Quando a tarefa é movida para a última coluna',
+ 'When task is closed' => 'Quando a tarefa é fechada',
+ 'Year(s)' => 'Ano(s)',
+ 'Yes' => 'Sim',
+ 'You are not allowed to change the assignee.' => 'Você não tem permissão para mudar o responsável.',
+ 'You are not allowed to update tasks assigned to someone else.' => 'Você não tem permissão para atualizar tarefas designadas para outras pessoas.',
+ 'You cannot create tasks in this column.' => 'Você não pode criar tarefas nesta coluna.',
+ '[DUPLICATE]' => '[DUPLICADO]',
+);
diff --git a/plugins/Group_assign/Model/GroupAssignCalendarModel.php b/plugins/Group_assign/Model/GroupAssignCalendarModel.php
new file mode 100644
index 00000000..5b45ca09
--- /dev/null
+++ b/plugins/Group_assign/Model/GroupAssignCalendarModel.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use DateTime;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\TimezoneModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Core\Base;
+
+/**
+ * Group_assign Calendar Model
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class GroupAssignCalendarModel extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'tasks';
+ /**
+ * Get query to fetch all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return \PicoDb\Table
+ */
+ public function getUserCalendarEvents($user_id, $start, $end)
+ {
+ $getMS_Ids = $this->db->table(MultiselectMemberModel::TABLE)
+ ->eq('user_id', $user_id)
+ ->findAllByColumn('group_id');
+
+ $getGr_Ids = $this->db->table(GroupMemberModel::TABLE)
+ ->eq('user_id', $user_id)
+ ->findAllByColumn('group_id');
+
+ $tasks = $this->db->table(self::TABLE)
+ ->beginOr()
+ ->eq('owner_id', $user_id)
+ ->in('owner_gp', $getGr_Ids)
+ ->in('owner_ms', $getMS_Ids)
+ ->closeOr()
+ ->gte('date_due', strtotime($start))
+ ->lte('date_due', strtotime($end))
+ ->neq('is_active', 0);
+
+ $tasks = $tasks->findAll();
+
+ $events = array();
+
+ foreach ($tasks as $task) {
+
+ $startDate = new DateTime();
+ $startDate->setTimestamp($task['date_started']);
+
+ $endDate = new DateTime();
+ $endDate->setTimestamp($task['date_due']);
+
+ if ($startDate->getTimestamp() == 0) { $startDate = $endDate; }
+
+ $allDay = $startDate == $endDate && $endDate->format('Hi') == '0000';
+ $format = $allDay ? 'Y-m-d' : 'Y-m-d\TH:i:s';
+
+ $events[] = array(
+ 'timezoneParam' => $this->timezoneModel->getCurrentTimezone(),
+ 'id' => $task['id'],
+ 'title' => t('#%d', $task['id']).' '.$task['title'],
+ 'backgroundColor' => $this->colorModel->getBackgroundColor('dark_grey'),
+ 'borderColor' => $this->colorModel->getBorderColor($task['color_id']),
+ 'textColor' => 'white',
+ 'url' => $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
+ 'start' => $startDate->format($format),
+ 'end' => $endDate->format($format),
+ 'editable' => $allDay,
+ 'allday' => $allDay,
+ );
+ }
+
+ return $events;
+ }
+
+}
diff --git a/plugins/Group_assign/Model/GroupAssignTaskDuplicationModel.php b/plugins/Group_assign/Model/GroupAssignTaskDuplicationModel.php
new file mode 100644
index 00000000..179f6630
--- /dev/null
+++ b/plugins/Group_assign/Model/GroupAssignTaskDuplicationModel.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\TagDuplicationModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ProjectTaskPriorityModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\SubtaskModel;
+use Kanboard\Core\Base;
+
+/**
+ * Task Duplication
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class GroupAssignTaskDuplicationModel extends Base
+{
+ /**
+ * Fields to copy when duplicating a task
+ *
+ * @access protected
+ * @var string[]
+ */
+ protected $fieldsToDuplicate = array(
+ 'title',
+ 'description',
+ 'date_due',
+ 'color_id',
+ 'project_id',
+ 'column_id',
+ 'owner_id',
+ 'score',
+ 'priority',
+ 'category_id',
+ 'time_estimated',
+ 'swimlane_id',
+ 'recurrence_status',
+ 'recurrence_trigger',
+ 'recurrence_factor',
+ 'recurrence_timeframe',
+ 'recurrence_basedate',
+ 'external_provider',
+ 'external_uri',
+ 'reference',
+ 'owner_ms',
+ 'owner_gp',
+ );
+
+ /**
+ * Duplicate a task to the same project
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean|integer Duplicated task id
+ */
+ public function duplicate($task_id)
+ {
+ $values = $this->copyFields($task_id);
+ $values['title'] = t('[DUPLICATE]').' '.$values['title'];
+
+ $new_task_id = $this->save($task_id, $values);
+
+ if ($new_task_id !== false) {
+ $this->tagDuplicationModel->duplicateTaskTags($task_id, $new_task_id);
+ $this->taskLinkModel->create($new_task_id, $task_id, 4);
+ }
+
+ return $new_task_id;
+ }
+
+ /**
+ * Check if the assignee and the category are available in the destination project
+ *
+ * @access public
+ * @param array $values
+ * @return array
+ */
+ public function checkDestinationProjectValues(array &$values)
+ {
+ // Check if the assigned user is allowed for the destination project
+ if ($values['owner_id'] > 0 && ! $this->projectPermissionModel->isUserAllowed($values['project_id'], $values['owner_id'])) {
+ $values['owner_id'] = 0;
+ }
+
+ // Check if the category exists for the destination project
+ if ($values['category_id'] > 0) {
+ $values['category_id'] = $this->categoryModel->getIdByName(
+ $values['project_id'],
+ $this->categoryModel->getNameById($values['category_id'])
+ );
+ }
+
+ // Check if the swimlane exists for the destination project
+ $values['swimlane_id'] = $this->swimlaneModel->getIdByName(
+ $values['project_id'],
+ $this->swimlaneModel->getNameById($values['swimlane_id'])
+ );
+
+ if ($values['swimlane_id'] == 0) {
+ $values['swimlane_id'] = $this->swimlaneModel->getFirstActiveSwimlaneId($values['project_id']);
+ }
+
+ // Check if the column exists for the destination project
+ if ($values['column_id'] > 0) {
+ $values['column_id'] = $this->columnModel->getColumnIdByTitle(
+ $values['project_id'],
+ $this->columnModel->getColumnTitleById($values['column_id'])
+ );
+
+ $values['column_id'] = $values['column_id'] ?: $this->columnModel->getFirstColumnId($values['project_id']);
+ }
+
+ // Check if priority exists for destination project
+ $values['priority'] = $this->projectTaskPriorityModel->getPriorityForProject(
+ $values['project_id'],
+ empty($values['priority']) ? 0 : $values['priority']
+ );
+
+ return $values;
+ }
+
+ /**
+ * Duplicate fields for the new task
+ *
+ * @access protected
+ * @param integer $task_id Task id
+ * @return array
+ */
+ protected function copyFields($task_id)
+ {
+ $task = $this->taskFinderModel->getById($task_id);
+ $values = array();
+
+ foreach ($this->fieldsToDuplicate as $field) {
+ $values[$field] = $task[$field];
+ }
+
+ $ms_id = $this->multiselectModel->create();
+ $users_in_ms = $this->multiselectMemberModel->getMembers($values['owner_ms']);
+ $values['owner_ms'] = $ms_id;
+ foreach ($users_in_ms as $user) {
+ $this->multiselectMemberModel->addUser($ms_id, $user['id']);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create the new task and duplicate subtasks
+ *
+ * @access protected
+ * @param integer $task_id Task id
+ * @param array $values Form values
+ * @return boolean|integer
+ */
+ protected function save($task_id, array $values)
+ {
+ $new_task_id = $this->taskCreationModel->create($values);
+
+ if ($new_task_id !== false) {
+ $this->subtaskModel->duplicate($task_id, $new_task_id);
+ }
+
+ return $new_task_id;
+ }
+}
diff --git a/plugins/Group_assign/Model/GroupColorExtension.php b/plugins/Group_assign/Model/GroupColorExtension.php
new file mode 100644
index 00000000..e8e19850
--- /dev/null
+++ b/plugins/Group_assign/Model/GroupColorExtension.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\GroupModel;
+
+class GroupColorExtension extends GroupModel
+{
+
+ public function getGroupColor($str) {
+ $code = dechex(crc32($str));
+ $code = substr($code, 0, 6);
+ return $code;
+ }
+
+ public function getFontColor($hex) {
+ // returns brightness value from 0 to 255
+ // strip off any leading #
+ $hex = str_replace('#', '', $hex);
+
+ $c_r = hexdec(substr($hex, 0, 2));
+ $c_g = hexdec(substr($hex, 2, 2));
+ $c_b = hexdec(substr($hex, 4, 2));
+
+ $brightness = (($c_r * 299) + ($c_g * 587) + ($c_b * 114)) / 1000;
+ if ($brightness > 130) { return 'black'; } else { return 'white'; }
+ }
+}
diff --git a/plugins/Group_assign/Model/MultiselectMemberModel.php b/plugins/Group_assign/Model/MultiselectMemberModel.php
new file mode 100644
index 00000000..ce0f3ace
--- /dev/null
+++ b/plugins/Group_assign/Model/MultiselectMemberModel.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Core\Queue\QueueManager;
+use Kanboard\Core\Base;
+
+/**
+ * Multiselect Member Model
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class MultiselectMemberModel extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'multiselect_has_users';
+
+ /**
+ * Get query to fetch all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return \PicoDb\Table
+ */
+ public function getQuery($group_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->join(UserModel::TABLE, 'id', 'user_id')
+ ->eq('group_id', $group_id);
+ }
+
+ /**
+ * Get all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getMembers($group_id)
+ {
+ return $this->getQuery($group_id)->findAll();
+ }
+
+ /**
+ * Get all not members
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getNotMembers($group_id)
+ {
+ $subquery = $this->db->table(self::TABLE)
+ ->columns('user_id')
+ ->eq('group_id', $group_id);
+
+ return $this->db->table(UserModel::TABLE)
+ ->notInSubquery('id', $subquery)
+ ->eq('is_active', 1)
+ ->findAll();
+ }
+
+ /**
+ * Add user to a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function addUser($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'group_id' => $group_id,
+ 'user_id' => $user_id,
+ ));
+ }
+
+ /**
+ * Remove user from a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeUser($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->remove();
+ }
+
+ /**
+ * Remove all users from a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeAllUsers($group_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->remove();
+ }
+
+ /**
+ * Check if a user is member
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function isMember($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->exists();
+ }
+
+ /**
+ * Get all groups for a given user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getGroups($user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(MultiselectModel::TABLE.'.id', MultiselectModel::TABLE.'.external_id')
+ ->join(MultiselectModel::TABLE, 'id', 'group_id')
+ ->eq(self::TABLE.'.user_id', $user_id)
+ ->asc(MultiselectModel::TABLE.'.id')
+ ->findAll();
+ }
+
+ /**
+ * Fire Assignee Change
+ *
+ * @access protected
+ * @param array $task
+ * @param array $changes
+ */
+ public function assigneeChanged(array $task, array $changes)
+ {
+ $events = array();
+ $events[] = TaskModel::EVENT_ASSIGNEE_CHANGE;
+
+ if (! empty($events)) {
+ $this->queueManager->push($this->taskEventJob
+ ->withParams($task['id'], $events, $changes, array(), $task)
+ );
+ }
+ }
+}
diff --git a/plugins/Group_assign/Model/MultiselectModel.php b/plugins/Group_assign/Model/MultiselectModel.php
new file mode 100644
index 00000000..36f1a9d1
--- /dev/null
+++ b/plugins/Group_assign/Model/MultiselectModel.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Core\Base;
+
+/**
+ * Multiselect Model
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class MultiselectModel extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'multiselect';
+
+ /**
+ * Get query to fetch all groups
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->db->table(self::TABLE)
+ ->columns('id', 'external_id')
+ ->subquery('SELECT COUNT(*) FROM '.MultiselectMemberModel::TABLE.' WHERE group_id='.self::TABLE.'.id', 'nb_users');
+ }
+
+ /**
+ * Get a specific group by id
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getById($group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $group_id)->findOne();
+ }
+
+ /**
+ * Get a specific group by externalID
+ *
+ * @access public
+ * @param string $external_id
+ * @return array
+ */
+ public function getByExternalId($external_id)
+ {
+ return $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOne();
+ }
+
+ /**
+ * Get specific groups by externalIDs
+ *
+ * @access public
+ * @param string[] $external_ids
+ * @return array
+ */
+ public function getByExternalIds(array $external_ids)
+ {
+ if (empty($external_ids)) {
+ return [];
+ }
+
+ return $this->db->table(self::TABLE)->in('external_id', $external_ids)->findAll();
+ }
+
+ /**
+ * Get all groups
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->getQuery()->asc('id')->findAll();
+ }
+
+ /**
+ * Remove a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @return boolean
+ */
+ public function remove($group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $group_id)->remove();
+ }
+
+ /**
+ * Create a new group
+ *
+ * @access public
+ * @param string $external_id
+ * @return integer|boolean
+ */
+ public function create($external_id = '')
+ {
+ return $this->db->table(self::TABLE)->persist(array(
+ 'external_id' => $external_id,
+ ));
+ }
+
+ /**
+ * Update existing group
+ *
+ * @access public
+ * @param array $values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
+ }
+
+ /**
+ * Get groupId from externalGroupId and create the group if not found
+ *
+ * @access public
+ * @param string $name
+ * @param string $external_id
+ * @return bool|integer
+ */
+ public function getOrCreateExternalGroupId($name, $external_id)
+ {
+ $group_id = $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOneColumn('id');
+
+ if (empty($group_id)) {
+ $group_id = $this->create($external_id);
+ }
+
+ return $group_id;
+ }
+}
diff --git a/plugins/Group_assign/Model/NewMetaMagikSubquery.php b/plugins/Group_assign/Model/NewMetaMagikSubquery.php
new file mode 100644
index 00000000..4870120e
--- /dev/null
+++ b/plugins/Group_assign/Model/NewMetaMagikSubquery.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\NewTaskFinderModel;
+
+/**
+ * New Task Finder model
+ * Extends Group_assign Model
+ *
+ * @package Kanboard\Plugin\Group_assign\Model
+ */
+class NewMetaMagikSubQuery extends NewTaskFinderModel
+{
+ const METADATA_TABLE = 'task_has_metadata';
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
+ {
+ // add subquery to original Model, changing only what we want
+ return parent::getExtendedQuery()
+ ->subquery('(SELECT COUNT(*) FROM '.self::METADATA_TABLE.' WHERE task_id=tasks.id)', 'nb_metadata');
+ }
+} \ No newline at end of file
diff --git a/plugins/Group_assign/Model/NewTaskFinderModel.php b/plugins/Group_assign/Model/NewTaskFinderModel.php
new file mode 100644
index 00000000..b8b10916
--- /dev/null
+++ b/plugins/Group_assign/Model/NewTaskFinderModel.php
@@ -0,0 +1,491 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\ActionParameterModel;
+use Kanboard\Model\AvatarFileModel;
+use Kanboard\Model\BoardModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ColumnMoveRestrictionModel;
+use Kanboard\Model\CommentModel;
+use Kanboard\Model\ConfigModel;
+use Kanboard\Model\CurrencyModel;
+use Kanboard\Model\CustomFilterModel;
+use Kanboard\Model\FileModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Model\GroupModel;
+use Kanboard\Model\LanguageModel;
+use Kanboard\Model\LastLoginModel;
+use Kanboard\Model\LinkModel;
+use Kanboard\Model\MetadataModel;
+use Kanboard\Model\NotificationModel;
+use Kanboard\Model\NotificationTypeModel;
+use Kanboard\Model\PasswordResetModel;
+use Kanboard\Model\ProjectActivityModel;
+use Kanboard\Model\ProjectDailyColumnStatsModel;
+use Kanboard\Model\ProjectDailyStatsModel;
+use Kanboard\Model\ProjectDuplicationModel;
+use Kanboard\Model\ProjectFileModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Model\ProjectMetadataModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\ProjectNotificationModel;
+use Kanboard\Model\ProjectNotificationTypeModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\ProjectRoleModel;
+use Kanboard\Model\ProjectRoleRestrictionModel;
+use Kanboard\Model\ProjectTaskDuplicationModel;
+use Kanboard\Model\ProjectTaskPriorityModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\RememberMeSessionModel;
+use Kanboard\Model\SettingModel;
+use Kanboard\Model\SubtaskModel;
+use Kanboard\Model\SubtaskPositionModel;
+use Kanboard\Model\SubtaskStatusModel;
+use Kanboard\Model\SubtaskTaskConversionModel;
+use Kanboard\Model\SubtaskTimeTrackingModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\TagDuplicationModel;
+use Kanboard\Model\TagModel;
+use Kanboard\Model\TaskAnalyticModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskExternalLinkModel;
+use Kanboard\Model\TaskFileModel;
+use Kanboard\Model\TaskLinkModel;
+use Kanboard\Model\TaskMetadataModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskModificationModel;
+use Kanboard\Model\TaskPositionModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskProjectMoveModel;
+use Kanboard\Model\TaskRecurrenceModel;
+use Kanboard\Model\TaskStatusModel;
+use Kanboard\Model\TaskTagModel;
+use Kanboard\Model\TimezoneModel;
+use Kanboard\Model\TransitionModel;
+use Kanboard\Model\UserLockingModel;
+use Kanboard\Model\UserMentionModel;
+use Kanboard\Model\UserMetadataModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\UserNotificationFilterModel;
+use Kanboard\Model\UserNotificationModel;
+use Kanboard\Model\UserNotificationTypeModel;
+use Kanboard\Model\UserUnreadNotificationModel;
+use Kanboard\Core\Base;
+
+
+class NewTaskFinderModel extends Base
+{
+ /**
+ * Get query for project user overview
+ *
+ * @access public
+ * @param array $project_ids
+ * @param integer $is_active
+ * @return \PicoDb\Table
+ */
+ public function getProjectUserOverviewQuery(array $project_ids, $is_active)
+ {
+ if (empty($project_ids)) {
+ $project_ids = array(-1);
+ }
+
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.date_started',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.color_id',
+ TaskModel::TABLE.'.priority',
+ TaskModel::TABLE.'.time_spent',
+ TaskModel::TABLE.'.time_estimated',
+ ProjectModel::TABLE.'.name AS project_name',
+ ColumnModel::TABLE.'.title AS column_name',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name'
+ )
+ ->eq(TaskModel::TABLE.'.is_active', $is_active)
+ ->in(ProjectModel::TABLE.'.id', $project_ids)
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE);
+ }
+
+ /**
+ * Get query for assigned user tasks
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return \PicoDb\Table
+ */
+ public function getUserQuery($user_id)
+ {
+ return $this->getExtendedQuery()
+ ->beginOr()
+ ->eq(TaskModel::TABLE.'.owner_id', $user_id)
+ ->addCondition(TaskModel::TABLE.".id IN (SELECT task_id FROM ".SubtaskModel::TABLE." WHERE ".SubtaskModel::TABLE.".user_id='$user_id')")
+ ->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$user_id')")
+ ->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$user_id')")
+ ->closeOr()
+ ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
+ ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE)
+ ->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0);
+ }
+
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->columns(
+ '(SELECT COUNT(*) FROM '.CommentModel::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
+ '(SELECT COUNT(*) FROM '.TaskFileModel::TABLE.' WHERE task_id=tasks.id) AS nb_files',
+ '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id) AS nb_subtasks',
+ '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links',
+ '(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id AND '.TaskLinkModel::TABLE.'.link_id = 9) AS is_milestone',
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.reference',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.description',
+ TaskModel::TABLE.'.date_creation',
+ TaskModel::TABLE.'.date_modification',
+ TaskModel::TABLE.'.date_completed',
+ TaskModel::TABLE.'.date_started',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.color_id',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.column_id',
+ TaskModel::TABLE.'.swimlane_id',
+ TaskModel::TABLE.'.owner_id',
+ TaskModel::TABLE.'.creator_id',
+ TaskModel::TABLE.'.position',
+ TaskModel::TABLE.'.is_active',
+ TaskModel::TABLE.'.score',
+ TaskModel::TABLE.'.category_id',
+ TaskModel::TABLE.'.priority',
+ TaskModel::TABLE.'.date_moved',
+ TaskModel::TABLE.'.recurrence_status',
+ TaskModel::TABLE.'.recurrence_trigger',
+ TaskModel::TABLE.'.recurrence_factor',
+ TaskModel::TABLE.'.recurrence_timeframe',
+ TaskModel::TABLE.'.recurrence_basedate',
+ TaskModel::TABLE.'.recurrence_parent',
+ TaskModel::TABLE.'.recurrence_child',
+ TaskModel::TABLE.'.time_estimated',
+ TaskModel::TABLE.'.time_spent',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name',
+ UserModel::TABLE.'.email AS assignee_email',
+ UserModel::TABLE.'.avatar_path AS assignee_avatar_path',
+ CategoryModel::TABLE.'.name AS category_name',
+ CategoryModel::TABLE.'.description AS category_description',
+ CategoryModel::TABLE.'.color_id AS category_color_id',
+ ColumnModel::TABLE.'.title AS column_name',
+ ColumnModel::TABLE.'.position AS column_position',
+ SwimlaneModel::TABLE.'.name AS swimlane_name',
+ ProjectModel::TABLE.'.name AS project_name',
+ TaskModel::TABLE.'.owner_ms',
+ GroupModel::TABLE.'.name AS assigned_groupname'
+ )
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
+ ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE)
+ ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE)
+ ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE);
+ }
+
+ /**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $status_id Status id
+ * @return array
+ */
+ public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.project_id', $project_id)
+ ->eq(TaskModel::TABLE.'.is_active', $status_id)
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAll();
+ }
+
+ /**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id
+ * @param array $status
+ * @return array
+ */
+ public function getAllIds($project_id, array $status = array(TaskModel::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.project_id', $project_id)
+ ->in(TaskModel::TABLE.'.is_active', $status)
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAllByColumn(TaskModel::TABLE.'.id');
+ }
+
+ /**
+ * Get overdue tasks query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getOverdueTasksQuery()
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.creator_id',
+ TaskModel::TABLE.'.owner_id',
+ ProjectModel::TABLE.'.name AS project_name',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name'
+ )
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->join(UserModel::TABLE, 'id', 'owner_id')
+ ->eq(ProjectModel::TABLE.'.is_active', 1)
+ ->eq(TaskModel::TABLE.'.is_active', 1)
+ ->neq(TaskModel::TABLE.'.date_due', 0)
+ ->lte(TaskModel::TABLE.'.date_due', time());
+ }
+
+ /**
+ * Get a list of overdue tasks for all projects
+ *
+ * @access public
+ * @return array
+ */
+ public function getOverdueTasks()
+ {
+ return $this->getOverdueTasksQuery()->findAll();
+ }
+
+ /**
+ * Get a list of overdue tasks by project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getOverdueTasksByProject($project_id)
+ {
+ return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.project_id', $project_id)->findAll();
+ }
+
+ /**
+ * Get a list of overdue tasks by user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getOverdueTasksByUser($user_id)
+ {
+ return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.owner_id', $user_id)->findAll();
+ }
+
+ /**
+ * Get project id for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return integer
+ */
+ public function getProjectId($task_id)
+ {
+ return (int) $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0;
+ }
+
+ /**
+ * Fetch a task by the id
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getById($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOne();
+ }
+
+ /**
+ * Fetch a task by the reference (external id)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $reference Task reference
+ * @return array
+ */
+ public function getByReference($project_id, $reference)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne();
+ }
+
+ /**
+ * Get task details (fetch more information from other tables)
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getDetails($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.*',
+ CategoryModel::TABLE.'.name AS category_name',
+ SwimlaneModel::TABLE.'.name AS swimlane_name',
+ ProjectModel::TABLE.'.name AS project_name',
+ ColumnModel::TABLE.'.title AS column_title',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name',
+ 'uc.username AS creator_username',
+ 'uc.name AS creator_name',
+ CategoryModel::TABLE.'.description AS category_description',
+ ColumnModel::TABLE.'.position AS column_position',
+ GroupModel::TABLE.'.name AS assigned_groupname',
+ ColumnModel::TABLE.'.position AS column_position'
+ )
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
+ ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE)
+ ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE)
+ ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.id', $task_id)
+ ->findOne();
+ }
+
+ /**
+ * Get iCal query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getICalQuery()
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'ua', 'id', TaskModel::TABLE, 'owner_id')
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->columns(
+ TaskModel::TABLE.'.*',
+ 'ua.email AS assignee_email',
+ 'ua.name AS assignee_name',
+ 'ua.username AS assignee_username',
+ 'uc.email AS creator_email',
+ 'uc.name AS creator_name',
+ 'uc.username AS creator_username'
+ );
+ }
+
+ /**
+ * Count all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param array $status List of status id
+ * @return integer
+ */
+ public function countByProjectId($project_id, array $status = array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Count the number of tasks for a given column and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param array $status
+ * @return int
+ */
+ public function countByColumnId($project_id, $column_id, array $status = array(TaskModel::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Count the number of tasks for a given column and swimlane
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param integer $swimlane_id Swimlane id
+ * @return integer
+ */
+ public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->eq('swimlane_id', $swimlane_id)
+ ->eq('is_active', 1)
+ ->count();
+ }
+
+ /**
+ * Return true if the task exists
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function exists($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->exists();
+ }
+
+ /**
+ * Get project token
+ *
+ * @access public
+ * @param integer $task_id
+ * @return string
+ */
+ public function getProjectToken($task_id)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.id', $task_id)
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->findOneColumn(ProjectModel::TABLE.'.token');
+ }
+}
diff --git a/plugins/Group_assign/Model/NewUserNotificationFilterModel.php b/plugins/Group_assign/Model/NewUserNotificationFilterModel.php
new file mode 100644
index 00000000..3fa67487
--- /dev/null
+++ b/plugins/Group_assign/Model/NewUserNotificationFilterModel.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\UserModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Core\Base;
+
+/**
+ * User Notification Filter
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class NewUserNotificationFilterModel extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const PROJECT_TABLE = 'user_has_notifications';
+
+ /**
+ * User filters
+ *
+ * @var integer
+ */
+ const FILTER_NONE = 1;
+ const FILTER_ASSIGNEE = 2;
+ const FILTER_CREATOR = 3;
+ const FILTER_BOTH = 4;
+
+ /**
+ * Get the list of filters
+ *
+ * @access public
+ * @return array
+ */
+ public function getFilters()
+ {
+ return array(
+ self::FILTER_NONE => t('All tasks'),
+ self::FILTER_ASSIGNEE => t('Only for tasks assigned to me'),
+ self::FILTER_CREATOR => t('Only for tasks created by me'),
+ self::FILTER_BOTH => t('Only for tasks created by me and tasks assigned to me'),
+ );
+ }
+
+ /**
+ * Get user selected filter
+ *
+ * @access public
+ * @param integer $user_id
+ * @return integer
+ */
+ public function getSelectedFilter($user_id)
+ {
+ return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter');
+ }
+
+ /**
+ * Save selected filter for a user
+ *
+ * @access public
+ * @param integer $user_id
+ * @param string $filter
+ * @return boolean
+ */
+ public function saveFilter($user_id, $filter)
+ {
+ return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array(
+ 'notifications_filter' => $filter,
+ ));
+ }
+
+ /**
+ * Get user selected projects
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getSelectedProjects($user_id)
+ {
+ return $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id');
+ }
+
+ /**
+ * Save selected projects for a user
+ *
+ * @access public
+ * @param integer $user_id
+ * @param integer[] $project_ids
+ * @return boolean
+ */
+ public function saveSelectedProjects($user_id, array $project_ids)
+ {
+ $results = array();
+ $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove();
+
+ foreach ($project_ids as $project_id) {
+ $results[] = $this->db->table(self::PROJECT_TABLE)->insert(array(
+ 'user_id' => $user_id,
+ 'project_id' => $project_id,
+ ));
+ }
+
+ return !in_array(false, $results, true);
+ }
+
+ /**
+ * Return true if the user should receive notification
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function shouldReceiveNotification(array $user, array $event_data)
+ {
+ $filters = array(
+ 'filterNone',
+ 'filterAssignee',
+ 'filterCreator',
+ 'filterBoth',
+ );
+
+ foreach ($filters as $filter) {
+ if ($this->$filter($user, $event_data)) {
+ return $this->filterProject($user, $event_data);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return true if the user will receive all notifications
+ *
+ * @access public
+ * @param array $user
+ * @return boolean
+ */
+ public function filterNone(array $user)
+ {
+ return $user['notifications_filter'] == self::FILTER_NONE;
+ }
+
+ /**
+ * Return true if the user is the assignee and selected the filter "assignee"
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterAssignee(array $user, array $event_data)
+ {
+ if (!isset($event_data['task']['owner_ms'])) $event_data['task']['owner_ms'] = 0;
+ if (!isset($event_data['task']['owner_gp'])) $event_data['task']['owner_gp'] = 0;
+ return $user['notifications_filter'] == self::FILTER_ASSIGNEE &&
+ ($event_data['task']['owner_id'] == $user['id'] ||
+ $this->multiselectMemberModel->isMember($event_data['task']['owner_ms'], $user['id']) ||
+ $this->groupMemberModel->isMember($event_data['task']['owner_gp'], $user['id']));
+ }
+
+ /**
+ * Return true if the user is the creator and enabled the filter "creator"
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterCreator(array $user, array $event_data)
+ {
+ return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id'];
+ }
+
+ /**
+ * Return true if the user is the assignee or the creator and selected the filter "both"
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterBoth(array $user, array $event_data)
+ {
+ if (!isset($event_data['task']['owner_ms'])) $event_data['task']['owner_ms'] = 0;
+ if (!isset($event_data['task']['owner_gp'])) $event_data['task']['owner_gp'] = 0;
+ return $user['notifications_filter'] == self::FILTER_BOTH &&
+ ($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id'] ||
+ $this->multiselectMemberModel->isMember($event_data['task']['owner_ms'], $user['id']) ||
+ $this->groupMemberModel->isMember($event_data['task']['owner_gp'], $user['id']));
+ }
+
+ /**
+ * Return true if the user want to receive notification for the selected project
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterProject(array $user, array $event_data)
+ {
+ $projects = $this->getSelectedProjects($user['id']);
+
+ if (! empty($projects)) {
+ return in_array($event_data['task']['project_id'], $projects);
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/Group_assign/Model/OldMetaMagikSubquery.php b/plugins/Group_assign/Model/OldMetaMagikSubquery.php
new file mode 100644
index 00000000..787b7e4a
--- /dev/null
+++ b/plugins/Group_assign/Model/OldMetaMagikSubquery.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\OldTaskFinderModel;
+
+/**
+ * New Task Finder model
+ * Extends Group_assign Model
+ *
+ * @package Kanboard\Plugin\Group_assign\Model
+ */
+class OldMetaMagikSubQuery extends OldTaskFinderModel
+{
+ const METADATA_TABLE = 'task_has_metadata';
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
+ {
+ // add subquery to original Model, changing only what we want
+ return parent::getExtendedQuery()
+ ->subquery('(SELECT COUNT(*) FROM '.self::METADATA_TABLE.' WHERE task_id=tasks.id)', 'nb_metadata');
+ }
+} \ No newline at end of file
diff --git a/plugins/Group_assign/Model/OldTaskFinderModel.php b/plugins/Group_assign/Model/OldTaskFinderModel.php
new file mode 100644
index 00000000..70d06126
--- /dev/null
+++ b/plugins/Group_assign/Model/OldTaskFinderModel.php
@@ -0,0 +1,489 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\ActionParameterModel;
+use Kanboard\Model\AvatarFileModel;
+use Kanboard\Model\BoardModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ColumnMoveRestrictionModel;
+use Kanboard\Model\CommentModel;
+use Kanboard\Model\ConfigModel;
+use Kanboard\Model\CurrencyModel;
+use Kanboard\Model\CustomFilterModel;
+use Kanboard\Model\FileModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Model\GroupModel;
+use Kanboard\Model\LanguageModel;
+use Kanboard\Model\LastLoginModel;
+use Kanboard\Model\LinkModel;
+use Kanboard\Model\MetadataModel;
+use Kanboard\Model\NotificationModel;
+use Kanboard\Model\NotificationTypeModel;
+use Kanboard\Model\PasswordResetModel;
+use Kanboard\Model\ProjectActivityModel;
+use Kanboard\Model\ProjectDailyColumnStatsModel;
+use Kanboard\Model\ProjectDailyStatsModel;
+use Kanboard\Model\ProjectDuplicationModel;
+use Kanboard\Model\ProjectFileModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Model\ProjectMetadataModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\ProjectNotificationModel;
+use Kanboard\Model\ProjectNotificationTypeModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\ProjectRoleModel;
+use Kanboard\Model\ProjectRoleRestrictionModel;
+use Kanboard\Model\ProjectTaskDuplicationModel;
+use Kanboard\Model\ProjectTaskPriorityModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\RememberMeSessionModel;
+use Kanboard\Model\SettingModel;
+use Kanboard\Model\SubtaskModel;
+use Kanboard\Model\SubtaskPositionModel;
+use Kanboard\Model\SubtaskStatusModel;
+use Kanboard\Model\SubtaskTaskConversionModel;
+use Kanboard\Model\SubtaskTimeTrackingModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\TagDuplicationModel;
+use Kanboard\Model\TagModel;
+use Kanboard\Model\TaskAnalyticModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskExternalLinkModel;
+use Kanboard\Model\TaskFileModel;
+use Kanboard\Model\TaskLinkModel;
+use Kanboard\Model\TaskMetadataModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskModificationModel;
+use Kanboard\Model\TaskPositionModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskProjectMoveModel;
+use Kanboard\Model\TaskRecurrenceModel;
+use Kanboard\Model\TaskStatusModel;
+use Kanboard\Model\TaskTagModel;
+use Kanboard\Model\TimezoneModel;
+use Kanboard\Model\TransitionModel;
+use Kanboard\Model\UserLockingModel;
+use Kanboard\Model\UserMentionModel;
+use Kanboard\Model\UserMetadataModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\UserNotificationFilterModel;
+use Kanboard\Model\UserNotificationModel;
+use Kanboard\Model\UserNotificationTypeModel;
+use Kanboard\Model\UserUnreadNotificationModel;
+use Kanboard\Core\Base;
+
+
+class OldTaskFinderModel extends Base
+{
+ /**
+ * Get query for project user overview
+ *
+ * @access public
+ * @param array $project_ids
+ * @param integer $is_active
+ * @return \PicoDb\Table
+ */
+ public function getProjectUserOverviewQuery(array $project_ids, $is_active)
+ {
+ if (empty($project_ids)) {
+ $project_ids = array(-1);
+ }
+
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.date_started',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.color_id',
+ TaskModel::TABLE.'.priority',
+ TaskModel::TABLE.'.time_spent',
+ TaskModel::TABLE.'.time_estimated',
+ ProjectModel::TABLE.'.name AS project_name',
+ ColumnModel::TABLE.'.title AS column_name',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name'
+ )
+ ->eq(TaskModel::TABLE.'.is_active', $is_active)
+ ->in(ProjectModel::TABLE.'.id', $project_ids)
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE);
+ }
+
+ /**
+ * Get query for assigned user tasks
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return \PicoDb\Table
+ */
+ public function getUserQuery($user_id)
+ {
+ return $this->getExtendedQuery()
+ ->beginOr()
+ ->eq(TaskModel::TABLE.'.owner_id', $user_id)
+ ->addCondition(TaskModel::TABLE.".id IN (SELECT task_id FROM ".SubtaskModel::TABLE." WHERE ".SubtaskModel::TABLE.".user_id='$user_id')")
+ ->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$user_id')")
+ ->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$user_id')")
+ ->closeOr()
+ ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
+ ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE)
+ ->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0);
+ }
+
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->columns(
+ '(SELECT COUNT(*) FROM '.CommentModel::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
+ '(SELECT COUNT(*) FROM '.TaskFileModel::TABLE.' WHERE task_id=tasks.id) AS nb_files',
+ '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id) AS nb_subtasks',
+ '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links',
+ '(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id AND '.TaskLinkModel::TABLE.'.link_id = 9) AS is_milestone',
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.reference',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.description',
+ TaskModel::TABLE.'.date_creation',
+ TaskModel::TABLE.'.date_modification',
+ TaskModel::TABLE.'.date_completed',
+ TaskModel::TABLE.'.date_started',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.color_id',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.column_id',
+ TaskModel::TABLE.'.swimlane_id',
+ TaskModel::TABLE.'.owner_id',
+ TaskModel::TABLE.'.creator_id',
+ TaskModel::TABLE.'.position',
+ TaskModel::TABLE.'.is_active',
+ TaskModel::TABLE.'.score',
+ TaskModel::TABLE.'.category_id',
+ TaskModel::TABLE.'.priority',
+ TaskModel::TABLE.'.date_moved',
+ TaskModel::TABLE.'.recurrence_status',
+ TaskModel::TABLE.'.recurrence_trigger',
+ TaskModel::TABLE.'.recurrence_factor',
+ TaskModel::TABLE.'.recurrence_timeframe',
+ TaskModel::TABLE.'.recurrence_basedate',
+ TaskModel::TABLE.'.recurrence_parent',
+ TaskModel::TABLE.'.recurrence_child',
+ TaskModel::TABLE.'.time_estimated',
+ TaskModel::TABLE.'.time_spent',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name',
+ UserModel::TABLE.'.email AS assignee_email',
+ UserModel::TABLE.'.avatar_path AS assignee_avatar_path',
+ CategoryModel::TABLE.'.name AS category_name',
+ CategoryModel::TABLE.'.description AS category_description',
+ ColumnModel::TABLE.'.title AS column_name',
+ ColumnModel::TABLE.'.position AS column_position',
+ SwimlaneModel::TABLE.'.name AS swimlane_name',
+ ProjectModel::TABLE.'.name AS project_name',
+ TaskModel::TABLE.'.owner_ms',
+ GroupModel::TABLE.'.name AS assigned_groupname'
+ )
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
+ ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE)
+ ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE)
+ ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE);
+ }
+
+ /**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $status_id Status id
+ * @return array
+ */
+ public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.project_id', $project_id)
+ ->eq(TaskModel::TABLE.'.is_active', $status_id)
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAll();
+ }
+
+ /**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id
+ * @param array $status
+ * @return array
+ */
+ public function getAllIds($project_id, array $status = array(TaskModel::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.project_id', $project_id)
+ ->in(TaskModel::TABLE.'.is_active', $status)
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAllByColumn(TaskModel::TABLE.'.id');
+ }
+
+ /**
+ * Get overdue tasks query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getOverdueTasksQuery()
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.creator_id',
+ TaskModel::TABLE.'.owner_id',
+ ProjectModel::TABLE.'.name AS project_name',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name'
+ )
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->join(UserModel::TABLE, 'id', 'owner_id')
+ ->eq(ProjectModel::TABLE.'.is_active', 1)
+ ->eq(TaskModel::TABLE.'.is_active', 1)
+ ->neq(TaskModel::TABLE.'.date_due', 0)
+ ->lte(TaskModel::TABLE.'.date_due', time());
+ }
+
+ /**
+ * Get a list of overdue tasks for all projects
+ *
+ * @access public
+ * @return array
+ */
+ public function getOverdueTasks()
+ {
+ return $this->getOverdueTasksQuery()->findAll();
+ }
+
+ /**
+ * Get a list of overdue tasks by project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getOverdueTasksByProject($project_id)
+ {
+ return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.project_id', $project_id)->findAll();
+ }
+
+ /**
+ * Get a list of overdue tasks by user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getOverdueTasksByUser($user_id)
+ {
+ return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.owner_id', $user_id)->findAll();
+ }
+
+ /**
+ * Get project id for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return integer
+ */
+ public function getProjectId($task_id)
+ {
+ return (int) $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0;
+ }
+
+ /**
+ * Fetch a task by the id
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getById($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOne();
+ }
+
+ /**
+ * Fetch a task by the reference (external id)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $reference Task reference
+ * @return array
+ */
+ public function getByReference($project_id, $reference)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne();
+ }
+
+ /**
+ * Get task details (fetch more information from other tables)
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getDetails($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.*',
+ CategoryModel::TABLE.'.name AS category_name',
+ SwimlaneModel::TABLE.'.name AS swimlane_name',
+ ProjectModel::TABLE.'.name AS project_name',
+ ColumnModel::TABLE.'.title AS column_title',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name',
+ 'uc.username AS creator_username',
+ 'uc.name AS creator_name',
+ CategoryModel::TABLE.'.description AS category_description',
+ ColumnModel::TABLE.'.position AS column_position',
+ GroupModel::TABLE.'.name AS assigned_groupname'
+ )
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
+ ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE)
+ ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE)
+ ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.id', $task_id)
+ ->findOne();
+ }
+
+ /**
+ * Get iCal query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getICalQuery()
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'ua', 'id', TaskModel::TABLE, 'owner_id')
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->columns(
+ TaskModel::TABLE.'.*',
+ 'ua.email AS assignee_email',
+ 'ua.name AS assignee_name',
+ 'ua.username AS assignee_username',
+ 'uc.email AS creator_email',
+ 'uc.name AS creator_name',
+ 'uc.username AS creator_username'
+ );
+ }
+
+ /**
+ * Count all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param array $status List of status id
+ * @return integer
+ */
+ public function countByProjectId($project_id, array $status = array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Count the number of tasks for a given column and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param array $status
+ * @return int
+ */
+ public function countByColumnId($project_id, $column_id, array $status = array(TaskModel::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Count the number of tasks for a given column and swimlane
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param integer $swimlane_id Swimlane id
+ * @return integer
+ */
+ public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->eq('swimlane_id', $swimlane_id)
+ ->eq('is_active', 1)
+ ->count();
+ }
+
+ /**
+ * Return true if the task exists
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function exists($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->exists();
+ }
+
+ /**
+ * Get project token
+ *
+ * @access public
+ * @param integer $task_id
+ * @return string
+ */
+ public function getProjectToken($task_id)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.id', $task_id)
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->findOneColumn(ProjectModel::TABLE.'.token');
+ }
+}
diff --git a/plugins/Group_assign/Model/TaskProjectDuplicationModel.php b/plugins/Group_assign/Model/TaskProjectDuplicationModel.php
new file mode 100644
index 00000000..835f2063
--- /dev/null
+++ b/plugins/Group_assign/Model/TaskProjectDuplicationModel.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\TaskLinkModel;
+
+/**
+ * Task Project Duplication
+ *
+ * @package Kanboard\Plugins\Group_assign
+ * @author Craig Crosby
+ */
+class TaskProjectDuplicationModel extends TaskDuplicationModel
+{
+ /**
+ * Duplicate a task to another project
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return boolean|integer
+ */
+ public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null, $owner_gp = 0, $owner_ms = 0)
+ {
+ $values = $this->prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
+
+ $this->checkDestinationProjectValues($values);
+ $new_task_id = $this->save($task_id, $values);
+ if ($new_task_id !== false) {
+ // Check if the group is allowed for the destination project
+ $group_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_gp');
+ if ($group_id > 0) {
+ $group_in_project = $this->db
+ ->table(ProjectGroupRoleModel::TABLE)
+ ->eq('project_id', $values['project_id'])
+ ->eq('group_id', $group_id)
+ ->exists();
+ if ($group_in_project) { $this->db->table(TaskModel::TABLE)->eq('id', $new_task_id)->update(['owner_gp' => $group_id]); }
+ }
+
+ // Check if the other assignees are allowed for the destination project
+ $ms_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_ms');
+ if ($ms_id > 0) {
+ $users_in_ms = $this->multiselectMemberModel->getMembers($ms_id);
+ $new_ms_id = $this->multiselectModel->create();
+ $this->db->table(TaskModel::TABLE)->eq('id', $new_task_id)->update(['owner_ms' => $new_ms_id]);
+ foreach ($users_in_ms as $user) {
+ if ($this->projectPermissionModel->isAssignable($values['project_id'], $user['id'])) {
+ $this->multiselectMemberModel->addUser($new_ms_id, $user['id']);
+ }
+ }
+ }
+
+ $this->tagDuplicationModel->duplicateTaskTagsToAnotherProject($task_id, $new_task_id, $project_id);
+ $this->taskLinkModel->create($new_task_id, $task_id, 4);
+ }
+
+ return $new_task_id;
+ }
+
+ /**
+ * Prepare values before duplication
+ *
+ * @access protected
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return array
+ */
+ protected function prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id)
+ {
+ $values = $this->copyFields($task_id);
+ $values['project_id'] = $project_id;
+ $values['column_id'] = $column_id !== null ? $column_id : $values['column_id'];
+ $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id'];
+ $values['category_id'] = $category_id !== null ? $category_id : $values['category_id'];
+ $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id'];
+
+ return $values;
+ }
+}
diff --git a/plugins/Group_assign/Model/TaskProjectMoveModel.php b/plugins/Group_assign/Model/TaskProjectMoveModel.php
new file mode 100644
index 00000000..554d3330
--- /dev/null
+++ b/plugins/Group_assign/Model/TaskProjectMoveModel.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\TaskLinkModel;
+
+/**
+ * Task Project Move
+ *
+ * @package Kanboard\Plugins\Group_assign
+ * @author Craig Crosby
+ */
+class TaskProjectMoveModel extends TaskDuplicationModel
+{
+ /**
+ * Move a task to another project
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return boolean
+ */
+ public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
+ {
+ $task = $this->taskFinderModel->getById($task_id);
+ $values = $this->prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, $task);
+
+ $this->checkDestinationProjectValues($values);
+ $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id);
+
+ // Check if the group is allowed for the destination project and unassign if not
+ $group_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_gp');
+ if ($group_id > 0) {
+ $group_in_project = $this->db
+ ->table(ProjectGroupRoleModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('group_id', $group_id)
+ ->exists();
+ if (!$group_in_project) { $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(['owner_gp' => 0]); }
+ }
+
+ // Check if the other assignees are allowed for the destination project and remove from ms group if not
+ $ms_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_ms');
+ if ($ms_id > 0) {
+ $users_in_ms = $this->multiselectMemberModel->getMembers($ms_id);
+ foreach ($users_in_ms as $user) {
+ if (! $this->projectPermissionModel->isAssignable($project_id, $user['id'])) {
+ $this->multiselectMemberModel->removeUser($ms_id, $user['id']);
+ }
+ }
+ }
+
+
+ if ($this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update($values)) {
+ $this->queueManager->push($this->taskEventJob->withParams($task_id, array(TaskModel::EVENT_MOVE_PROJECT), $values));
+ }
+
+ return true;
+ }
+
+ /**
+ * Prepare new task values
+ *
+ * @access protected
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @param array $task
+ * @return array
+ */
+ protected function prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, array $task)
+ {
+ $values = array();
+ $values['is_active'] = 1;
+ $values['project_id'] = $project_id;
+ $values['column_id'] = $column_id !== null ? $column_id : $task['column_id'];
+ $values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1;
+ $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id'];
+ $values['category_id'] = $category_id !== null ? $category_id : $task['category_id'];
+ $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id'];
+ $values['priority'] = $task['priority'];
+ return $values;
+ }
+}
diff --git a/plugins/Group_assign/Model/TaskRecurrenceModel.php b/plugins/Group_assign/Model/TaskRecurrenceModel.php
new file mode 100644
index 00000000..999751e1
--- /dev/null
+++ b/plugins/Group_assign/Model/TaskRecurrenceModel.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use DateInterval;
+use DateTime;
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\TaskLinkModel;
+
+/**
+ * Task Recurrence
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class TaskRecurrenceModel extends GroupAssignTaskDuplicationModel
+{
+ /**
+ * Return the list user selectable recurrence status
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceStatusList()
+ {
+ return array(
+ TaskModel::RECURRING_STATUS_NONE => t('No'),
+ TaskModel::RECURRING_STATUS_PENDING => t('Yes'),
+ );
+ }
+
+ /**
+ * Return the list recurrence triggers
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceTriggerList()
+ {
+ return array(
+ TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'),
+ TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'),
+ TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'),
+ );
+ }
+
+ /**
+ * Return the list options to calculate recurrence due date
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceBasedateList()
+ {
+ return array(
+ TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'),
+ TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'),
+ );
+ }
+
+ /**
+ * Return the list recurrence timeframes
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceTimeframeList()
+ {
+ return array(
+ TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'),
+ TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'),
+ TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'),
+ );
+ }
+
+ /**
+ * Duplicate recurring task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean|integer Recurrence task id
+ */
+ public function duplicateRecurringTask($task_id)
+ {
+ $values = $this->copyFields($task_id);
+
+ if ($values['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) {
+ $values['recurrence_parent'] = $task_id;
+ $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']);
+ $this->calculateRecurringTaskDueDate($values);
+
+ $recurring_task_id = $this->save($task_id, $values);
+
+ if ($recurring_task_id !== false) {
+ $this->tagDuplicationModel->duplicateTaskTags($task_id, $recurring_task_id);
+
+ $parent_update = $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('id', $task_id)
+ ->update(array(
+ 'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED,
+ 'recurrence_child' => $recurring_task_id,
+ ));
+
+ if ($parent_update) {
+ return $recurring_task_id;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Calculate new due date for new recurrence task
+ *
+ * @access public
+ * @param array $values Task fields
+ */
+ public function calculateRecurringTaskDueDate(array &$values)
+ {
+ if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) {
+ if ($values['recurrence_basedate'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) {
+ $values['date_due'] = time();
+ }
+
+ $factor = abs($values['recurrence_factor']);
+ $subtract = $values['recurrence_factor'] < 0;
+
+ switch ($values['recurrence_timeframe']) {
+ case TaskModel::RECURRING_TIMEFRAME_MONTHS:
+ $interval = 'P' . $factor . 'M';
+ break;
+ case TaskModel::RECURRING_TIMEFRAME_YEARS:
+ $interval = 'P' . $factor . 'Y';
+ break;
+ default:
+ $interval = 'P' . $factor . 'D';
+ }
+
+ $date_due = new DateTime();
+ $date_due->setTimestamp($values['date_due']);
+
+ $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval));
+
+ $values['date_due'] = $date_due->getTimestamp();
+ }
+ }
+}
diff --git a/plugins/Group_assign/Plugin.php b/plugins/Group_assign/Plugin.php
new file mode 100644
index 00000000..6f69388c
--- /dev/null
+++ b/plugins/Group_assign/Plugin.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign;
+
+use Kanboard\Core\Plugin\Base;
+use Kanboard\Core\Translator;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Plugin\Group_assign\Model\NewTaskFinderModel;
+use Kanboard\Plugin\Group_assign\Model\NewUserNotificationFilterModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Plugin\Group_assign\Model\OldTaskFinderModel;
+use Kanboard\Plugin\Group_assign\Helper\NewTaskHelper;
+use Kanboard\Plugin\Group_assign\Filter\TaskAllAssigneeFilter;
+use Kanboard\Plugin\Group_assign\Action\EmailGroup;
+use Kanboard\Plugin\Group_assign\Action\EmailGroupDue;
+use Kanboard\Plugin\Group_assign\Action\EmailOtherAssignees;
+use Kanboard\Plugin\Group_assign\Action\EmailOtherAssigneesDue;
+use Kanboard\Plugin\Group_assign\Action\AssignGroup;
+use Kanboard\Plugin\Group_assign\Model\GroupAssignCalendarModel;
+use Kanboard\Plugin\Group_assign\Model\GroupAssignTaskDuplicationModel;
+use Kanboard\Plugin\Group_assign\Model\TaskProjectDuplicationModel;
+use Kanboard\Plugin\Group_assign\Model\TaskProjectMoveModel;
+use Kanboard\Plugin\Group_assign\Model\TaskRecurrenceModel;
+use Kanboard\Plugin\Group_assign\Model\NewMetaMagikSubquery;
+use Kanboard\Plugin\Group_assign\Model\OldMetaMagikSubquery;
+use PicoDb\Table;
+use PicoDb\Database;
+use Kanboard\Core\Security\Role;
+
+class Plugin extends Base
+{
+
+ public function initialize()
+ {
+ //Events & Changes
+ $this->template->setTemplateOverride('task/changes', 'group_assign:task/changes');
+
+ //Notifications
+ $this->container['userNotificationFilterModel'] = $this->container->factory(function ($c) {
+ return new NewUserNotificationFilterModel($c);
+ });
+
+ //Helpers
+ $this->helper->register('newTaskHelper', '\Kanboard\Plugin\Group_assign\Helper\NewTaskHelper');
+ $this->helper->register('smallAvatarHelperExtend', '\Kanboard\Plugin\Group_assign\Helper\SmallAvatarHelperExtend');
+
+
+ //Models and backward compatibility
+
+ $applications_version = str_replace('v', '', APP_VERSION);
+ if (strpos(APP_VERSION, 'master') !== false && file_exists('ChangeLog')) { $applications_version = trim(file_get_contents('ChangeLog', false, null, 8, 6), ' '); }
+ $clean_appversion = preg_replace('/\s+/', '', $applications_version);
+
+ if (version_compare($clean_appversion, '1.2.5', '>')) {
+ if (file_exists('plugins/MetaMagik')){
+ $this->container['taskFinderModel'] = $this->container->factory(function ($c) {
+ return new NewMetaMagikSubquery($c);
+ });
+ } else {
+ $this->container['taskFinderModel'] = $this->container->factory(function ($c) {
+ return new NewTaskFinderModel($c);
+ });
+ }
+ $this->container['taskDuplicationModel'] = $this->container->factory(function ($c) {
+ return new GroupAssignTaskDuplicationModel($c);
+ });
+ $this->container['taskProjectDuplicationModel '] = $this->container->factory(function ($c) {
+ return new TaskProjectDuplicationModel ($c);
+ });
+ $this->container['taskProjectMoveModel '] = $this->container->factory(function ($c) {
+ return new TaskProjectMoveModel ($c);
+ });
+ $this->container['taskRecurrenceModel '] = $this->container->factory(function ($c) {
+ return new TaskRecurrenceModel ($c);
+ });
+ } else {
+ if (file_exists('plugins/MetaMagik')){
+ $this->container['taskFinderModel'] = $this->container->factory(function ($c) {
+ return new OldMetaMagikSubquery($c);
+ });
+ } else {
+ $this->container['taskFinderModel'] = $this->container->factory(function ($c) {
+ return new OldTaskFinderModel($c);
+ });
+ }
+ $this->container['taskDuplicationModel'] = $this->container->factory(function ($c) {
+ return new GroupAssignTaskDuplicationModel($c);
+ });
+ $this->container['taskProjectDuplicationModel '] = $this->container->factory(function ($c) {
+ return new TaskProjectDuplicationModel ($c);
+ });
+ $this->container['taskProjectMoveModel '] = $this->container->factory(function ($c) {
+ return new TaskProjectMoveModel ($c);
+ });
+ $this->container['taskRecurrenceModel '] = $this->container->factory(function ($c) {
+ return new TaskRecurrenceModel ($c);
+ });
+ }
+
+ //Task - Template - details.php
+ $this->template->hook->attach('template:task:details:third-column', 'group_assign:task/details');
+ $this->template->hook->attach('template:task:details:third-column', 'group_assign:task/multi');
+
+ //Forms - task_creation.php and task_modification.php
+ $this->template->setTemplateOverride('task_creation/show', 'group_assign:task_creation/show');
+ $this->template->setTemplateOverride('task_modification/show', 'group_assign:task_modification/show');
+
+ //Board
+ $this->template->hook->attach('template:board:private:task:before-title', 'group_assign:board/group');
+ $this->template->hook->attach('template:board:private:task:before-title', 'group_assign:board/multi');
+ $groupmodel = $this->projectGroupRoleModel;
+ $this->template->hook->attachCallable('template:app:filters-helper:after', 'group_assign:board/filter', function($array = array()) use ($groupmodel) {
+ if(!empty($array) && $array['id'] >= 1){
+ return ['grouplist' => array_column($groupmodel->getGroups($array['id']), 'name')];
+ } else {
+ return ['grouplist' => array()];
+ }
+ });
+
+ //Filter
+ $this->container->extend('taskLexer', function($taskLexer, $c) {
+ $taskLexer->withFilter(TaskAllAssigneeFilter::getInstance()->setDatabase($c['db'])
+ ->setCurrentUserId($c['userSession']->getId()));
+ return $taskLexer;
+ });
+
+ //Actions
+ $this->actionManager->register(new EmailGroup($this->container));
+ $this->actionManager->register(new EmailGroupDue($this->container));
+ $this->actionManager->register(new EmailOtherAssignees($this->container));
+ $this->actionManager->register(new EmailOtherAssigneesDue($this->container));
+ $this->actionManager->register(new AssignGroup($this->container));
+
+ //Params
+ $this->template->setTemplateOverride('action_creation/params', 'group_assign:action_creation/params');
+
+ //CSS
+ $this->hook->on('template:layout:css', array('template' => 'plugins/Group_assign/Assets/css/group_assign.css'));
+
+ //JS
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Group_assign/Assets/js/group_assign.js'));
+
+ //Calendar Events
+ $container = $this->container;
+
+ $this->hook->on('controller:calendar:user:events', function($user_id, $start, $end) use ($container) {
+ $model = new GroupAssignCalendarModel($container);
+ return $model->getUserCalendarEvents($user_id, $start, $end); // Return new events
+ });
+
+ //Roles
+
+ $this->template->hook->attach('template:config:application', 'group_assign:config/toggle');
+
+ if ($this->configModel->get('enable_am_group_management', '2') == 1) {
+ $this->applicationAccessMap->add('GroupListController', '*', Role::APP_MANAGER);
+ $this->applicationAccessMap->add('GroupCreationController', '*', Role::APP_MANAGER);
+ $this->template->setTemplateOverride('header/user_dropdown', 'group_assign:header/user_dropdown');
+ }
+
+
+ }
+
+ public function onStartup()
+ {
+ Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale');
+ }
+
+ public function getClasses()
+ {
+ return [
+ 'Plugin\Group_assign\Model' => [
+ 'MultiselectMemberModel', 'MultiselectModel', 'GroupColorExtension', 'TaskProjectDuplicationModel', 'TaskProjectMoveModel', 'TaskRecurrenceModel',
+ ],
+ ];
+ }
+
+ public function getPluginName()
+ {
+ return 'Group_assign';
+ }
+ public function getPluginDescription()
+ {
+ return t('Add group assignment to tasks');
+ }
+ public function getPluginAuthor()
+ {
+ return 'Craig Crosby';
+ }
+ public function getPluginVersion()
+ {
+ return '1.7.8';
+ }
+ public function getPluginHomepage()
+ {
+ return 'https://github.com/creecros/group_assign';
+ }
+}
diff --git a/plugins/Group_assign/README.md b/plugins/Group_assign/README.md
new file mode 100644
index 00000000..43078d9c
--- /dev/null
+++ b/plugins/Group_assign/README.md
@@ -0,0 +1,95 @@
+## Checkout our latest project
+[![](https://raw.githubusercontent.com/docpht/docpht/master/public/assets/img/logo.png)](https://github.com/docpht/docpht)
+
+- With [DocPHT](https://github.com/docpht/docpht) you can take notes and quickly document anything and without the use of any database.
+-----------
+[![Latest release](https://img.shields.io/github/release/creecros/Group_assign.svg)](https://github.com/creecros/Group_assign/releases)
+[![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/creecros/Group_assign/blob/master/LICENSE)
+[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/creecros/Group_assign/graphs/contributors)
+[![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)]()
+[![Downloads](https://img.shields.io/github/downloads/creecros/Group_assign/total.svg)](https://github.com/creecros/Group_assign/releases)
+
+Donate to help keep this project maintained.
+<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SEGNEVQFXHXGW&source=url">
+<img src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif" border="0" name="submit" title="PayPal - The safer, easier way to pay online!" alt="Donate with PayPal button" /></a>
+
+:star: If you use it, you should star it on Github!
+It's the least you can do for all the work put into it!
+
+
+# Group_assign
+Assign Tasks to Groups or from Multi-Select of Users with permissions from the project
+
+# Requirements
+Kanboard v1.1.0 or Higher
+
+# Features and usage
+* A task can have an assigned group or selection of users
+* Can only assign groups or other assigness to a task that have permissions in the Project.
+* If a user is in a group that a task is assigned to, it will show up on their dashboard.
+* If a user is in other assignees multiselect that a task is assigned to, it will show up on their dashboard.
+* If a user is in a group that a task is assigned to, it will show up in their calendar.
+* If a user is in other assignees multiselect that a task is assigned to, it will show up in their calendar.
+* 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.
+* Includes 5 Automatic Actions to utilize the Assigned Group
+ * Email Assigned Group on Task Modification, Creation, Close, or Movement
+ * Email Assigned Group of impending Task Due Date
+ * Email Other Assignees on Task Modification, Creation, Close, or Movement
+ * Email Other Assignees of impending Task Due Date
+ * Assign task to a group on creation or movement
+* 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.
+* 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.
+* 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.
+* 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.
+* User assigneed via a group or multiselect will now recieve notifications
+* Changing assigned group or any multiselect users will now trigger `EVENT_ASSIGNEE_CHANGE`
+* Duplicating Tasks will include assigned groups and other users.
+ * Duplicating to another project or moving to another project will check permissions of assignees, and remove those without permission.
+* Task Reccurences will include group assigned and other assignees in the recurrence.
+* Setting included to enable group managment for Application Managers
+ * Found in `Settings > Application settings`
+
+# Future enhancments
+Find bugs or missing functionality, please report it.
+
+- [x] Add a few basic automatic actions that utilize Groups assigned
+- [x] Add relationship for ``allassignees:Username`` or ``allassignees:Name`` in the table lookup
+- [x] Add an event for assigned group change.
+- [x] Incorporate into notifications
+- [x] Address Task Duplication
+- [x] Task Recurrence
+
+# Manual Installation
+
+- Find the release you wish to install: https://github.com/creecros/Group_assign/releases
+- Download the provided zip, not the source zip, i.e. `Group_assign-x.x.x.zip`
+- Unzip contents to the plugins folder
+
+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.
+
+# Screenshots
+
+## Task Details:
+![image](https://user-images.githubusercontent.com/26339368/49951197-64546680-fec7-11e8-9473-82820b1a4f7e.png)
+
+## Task Creation/Modification:
+![image](https://user-images.githubusercontent.com/26339368/38753761-692db008-3f2d-11e8-8ce2-59d88ddf39b1.png)
+![image](https://user-images.githubusercontent.com/26339368/49557918-3c696f80-f8d7-11e8-91b8-7cef11c6eec0.png)
+
+## Board View:
+![image](https://user-images.githubusercontent.com/26339368/49951135-3a9b3f80-fec7-11e8-9bf6-3a777c09c675.png)
+
+## Users Calendar View
+
+- 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.
+
+![image](https://user-images.githubusercontent.com/26339368/49655821-b7cb3e00-fa09-11e8-9608-952abbf146fa.png)
+
+
+## Automatic Actions:
+![image](https://user-images.githubusercontent.com/26339368/38754253-0a0fd2de-3f2f-11e8-9dde-2036de011a6b.png)
+
+![image](https://user-images.githubusercontent.com/26339368/38754279-2285d0d4-3f2f-11e8-88c2-0ed91e452f90.png)
+
+![image](https://user-images.githubusercontent.com/26339368/38754288-310df2c6-3f2f-11e8-9993-39e96b55076c.png)
+
diff --git a/plugins/Group_assign/Schema/Mysql.php b/plugins/Group_assign/Schema/Mysql.php
new file mode 100644
index 00000000..17318a29
--- /dev/null
+++ b/plugins/Group_assign/Schema/Mysql.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\Plugin\group_assign\Schema;
+
+use PDO;
+
+const VERSION = 2;
+
+function version_2(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE `tasks` ADD COLUMN `owner_ms` INT DEFAULT '0'");
+
+ $pdo->exec("
+ CREATE TABLE `multiselect` (
+ id INT NOT NULL AUTO_INCREMENT,
+ external_id VARCHAR(255) DEFAULT '',
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+ $pdo->exec("
+ CREATE TABLE multiselect_has_users (
+ group_id INT NOT NULL,
+ user_id INT NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES `multiselect`(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
+
+function version_1(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE `tasks` ADD COLUMN `owner_gp` INT DEFAULT '0'");
+}
diff --git a/plugins/Group_assign/Schema/Postgres.php b/plugins/Group_assign/Schema/Postgres.php
new file mode 100644
index 00000000..54b0a2be
--- /dev/null
+++ b/plugins/Group_assign/Schema/Postgres.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\Plugin\group_assign\Schema;
+
+use PDO;
+
+const VERSION = 2;
+
+function version_2(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_ms INT DEFAULT '0'");
+
+ $pdo->exec("
+ CREATE TABLE multiselect (
+ id SERIAL PRIMARY KEY,
+ external_id VARCHAR(255) DEFAULT ''
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE multiselect_has_users (
+ group_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES multiselect(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ )
+ ");
+}
+
+function version_1(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_gp INT DEFAULT '0'");
+}
diff --git a/plugins/Group_assign/Schema/Sqlite.php b/plugins/Group_assign/Schema/Sqlite.php
new file mode 100644
index 00000000..0d5d3e9c
--- /dev/null
+++ b/plugins/Group_assign/Schema/Sqlite.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\Plugin\group_assign\Schema;
+
+use PDO;
+
+const VERSION = 2;
+
+function version_2(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_ms INTEGER DEFAULT '0'");
+
+ $pdo->exec("
+ CREATE TABLE multiselect (
+ id INTEGER PRIMARY KEY,
+ external_id TEXT DEFAULT ''
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE multiselect_has_users (
+ group_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES multiselect(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ )
+ ");
+}
+
+function version_1(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_gp INTEGER DEFAULT '0'");
+}
diff --git a/plugins/Group_assign/Template/action_creation/params.php b/plugins/Group_assign/Template/action_creation/params.php
new file mode 100644
index 00000000..0506007b
--- /dev/null
+++ b/plugins/Group_assign/Template/action_creation/params.php
@@ -0,0 +1,73 @@
+<div class="page-header">
+ <h2><?= t('Define action parameters') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->url->href('ActionCreationController', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('event_name', $values) ?>
+ <?= $this->form->hidden('action_name', $values) ?>
+
+ <?= $this->form->label(t('Action'), 'action_name') ?>
+ <?= $this->form->select('action_name', $available_actions, $values, array(), array('disabled')) ?>
+
+ <?= $this->form->label(t('Event'), 'event_name') ?>
+ <?= $this->form->select('event_name', $events, $values, array(), array('disabled')) ?>
+
+ <?php foreach ($action_params as $param_name => $param_desc): ?>
+ <?php if ($this->text->contains($param_name, 'column_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'user_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'group_id')): ?>
+ <?php $groups = $this->model->projectGroupRoleModel->getGroups($values['project_id']); ?>
+ <?php $groupnames = array_column($groups, 'name'); ?>
+ <?php $groupids = array_column($groups, 'id'); ?>
+ <?php array_unshift($groupnames, t('Unassigned')); ?>
+ <?php array_unshift($groupids, 0); ?>
+ <?php $groupvalues = array_combine($groupids, $groupnames); ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $groupvalues, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'check_box')): ?>
+ <?= $this->form->label(t('Options'), $param_name) ?>
+ <?= $this->form->checkbox('params['.$param_name.']', $param_desc, 1) ?>
+ <?php elseif ($this->text->contains($param_name, 'project_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'color_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'category_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'link_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $links_list, $values) ?>
+ <?php elseif ($param_name === 'priority'): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $priorities_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'duration')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->number('params['.$param_name.']', $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'swimlane_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $swimlane_list, $values) ?>
+ <?php elseif (is_array($param_desc)): ?>
+ <?= $this->form->label(ucfirst($param_name), $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $param_desc, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'multitasktitles')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->textarea('params['.$param_name.']', $values) ?>
+ <div class="form-help">
+ <?= t('Enter one line per task, or leave blank to copy Task Title and create only one subtask.') ?>
+ </div>
+ <?php else: ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->text('params['.$param_name.']', $values) ?>
+ <?php endif ?>
+ <?php endforeach ?>
+
+ <?= $this->modal->submitButtons() ?>
+</form>
diff --git a/plugins/Group_assign/Template/board/filter.php b/plugins/Group_assign/Template/board/filter.php
new file mode 100644
index 00000000..322c483f
--- /dev/null
+++ b/plugins/Group_assign/Template/board/filter.php
@@ -0,0 +1,12 @@
+<?php if (isset($grouplist) && !empty($grouplist)) : ?>
+</div>
+<div class="input-addon-item">
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Group filters') ?>"><i class="fa fa-users fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <?php foreach ($grouplist as $group) : ?>
+ <li><a href="#" class="filter-helper" data-unique-filter='allassignees:"<?= $this->text->e($group) ?>"'><?= $this->text->e($group) ?></a></li>
+ <?php endforeach ?>
+ </ul>
+ </div>
+<?php endif ?> \ No newline at end of file
diff --git a/plugins/Group_assign/Template/board/group.php b/plugins/Group_assign/Template/board/group.php
new file mode 100644
index 00000000..6ac95c1f
--- /dev/null
+++ b/plugins/Group_assign/Template/board/group.php
@@ -0,0 +1,7 @@
+<span>
+<?php if ($task['assigned_groupname']): ?>
+ <strong class="assigned-group-label"><?= t('Assigned Group:') ?></strong>
+ <span class="assigned-group" style="background-color: #<?= $this->task->groupColorExtension->getGroupColor($task['assigned_groupname']) ?>; color:<?= $this->task->groupColorExtension->getFontColor($this->task->groupColorExtension->getGroupColor($task['assigned_groupname'])) ?>;"><?= $this->text->e($task['assigned_groupname'] ?: $task['owner_gp']) ?></span>
+ <br>
+<?php endif ?>
+</span>
diff --git a/plugins/Group_assign/Template/board/multi.php b/plugins/Group_assign/Template/board/multi.php
new file mode 100644
index 00000000..46084b5f
--- /dev/null
+++ b/plugins/Group_assign/Template/board/multi.php
@@ -0,0 +1,5 @@
+<?php if ($task['owner_ms'] > 0 && count($this->task->multiselectMemberModel->getMembers($task['owner_ms'])) > 0) : ?>
+<strong class="assigned-other-label"><small><?= t('Other Assignees:') ?></small></strong>
+ <?= $this->helper->smallAvatarHelperExtend->miniMultiple($task['owner_ms'], 'avatar-inline') ?>
+<br>
+<?php endif ?>
diff --git a/plugins/Group_assign/Template/config/toggle.php b/plugins/Group_assign/Template/config/toggle.php
new file mode 100644
index 00000000..801a6a26
--- /dev/null
+++ b/plugins/Group_assign/Template/config/toggle.php
@@ -0,0 +1,4 @@
+<div class="panel">
+ <?= $this->form->radio('enable_am_group_management', 'Enable Group Managment for Application Managers' , 1, isset($values['enable_am_group_management'])&& $values['enable_am_group_management']==1) ?>
+ <?= $this->form->radio('enable_am_group_management', 'Disable Group Managment for Application Managers' , 2, isset($values['enable_am_group_management'])&& $values['enable_am_group_management']==2) ?>
+</div>
diff --git a/plugins/Group_assign/Template/header/user_dropdown.php b/plugins/Group_assign/Template/header/user_dropdown.php
new file mode 100644
index 00000000..e3a38787
--- /dev/null
+++ b/plugins/Group_assign/Template/header/user_dropdown.php
@@ -0,0 +1,46 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><?= $this->avatar->currentUserSmall('avatar-inline') ?><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li class="no-hover"><strong><?= $this->text->e($this->user->getFullname()) ?></strong></li>
+ <li>
+ <?= $this->url->icon('tachometer', t('My dashboard'), 'DashboardController', 'show', array('user_id' => $this->user->getId())) ?>
+ </li>
+ <li>
+ <?= $this->url->icon('home', t('My profile'), 'UserViewController', 'show', array('user_id' => $this->user->getId())) ?>
+ </li>
+ <li>
+ <?= $this->url->icon('folder', t('Projects management'), 'ProjectListController', 'show') ?>
+ </li>
+ <?php if ($this->user->hasAccess('GroupListController', 'index') && $_SESSION['user']['role'] == 'app-manager'): ?>
+ <li>
+ <?= $this->url->icon('group', t('Groups management'), 'GroupListController', 'index') ?>
+ </li>
+ <?php endif ?>
+ <?php if ($this->user->hasAccess('UserListController', 'show')): ?>
+ <li>
+ <?= $this->url->icon('user', t('Users management'), 'UserListController', 'show') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('group', t('Groups management'), 'GroupListController', 'index') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('cubes', t('Plugins'), 'PluginController', 'show') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('cog', t('Settings'), 'ConfigController', 'index') ?>
+ </li>
+ <?php endif ?>
+
+ <?= $this->hook->render('template:header:dropdown') ?>
+
+ <li>
+ <i class="fa fa-fw fa-life-ring" aria-hidden="true"></i>
+ <?= $this->url->doc(t('Documentation'), 'index') ?>
+ </li>
+ <?php if (! DISABLE_LOGOUT): ?>
+ <li>
+ <?= $this->url->icon('sign-out', t('Logout'), 'AuthController', 'logout') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+</div>
diff --git a/plugins/Group_assign/Template/task/changes.php b/plugins/Group_assign/Template/task/changes.php
new file mode 100644
index 00000000..7a0d2720
--- /dev/null
+++ b/plugins/Group_assign/Template/task/changes.php
@@ -0,0 +1,92 @@
+<?php if (! empty($changes)): ?>
+ <ul>
+ <?php
+
+ foreach ($changes as $field => $value) {
+ switch ($field) {
+ case 'title':
+ echo '<li>'.t('New title: %s', $task['title']).'</li>';
+ break;
+ case 'owner_id':
+ if (empty($task['owner_id'])) {
+ echo '<li>'.t('The task is not assigned anymore').'</li>';
+ } else {
+ echo '<li>'.t('New assignee: %s', $task['assignee_name'] ?: $task['assignee_username']).'</li>';
+ }
+ break;
+ case 'category_id':
+ if (empty($task['category_id'])) {
+ echo '<li>'.t('There is no category now').'</li>';
+ } else {
+ echo '<li>'.t('New category: %s', $task['category_name']).'</li>';
+ }
+ break;
+ case 'color_id':
+ echo '<li>'.t('New color: %s', $this->text->in($task['color_id'], $this->task->getColors())).'</li>';
+ break;
+ case 'score':
+ echo '<li>'.t('New complexity: %d', $task['score']).'</li>';
+ break;
+ case 'date_due':
+ if (empty($task['date_due'])) {
+ echo '<li>'.t('The due date have been removed').'</li>';
+ } else {
+ echo '<li>'.t('New due date: ').$this->dt->datetime($task['date_due']).'</li>';
+ }
+ break;
+ case 'description':
+ if (empty($task['description'])) {
+ echo '<li>'.t('There is no description anymore').'</li>';
+ }
+ break;
+ case 'recurrence_status':
+ case 'recurrence_trigger':
+ case 'recurrence_factor':
+ case 'recurrence_timeframe':
+ case 'recurrence_basedate':
+ case 'recurrence_parent':
+ case 'recurrence_child':
+ echo '<li>'.t('Recurrence settings have been modified').'</li>';
+ break;
+ case 'time_spent':
+ echo '<li>'.t('Time spent changed: %sh', $task['time_spent']).'</li>';
+ break;
+ case 'time_estimated':
+ echo '<li>'.t('Time estimated changed: %sh', $task['time_estimated']).'</li>';
+ break;
+ case 'date_started':
+ if ($value != 0) {
+ echo '<li>'.t('Start date changed: ').$this->dt->datetime($task['date_started']).'</li>';
+ }
+ break;
+ case 'owner_gp':
+ if (empty($task['owner_gp'])) {
+ echo '<li>'.t('The task is not assigned to a group anymore').'</li>';
+ } else {
+ echo '<li>'.t('New group assigned: %s', $task['assigned_groupname']).'</li>';
+ }
+ break;
+ case 'owner_ms':
+ if (empty($task['owner_ms'])) {
+ echo '<li>'.t('The task is not assigned to multiple users anymore').'</li>';
+ } else {
+ echo '<li>'.t('The task has been assigned other users').'</li>';
+ }
+ break;
+ default:
+ echo '<li>'.t('The field "%s" have been updated', $field).'</li>';
+ }
+ }
+
+ ?>
+ </ul>
+
+ <?php if (! empty($changes['description'])): ?>
+ <p><strong><?= t('The description has been modified:') ?></strong></p>
+ <?php if (isset($public)): ?>
+ <div class="markdown"><?= $this->text->markdown($task['description'], true) ?></div>
+ <?php else: ?>
+ <div class="markdown"><?= $this->text->markdown($task['description']) ?></div>
+ <?php endif ?>
+ <?php endif ?>
+<?php endif ?>
diff --git a/plugins/Group_assign/Template/task/details.php b/plugins/Group_assign/Template/task/details.php
new file mode 100644
index 00000000..907739bc
--- /dev/null
+++ b/plugins/Group_assign/Template/task/details.php
@@ -0,0 +1,10 @@
+ <li>
+ <strong><?= t('Assigned Group:') ?></strong>
+ <span>
+ <?php if ($task['assigned_groupname']): ?>
+ <span class="assigned-group" style="background-color: #<?= $this->task->groupColorExtension->getGroupColor($task['assigned_groupname']) ?>; color:<?= $this->task->groupColorExtension->getFontColor($this->task->groupColorExtension->getGroupColor($task['assigned_groupname'])) ?>;"><?= $this->text->e($task['assigned_groupname'] ?: $task['owner_gp']) ?></span>
+ <?php else: ?>
+ <?= t('not assigned') ?>
+ <?php endif ?>
+ </span>
+ </li>
diff --git a/plugins/Group_assign/Template/task/multi.php b/plugins/Group_assign/Template/task/multi.php
new file mode 100644
index 00000000..5c1fcfb0
--- /dev/null
+++ b/plugins/Group_assign/Template/task/multi.php
@@ -0,0 +1,6 @@
+ <?php if ($task['owner_ms'] > 0 && count($this->task->multiselectMemberModel->getMembers($task['owner_ms'])) > 0) : ?>
+ <li>
+ <strong><?= t('Other Assignees:') ?></strong>
+ </li>
+ <?= $this->helper->smallAvatarHelperExtend->smallMultiple($task['owner_ms'], 'avatar-inline') ?>
+ <?php endif ?>
diff --git a/plugins/Group_assign/Template/task_creation/show.php b/plugins/Group_assign/Template/task_creation/show.php
new file mode 100644
index 00000000..81f610a8
--- /dev/null
+++ b/plugins/Group_assign/Template/task_creation/show.php
@@ -0,0 +1,50 @@
+<div class="page-header">
+ <h2><?= $this->text->e($project['name']) ?> &gt; <?= t('New task') ?></h2>
+</div>
+<form method="post" action="<?= $this->url->href('GroupAssignTaskCreationController', 'save', array('plugin' => 'Group_assign', 'project_id' => $project['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <div class="task-form-container">
+ <div class="task-form-main-column">
+ <?= $this->task->renderTitleField($values, $errors) ?>
+ <?= $this->task->renderDescriptionField($values, $errors) ?>
+ <?= $this->task->renderDescriptionTemplateDropdown($project['id']) ?>
+ <?= $this->task->renderTagField($project) ?>
+
+ <?= $this->hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-secondary-column">
+ <?= $this->task->renderColorField($values) ?>
+ <?= $this->task->renderAssigneeField($users_list, $values, $errors) ?>
+ <?= $this->helper->newTaskHelper->renderGroupField($values, $errors) ?>
+ <?= $this->helper->newTaskHelper->renderMultiAssigneeField($users_list, $values) ?>
+ <?= $this->task->renderCategoryField($categories_list, $values, $errors) ?>
+ <?= $this->task->renderSwimlaneField($swimlanes_list, $values, $errors) ?>
+ <?= $this->task->renderColumnField($columns_list, $values, $errors) ?>
+ <?= $this->task->renderPriorityField($project, $values) ?>
+
+ <?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-secondary-column">
+ <?= $this->task->renderDueDateField($values, $errors) ?>
+ <?= $this->task->renderStartDateField($values, $errors) ?>
+ <?= $this->task->renderTimeEstimatedField($values, $errors) ?>
+ <?= $this->task->renderTimeSpentField($values, $errors) ?>
+ <?= $this->task->renderScoreField($values, $errors) ?>
+ <?= $this->task->renderReferenceField($values, $errors) ?>
+
+ <?= $this->hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-bottom">
+ <?php if (! isset($duplicate)): ?>
+ <?= $this->form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?>
+ <?= $this->form->checkbox('duplicate_multiple_projects', t('Duplicate to multiple projects'), 1) ?>
+ <?php endif ?>
+
+ <?= $this->modal->submitButtons() ?>
+ </div>
+ </div>
+</form>
diff --git a/plugins/Group_assign/Template/task_modification/show.php b/plugins/Group_assign/Template/task_modification/show.php
new file mode 100644
index 00000000..31599ef6
--- /dev/null
+++ b/plugins/Group_assign/Template/task_modification/show.php
@@ -0,0 +1,43 @@
+<div class="page-header">
+ <h2><?= $this->text->e($project['name']) ?> &gt; <?= $this->text->e($task['title']) ?></h2>
+</div>
+<form method="post" action="<?= $this->url->href('GroupAssignTaskModificationController', 'update', array('plugin' => 'Group_assign', 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <div class="task-form-container">
+ <div class="task-form-main-column">
+ <?= $this->task->renderTitleField($values, $errors) ?>
+ <?= $this->task->renderDescriptionField($values, $errors) ?>
+ <?= $this->task->renderDescriptionTemplateDropdown($project['id']) ?>
+ <?= $this->task->renderTagField($project, $tags) ?>
+
+ <?= $this->hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-secondary-column">
+ <?= $this->task->renderColorField($values) ?>
+ <?= $this->task->renderAssigneeField($users_list, $values, $errors) ?>
+ <?= $this->helper->newTaskHelper->renderGroupField($values, $errors) ?>
+ <?= $this->helper->newTaskHelper->renderMultiAssigneeField($users_list, $values) ?>
+ <?= $this->task->renderCategoryField($categories_list, $values, $errors) ?>
+ <?= $this->task->renderPriorityField($project, $values) ?>
+
+ <?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-secondary-column">
+ <?= $this->task->renderDueDateField($values, $errors) ?>
+ <?= $this->task->renderStartDateField($values, $errors) ?>
+ <?= $this->task->renderTimeEstimatedField($values, $errors) ?>
+ <?= $this->task->renderTimeSpentField($values, $errors) ?>
+ <?= $this->task->renderScoreField($values, $errors) ?>
+ <?= $this->task->renderReferenceField($values, $errors) ?>
+
+ <?= $this->hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-bottom">
+ <?= $this->modal->submitButtons() ?>
+ </div>
+ </div>
+</form>
diff --git a/plugins/Group_assign/Test/Helper/NewTaskHelperTest.php b/plugins/Group_assign/Test/Helper/NewTaskHelperTest.php
new file mode 100644
index 00000000..8c378474
--- /dev/null
+++ b/plugins/Group_assign/Test/Helper/NewTaskHelperTest.php
@@ -0,0 +1,34 @@
+<?php
+
+require_once 'tests/units/Base.php';
+
+use Kanboard\Core\Plugin\Loader;
+use Kanboard\Plugin\Group_assign\Helper\NewTaskHelper;
+
+class NewTaskHelperTest extends Base
+{
+ public function setUp()
+ {
+ parent::setUp();
+ $plugin = new Loader($this->container);
+ $plugin->scan();
+ }
+ public function testSelectPriority()
+ {
+ $helper = new NewTaskHelper($this->container);
+ $this->assertNotEmpty($helper->renderPriorityField(array('priority_end' => '1', 'priority_start' => '5', 'priority_default' => '2'), array()));
+ $this->assertNotEmpty($helper->renderPriorityField(array('priority_end' => '3', 'priority_start' => '1', 'priority_default' => '2'), array()));
+ }
+ public function testFormatPriority()
+ {
+ $helper = new NewTaskHelper($this->container);
+ $this->assertEquals(
+ '<span class="task-priority" title="Task priority">P2</span>',
+ $helper->renderPriority(2)
+ );
+ $this->assertEquals(
+ '<span class="task-priority" title="Task priority">-P6</span>',
+ $helper->renderPriority(-6)
+ );
+ }
+}
diff --git a/plugins/Group_assign/Test/Model/NewTaskFinderModelTest.php b/plugins/Group_assign/Test/Model/NewTaskFinderModelTest.php
new file mode 100644
index 00000000..58ff4404
--- /dev/null
+++ b/plugins/Group_assign/Test/Model/NewTaskFinderModelTest.php
@@ -0,0 +1,184 @@
+<?php
+
+require_once 'tests/units/Base.php';
+
+use Kanboard\Core\Plugin\Loader;
+use Kanboard\Plugin\Group_assign\Model\NewTaskFinderModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\TaskModel;
+
+class NewTaskFinderModelTest extends Base
+{
+ public function setUp()
+ {
+ parent::setUp();
+ $plugin = new Loader($this->container);
+ $plugin->scan();
+ }
+
+ public function testGetDetails()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $categoryModel = new \Kanboard\Model\CategoryModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $categoryModel->create(array('project_id' => 1, 'name' => 'C1')));
+ $this->assertEquals(1, $taskCreationModel->create(array(
+ 'project_id' => 1,
+ 'title' => 'Task #1',
+ 'reference' => 'test',
+ 'description' => 'desc',
+ 'owner_id' => 1,
+ 'category_id' => 1,
+ )));
+ $task = $taskFinderModel->getDetails(1);
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals('test', $task['reference']);
+ $this->assertEquals('Task #1', $task['title']);
+ $this->assertEquals('desc', $task['description']);
+ $this->assertEquals(time(), $task['date_creation'], 'Delta', 1);
+ $this->assertEquals(time(), $task['date_modification'], 'Delta', 1);
+ $this->assertEquals(time(), $task['date_moved'], 'Delta', 1);
+ $this->assertEquals(0, $task['date_completed']);
+ $this->assertEquals(0, $task['date_due']);
+ $this->assertEquals(0, $task['date_started']);
+ $this->assertEquals(0, $task['time_estimated']);
+ $this->assertEquals(0, $task['time_spent']);
+ $this->assertEquals('yellow', $task['color_id']);
+ $this->assertEquals(1, $task['project_id']);
+ $this->assertEquals(1, $task['column_id']);
+ $this->assertEquals(1, $task['owner_id']);
+ $this->assertEquals(0, $task['creator_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(TaskModel::STATUS_OPEN, $task['is_active']);
+ $this->assertEquals(0, $task['score']);
+ $this->assertEquals(1, $task['category_id']);
+ $this->assertEquals(0, $task['priority']);
+ $this->assertEquals(1, $task['swimlane_id']);
+ $this->assertEquals(TaskModel::RECURRING_STATUS_NONE, $task['recurrence_status']);
+ $this->assertEquals(TaskModel::RECURRING_TRIGGER_FIRST_COLUMN, $task['recurrence_trigger']);
+ $this->assertEquals(0, $task['recurrence_factor']);
+ $this->assertEquals(TaskModel::RECURRING_TIMEFRAME_DAYS, $task['recurrence_timeframe']);
+ $this->assertEquals(TaskModel::RECURRING_BASEDATE_DUEDATE, $task['recurrence_basedate']);
+ $this->assertEquals(0, $task['recurrence_parent']);
+ $this->assertEquals(0, $task['recurrence_child']);
+ $this->assertEquals('C1', $task['category_name']);
+ $this->assertEquals('Default swimlane', $task['swimlane_name']);
+ $this->assertEquals('Project #1', $task['project_name']);
+ $this->assertEquals('Backlog', $task['column_title']);
+ $this->assertEquals('admin', $task['assignee_username']);
+ $this->assertEquals('', $task['assignee_name']);
+ $this->assertEquals('', $task['creator_username']);
+ $this->assertEquals('', $task['creator_name']);
+ }
+ public function testGetTasksForDashboardWithHiddenColumn()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $columnModel = new ColumnModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1)));
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(2, $tasks);
+ $this->assertTrue($columnModel->update(2, 'Test', 0, '', 1));
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ $this->assertEquals(1, $tasks[0]['column_id']);
+ $this->assertTrue($columnModel->update(2, 'Test', 0, '', 0));
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(2, $tasks);
+ }
+ public function testGetOverdueTasks()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1)));
+ $tasks = $taskFinderModel->getOverdueTasks();
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ }
+ public function testGetOverdueTasksByProject()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1)));
+ $tasks = $taskFinderModel->getOverdueTasksByProject(1);
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ }
+ public function testGetOverdueTasksByUser()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'owner_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'owner_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1)));
+ $tasks = $taskFinderModel->getOverdueTasksByUser(1);
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(2, $tasks);
+ $this->assertEquals(1, $tasks[0]['id']);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ $this->assertEquals(1, $tasks[0]['owner_id']);
+ $this->assertEquals(1, $tasks[0]['project_id']);
+ $this->assertEquals('Project #1', $tasks[0]['project_name']);
+ $this->assertEquals('admin', $tasks[0]['assignee_username']);
+ $this->assertEquals('', $tasks[0]['assignee_name']);
+ $this->assertEquals('Task #2', $tasks[1]['title']);
+ }
+ public function testCountByProject()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2)));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 2)));
+ $this->assertEquals(1, $taskFinderModel->countByProjectId(1));
+ $this->assertEquals(2, $taskFinderModel->countByProjectId(2));
+ }
+ public function testGetProjectToken()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertTrue($projectModel->enablePublicAccess(1));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2)));
+ $project = $projectModel->getById(1);
+ $this->assertEquals($project['token'], $taskFinderModel->getProjectToken(1));
+ $this->assertEmpty($taskFinderModel->getProjectToken(2));
+ }
+}
diff --git a/plugins/Group_assign/Test/PluginTest.php b/plugins/Group_assign/Test/PluginTest.php
new file mode 100644
index 00000000..c647786c
--- /dev/null
+++ b/plugins/Group_assign/Test/PluginTest.php
@@ -0,0 +1,19 @@
+<?php
+
+require_once 'tests/units/Base.php';
+
+use Kanboard\Plugin\Group_assign\Plugin;
+
+class PluginTest extends Base
+{
+ public function testPlugin()
+ {
+ $plugin = new Plugin($this->container);
+ $this->assertSame(null, $plugin->initialize());
+ $this->assertNotEmpty($plugin->getPluginName());
+ $this->assertNotEmpty($plugin->getPluginDescription());
+ $this->assertNotEmpty($plugin->getPluginAuthor());
+ $this->assertNotEmpty($plugin->getPluginVersion());
+ $this->assertNotEmpty($plugin->getPluginHomepage());
+ }
+}
diff --git a/plugins/Group_assign/_config.yml b/plugins/Group_assign/_config.yml
new file mode 100644
index 00000000..be854e84
--- /dev/null
+++ b/plugins/Group_assign/_config.yml
@@ -0,0 +1,3 @@
+theme: jekyll-theme-cayman
+plugins:
+ - jemoji