diff options
Diffstat (limited to 'app/Model')
-rw-r--r-- | app/Model/Action.php | 2 | ||||
-rw-r--r-- | app/Model/Comment.php | 9 | ||||
-rw-r--r-- | app/Model/Notification.php | 54 | ||||
-rw-r--r-- | app/Model/ProjectGroupRoleFilter.php | 89 | ||||
-rw-r--r-- | app/Model/ProjectPermission.php | 34 | ||||
-rw-r--r-- | app/Model/ProjectUserRoleFilter.php | 88 | ||||
-rw-r--r-- | app/Model/Task.php | 1 | ||||
-rw-r--r-- | app/Model/TaskCreation.php | 11 | ||||
-rw-r--r-- | app/Model/TaskPosition.php | 180 | ||||
-rw-r--r-- | app/Model/UserMention.php | 61 | ||||
-rw-r--r-- | app/Model/UserNotification.php | 19 |
11 files changed, 382 insertions, 166 deletions
diff --git a/app/Model/Action.php b/app/Model/Action.php index 289471f4..d3d18edb 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -427,7 +427,7 @@ class Action extends Base return $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; case 'user_id': case 'owner_id': - return $this->projectPermission->isMember($project_id, $param['value']) ? $param['value'] : false; + return $this->projectPermission->isAssignable($project_id, $param['value']) ? $param['value'] : false; default: return $param['value']; } diff --git a/app/Model/Comment.php b/app/Model/Comment.php index f60a96e3..71e964dc 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -26,8 +26,9 @@ class Comment extends Base * * @var string */ - const EVENT_UPDATE = 'comment.update'; - const EVENT_CREATE = 'comment.create'; + const EVENT_UPDATE = 'comment.update'; + const EVENT_CREATE = 'comment.create'; + const EVENT_USER_MENTION = 'comment.user.mention'; /** * Get all comments for a given task @@ -110,7 +111,9 @@ class Comment extends Base $comment_id = $this->persist(self::TABLE, $values); if ($comment_id) { - $this->container['dispatcher']->dispatch(self::EVENT_CREATE, new CommentEvent(array('id' => $comment_id) + $values)); + $event = new CommentEvent(array('id' => $comment_id) + $values); + $this->dispatcher->dispatch(self::EVENT_CREATE, $event); + $this->userMention->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event); } return $comment_id; diff --git a/app/Model/Notification.php b/app/Model/Notification.php index f1122993..87c1a796 100644 --- a/app/Model/Notification.php +++ b/app/Model/Notification.php @@ -74,6 +74,10 @@ class Notification extends Base return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); case File::EVENT_CREATE: return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); + case Task::EVENT_USER_MENTION: + return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']); + case Comment::EVENT_USER_MENTION: + return e('%s mentioned you in a comment on the task #%d', $event_author, $event_data['task']['id']); default: return e('Notification'); } @@ -91,52 +95,40 @@ class Notification extends Base { switch ($event_name) { case File::EVENT_CREATE: - $title = e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); - break; + return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); case Comment::EVENT_CREATE: - $title = e('New comment on task #%d', $event_data['comment']['task_id']); - break; + return e('New comment on task #%d', $event_data['comment']['task_id']); case Comment::EVENT_UPDATE: - $title = e('Comment updated on task #%d', $event_data['comment']['task_id']); - break; + return e('Comment updated on task #%d', $event_data['comment']['task_id']); case Subtask::EVENT_CREATE: - $title = e('New subtask on task #%d', $event_data['subtask']['task_id']); - break; + return e('New subtask on task #%d', $event_data['subtask']['task_id']); case Subtask::EVENT_UPDATE: - $title = e('Subtask updated on task #%d', $event_data['subtask']['task_id']); - break; + return e('Subtask updated on task #%d', $event_data['subtask']['task_id']); case Task::EVENT_CREATE: - $title = e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); - break; + return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); case Task::EVENT_UPDATE: - $title = e('Task updated #%d', $event_data['task']['id']); - break; + return e('Task updated #%d', $event_data['task']['id']); case Task::EVENT_CLOSE: - $title = e('Task #%d closed', $event_data['task']['id']); - break; + return e('Task #%d closed', $event_data['task']['id']); case Task::EVENT_OPEN: - $title = e('Task #%d opened', $event_data['task']['id']); - break; + return e('Task #%d opened', $event_data['task']['id']); case Task::EVENT_MOVE_COLUMN: - $title = e('Column changed for task #%d', $event_data['task']['id']); - break; + return e('Column changed for task #%d', $event_data['task']['id']); case Task::EVENT_MOVE_POSITION: - $title = e('New position for task #%d', $event_data['task']['id']); - break; + return e('New position for task #%d', $event_data['task']['id']); case Task::EVENT_MOVE_SWIMLANE: - $title = e('Swimlane changed for task #%d', $event_data['task']['id']); - break; + return e('Swimlane changed for task #%d', $event_data['task']['id']); case Task::EVENT_ASSIGNEE_CHANGE: - $title = e('Assignee changed on task #%d', $event_data['task']['id']); - break; + return e('Assignee changed on task #%d', $event_data['task']['id']); case Task::EVENT_OVERDUE: $nb = count($event_data['tasks']); - $title = $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); - break; + return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); + case Task::EVENT_USER_MENTION: + return e('You were mentioned in the task #%d', $event_data['task']['id']); + case Comment::EVENT_USER_MENTION: + return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']); default: - $title = e('Notification'); + return e('Notification'); } - - return $title; } } diff --git a/app/Model/ProjectGroupRoleFilter.php b/app/Model/ProjectGroupRoleFilter.php new file mode 100644 index 00000000..989d3073 --- /dev/null +++ b/app/Model/ProjectGroupRoleFilter.php @@ -0,0 +1,89 @@ +<?php + +namespace Kanboard\Model; + +/** + * Project Group Role Filter + * + * @package model + * @author Frederic Guillot + */ +class ProjectGroupRoleFilter extends Base +{ + /** + * Query + * + * @access protected + * @var \PicoDb\Table + */ + protected $query; + + /** + * Initialize filter + * + * @access public + * @return UserFilter + */ + public function create() + { + $this->query = $this->db->table(ProjectGroupRole::TABLE); + return $this; + } + + /** + * Get all results of the filter + * + * @access public + * @param string $column + * @return array + */ + public function findAll($column = '') + { + if ($column !== '') { + return $this->query->asc($column)->findAllByColumn($column); + } + + return $this->query->findAll(); + } + + /** + * Get the PicoDb query + * + * @access public + * @return \PicoDb\Table + */ + public function getQuery() + { + return $this->query; + } + + /** + * Filter by project id + * + * @access public + * @param integer $project_id + * @return ProjectUserRoleFilter + */ + public function filterByProjectId($project_id) + { + $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id); + return $this; + } + + /** + * Filter by username + * + * @access public + * @param string $input + * @return ProjectUserRoleFilter + */ + public function startWithUsername($input) + { + $this->query + ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) + ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->ilike(User::TABLE.'.username', $input.'%'); + + return $this; + } +} diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php index 4ad9bbf1..cea62e13 100644 --- a/app/Model/ProjectPermission.php +++ b/app/Model/ProjectPermission.php @@ -44,6 +44,25 @@ class ProjectPermission extends Base } /** + * Get all usernames (fetch users from groups) + * + * @access public + * @param integer $project_id + * @param string $input + * @return array + */ + public function findUsernames($project_id, $input) + { + $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); + $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); + $members = array_unique(array_merge($userMembers, $groupMembers)); + + sort($members); + + return $members; + } + + /** * Return true if everybody is allowed for the project * * @access public @@ -86,12 +105,25 @@ class ProjectPermission extends Base * @param integer $user_id * @return boolean */ - public function isMember($project_id, $user_id) + public function isAssignable($project_id, $user_id) { return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); } /** + * Return true if the user is member + * + * @access public + * @param integer $project_id + * @param integer $user_id + * @return boolean + */ + public function isMember($project_id, $user_id) + { + return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER, Role::PROJECT_VIEWER)); + } + + /** * Get active project ids by user * * @access public diff --git a/app/Model/ProjectUserRoleFilter.php b/app/Model/ProjectUserRoleFilter.php new file mode 100644 index 00000000..64403643 --- /dev/null +++ b/app/Model/ProjectUserRoleFilter.php @@ -0,0 +1,88 @@ +<?php + +namespace Kanboard\Model; + +/** + * Project User Role Filter + * + * @package model + * @author Frederic Guillot + */ +class ProjectUserRoleFilter extends Base +{ + /** + * Query + * + * @access protected + * @var \PicoDb\Table + */ + protected $query; + + /** + * Initialize filter + * + * @access public + * @return UserFilter + */ + public function create() + { + $this->query = $this->db->table(ProjectUserRole::TABLE); + return $this; + } + + /** + * Get all results of the filter + * + * @access public + * @param string $column + * @return array + */ + public function findAll($column = '') + { + if ($column !== '') { + return $this->query->asc($column)->findAllByColumn($column); + } + + return $this->query->findAll(); + } + + /** + * Get the PicoDb query + * + * @access public + * @return \PicoDb\Table + */ + public function getQuery() + { + return $this->query; + } + + /** + * Filter by project id + * + * @access public + * @param integer $project_id + * @return ProjectUserRoleFilter + */ + public function filterByProjectId($project_id) + { + $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id); + return $this; + } + + /** + * Filter by username + * + * @access public + * @param string $input + * @return ProjectUserRoleFilter + */ + public function startWithUsername($input) + { + $this->query + ->join(User::TABLE, 'id', 'user_id') + ->ilike(User::TABLE.'.username', $input.'%'); + + return $this; + } +} diff --git a/app/Model/Task.php b/app/Model/Task.php index f1cd094f..7aa9e312 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -41,6 +41,7 @@ class Task extends Base const EVENT_CREATE_UPDATE = 'task.create_update'; const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change'; const EVENT_OVERDUE = 'task.overdue'; + const EVENT_USER_MENTION = 'task.user.mention'; /** * Recurrence: status diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php index 5ef1a04b..88912d4d 100644 --- a/app/Model/TaskCreation.php +++ b/app/Model/TaskCreation.php @@ -86,8 +86,13 @@ class TaskCreation extends Base */ private function fireEvents($task_id, array $values) { - $values['task_id'] = $task_id; - $this->container['dispatcher']->dispatch(Task::EVENT_CREATE_UPDATE, new TaskEvent($values)); - $this->container['dispatcher']->dispatch(Task::EVENT_CREATE, new TaskEvent($values)); + $event = new TaskEvent(array('task_id' => $task_id) + $values); + + $this->dispatcher->dispatch(Task::EVENT_CREATE_UPDATE, $event); + $this->dispatcher->dispatch(Task::EVENT_CREATE, $event); + + if (! empty($values['description'])) { + $this->userMention->fireEvents($values['description'], Task::EVENT_USER_MENTION, $event); + } } } diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php index da363cb3..53193c9b 100644 --- a/app/Model/TaskPosition.php +++ b/app/Model/TaskPosition.php @@ -26,28 +26,34 @@ class TaskPosition extends Base */ public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true) { + $result = false; + if ($position < 1) { - return false; + return $result; } $task = $this->taskFinder->getById($task_id); - // Ignore closed tasks if ($task['is_active'] == Task::STATUS_CLOSED) { return true; } - $result = false; + $this->db->startTransaction(); - if ($task['swimlane_id'] != $swimlane_id) { - $result = $this->saveSwimlaneChange($project_id, $task_id, $position, $task['column_id'], $column_id, $task['swimlane_id'], $swimlane_id); - } elseif ($task['column_id'] != $column_id) { - $result = $this->saveColumnChange($project_id, $task_id, $position, $swimlane_id, $task['column_id'], $column_id); + if ($task['swimlane_id'] != $swimlane_id || $task['column_id'] != $column_id) { + $result = $this->moveTaskToAnotherColumn($task, $swimlane_id, $column_id, $position); } elseif ($task['position'] != $position) { - $result = $this->savePositionChange($project_id, $task_id, $position, $column_id, $swimlane_id); + $result = $this->moveTaskWithinSameColumn($task, $position); } - if ($result && $fire_events) { + if (! $result) { + $this->db->cancelTransaction(); + return false; + } + + $this->db->closeTransaction(); + + if ($fire_events) { $this->fireEvents($task, $column_id, $position, $swimlane_id); } @@ -55,145 +61,87 @@ class TaskPosition extends Base } /** - * Move a task to another swimlane + * Move a task to another column/swimlane * * @access private - * @param integer $project_id - * @param integer $task_id - * @param integer $position - * @param integer $original_column_id - * @param integer $new_column_id - * @param integer $original_swimlane_id - * @param integer $new_swimlane_id + * @param array $task + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $position * @return boolean */ - private function saveSwimlaneChange($project_id, $task_id, $position, $original_column_id, $new_column_id, $original_swimlane_id, $new_swimlane_id) + private function moveTaskToAnotherColumn(array $task, $swimlane_id, $column_id, $position) { - $this->db->startTransaction(); - $r1 = $this->saveTaskPositions($project_id, $task_id, 0, $original_column_id, $original_swimlane_id); - $r2 = $this->saveTaskPositions($project_id, $task_id, $position, $new_column_id, $new_swimlane_id); - $this->db->closeTransaction(); + $results = array(); + $max = $this->getQuery($task['project_id'], $swimlane_id, $column_id)->count(); + $position = $max > 0 && $position > $max ? $max + 1 : $position; - return $r1 && $r2; - } + $results[] = $this->getQuery($task['project_id'], $task['swimlane_id'], $task['column_id'])->gt('position', $task['position'])->decrement('position', 1); + $results[] = $this->getQuery($task['project_id'], $swimlane_id, $column_id)->gte('position', $position)->increment('position', 1); + $results[] = $this->updateTaskPosition($task['id'], $swimlane_id, $column_id, $position); - /** - * Move a task to another column - * - * @access private - * @param integer $project_id - * @param integer $task_id - * @param integer $position - * @param integer $swimlane_id - * @param integer $original_column_id - * @param integer $new_column_id - * @return boolean - */ - private function saveColumnChange($project_id, $task_id, $position, $swimlane_id, $original_column_id, $new_column_id) - { - $this->db->startTransaction(); - $r1 = $this->saveTaskPositions($project_id, $task_id, 0, $original_column_id, $swimlane_id); - $r2 = $this->saveTaskPositions($project_id, $task_id, $position, $new_column_id, $swimlane_id); - $this->db->closeTransaction(); - - return $r1 && $r2; + return !in_array(false, $results, true); } /** - * Move a task to another position in the same column + * Move a task within the same column * * @access private - * @param integer $project_id - * @param integer $task_id - * @param integer $position - * @param integer $column_id - * @param integer $swimlane_id + * @param array $task + * @param integer $position * @return boolean */ - private function savePositionChange($project_id, $task_id, $position, $column_id, $swimlane_id) + private function moveTaskWithinSameColumn(array $task, $position) { - $this->db->startTransaction(); - $result = $this->saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id); - $this->db->closeTransaction(); + $results = array(); + $max = $this->getQuery($task['project_id'], $task['swimlane_id'], $task['column_id'])->count(); + $position = $max > 0 && $position > $max ? $max : $position; + + if ($position >= $max) { + $results[] = $this->getQuery($task['project_id'], $task['swimlane_id'], $task['column_id'])->lte('position', $position)->decrement('position', 1); + } else { + $results[] = $this->getQuery($task['project_id'], $task['swimlane_id'], $task['column_id'])->gte('position', $position)->increment('position', 1); + } - return $result; + $results[] = $this->updateTaskPosition($task['id'], $task['swimlane_id'], $task['column_id'], $position); + + return !in_array(false, $results, true); } /** - * Save all task positions for one column + * Update final task position * * @access private - * @param integer $project_id - * @param integer $task_id - * @param integer $position - * @param integer $column_id - * @param integer $swimlane_id + * @param integer $task_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $position * @return boolean */ - private function saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id) + private function updateTaskPosition($task_id, $swimlane_id, $column_id, $position) { - $tasks_ids = $this->db->table(Task::TABLE) + return $this->db->table(Task::TABLE) + ->eq('id', $task_id) ->eq('is_active', 1) - ->eq('swimlane_id', $swimlane_id) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->neq('id', $task_id) - ->asc('position') - ->asc('id') - ->findAllByColumn('id'); - - $offset = 1; - - foreach ($tasks_ids as $current_task_id) { - - // Insert the new task - if ($position == $offset) { - if (! $this->saveTaskPosition($task_id, $offset, $column_id, $swimlane_id)) { - return false; - } - $offset++; - } - - // Rewrite other tasks position - if (! $this->saveTaskPosition($current_task_id, $offset, $column_id, $swimlane_id)) { - return false; - } - - $offset++; - } - - // Insert the new task at the bottom and normalize bad position - if ($position >= $offset && ! $this->saveTaskPosition($task_id, $offset, $column_id, $swimlane_id)) { - return false; - } - - return true; + ->update(array('position' => $position, 'column_id' => $column_id, 'swimlane_id' => $swimlane_id, 'date_modification' => time())); } /** - * Save new task position + * Get common query * * @access private - * @param integer $task_id - * @param integer $position - * @param integer $column_id - * @param integer $swimlane_id - * @return boolean + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @return \PicoDb\Table */ - private function saveTaskPosition($task_id, $position, $column_id, $swimlane_id) + private function getQuery($project_id, $swimlane_id, $column_id) { - $result = $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array( - 'position' => $position, - 'column_id' => $column_id, - 'swimlane_id' => $swimlane_id, - )); - - if (! $result) { - $this->db->cancelTransaction(); - return false; - } - - return true; + return $this->db->table(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('swimlane_id', $swimlane_id) + ->eq('column_id', $column_id) + ->eq('is_active', 1); } /** diff --git a/app/Model/UserMention.php b/app/Model/UserMention.php new file mode 100644 index 00000000..97a4e419 --- /dev/null +++ b/app/Model/UserMention.php @@ -0,0 +1,61 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Event\GenericEvent; + +/** + * User Mention + * + * @package model + * @author Frederic Guillot + */ +class UserMention extends Base +{ + /** + * Get list of mentioned users + * + * @access public + * @param string $content + * @return array + */ + public function getMentionedUsers($content) + { + $users = array(); + + if (preg_match_all('/@([^\s]+)/', $content, $matches)) { + $users = $this->db->table(User::TABLE) + ->columns('id', 'username', 'name', 'email', 'language') + ->eq('notifications_enabled', 1) + ->neq('id', $this->userSession->getId()) + ->in('username', array_unique($matches[1])) + ->findAll(); + } + + return $users; + } + + /** + * Fire events for user mentions + * + * @access public + * @param string $content + * @param string $eventName + * @param GenericEvent $event + */ + public function fireEvents($content, $eventName, GenericEvent $event) + { + if (empty($event['project_id'])) { + $event['project_id'] = $this->taskFinder->getProjectId($event['task_id']); + } + + $users = $this->getMentionedUsers($content); + + foreach ($users as $user) { + if ($this->projectPermission->isMember($event['project_id'], $user['id'])) { + $event['mention'] = $user; + $this->dispatcher->dispatch($eventName, $event); + } + } + } +} diff --git a/app/Model/UserNotification.php b/app/Model/UserNotification.php index e00f23c5..8161288c 100644 --- a/app/Model/UserNotification.php +++ b/app/Model/UserNotification.php @@ -21,18 +21,12 @@ class UserNotification extends Base */ public function sendNotifications($event_name, array $event_data) { - $logged_user_id = $this->userSession->isLogged() ? $this->userSession->getId() : 0; - $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id); - - if (! empty($users)) { - foreach ($users as $user) { - if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) { - $this->sendUserNotification($user, $event_name, $event_data); - } - } + $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $this->userSession->getId()); - // Restore locales - $this->config->setupTranslations(); + foreach ($users as $user) { + if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) { + $this->sendUserNotification($user, $event_name, $event_data); + } } } @@ -58,6 +52,9 @@ class UserNotification extends Base foreach ($this->userNotificationType->getSelectedTypes($user['id']) as $type) { $this->userNotificationType->getType($type)->notifyUser($user, $event_name, $event_data); } + + // Restore locales + $this->config->setupTranslations(); } /** |