diff options
Diffstat (limited to 'app/Model')
35 files changed, 1372 insertions, 717 deletions
diff --git a/app/Model/ActionParameterModel.php b/app/Model/ActionParameterModel.php index 9895da0f..cdac396e 100644 --- a/app/Model/ActionParameterModel.php +++ b/app/Model/ActionParameterModel.php @@ -157,6 +157,9 @@ class ActionParameterModel extends Base case 'user_id': case 'owner_id': return $this->projectPermissionModel->isAssignable($project_id, $value) ? $value : false; + case 'swimlane_id': + $column = $this->swimlaneModel->getById($value); + return empty($column) ? false : $this->swimlaneModel->getIdByName($project_id, $column['name']) ?: false; default: return $value; } diff --git a/app/Model/ColorModel.php b/app/Model/ColorModel.php index 9e69dda2..9fa7ff85 100644 --- a/app/Model/ColorModel.php +++ b/app/Model/ColorModel.php @@ -154,6 +154,8 @@ class ColorModel extends Base $listing[$color_id] = t($color['name']); } + $this->hook->reference('model:color:get-list', $listing); + return $listing; } diff --git a/app/Model/ColumnModel.php b/app/Model/ColumnModel.php index 0a9c55a8..5498ef54 100644 --- a/app/Model/ColumnModel.php +++ b/app/Model/ColumnModel.php @@ -138,11 +138,12 @@ class ColumnModel extends Base * Add a new column to the board * * @access public - * @param integer $project_id Project id - * @param string $title Column title - * @param integer $task_limit Task limit - * @param string $description Column description - * @return boolean|integer + * @param integer $project_id Project id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Column description + * @param integer $hide_in_dashboard + * @return bool|int */ public function create($project_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0) { @@ -166,6 +167,7 @@ class ColumnModel extends Base * @param string $title Column title * @param integer $task_limit Task limit * @param string $description Optional description + * @param integer $hide_in_dashboard * @return boolean */ public function update($column_id, $title, $task_limit = 0, $description = '', $hide_in_dashboard = 0) diff --git a/app/Model/ColumnMoveRestrictionModel.php b/app/Model/ColumnMoveRestrictionModel.php new file mode 100644 index 00000000..c2603efd --- /dev/null +++ b/app/Model/ColumnMoveRestrictionModel.php @@ -0,0 +1,122 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Class ColumnMoveRestrictionModel + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class ColumnMoveRestrictionModel extends Base +{ + const TABLE = 'column_has_move_restrictions'; + + /** + * Fetch one restriction + * + * @param int $project_id + * @param int $restriction_id + * @return array|null + */ + public function getById($project_id, $restriction_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.src_column_id', + self::TABLE.'.dst_column_id', + 'pr.role', + 'sc.title as src_column_title', + 'dc.title as dst_column_title' + ) + ->left(ColumnModel::TABLE, 'sc', 'id', self::TABLE, 'src_column_id') + ->left(ColumnModel::TABLE, 'dc', 'id', self::TABLE, 'dst_column_id') + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') + ->eq(self::TABLE.'.project_id', $project_id) + ->eq(self::TABLE.'.restriction_id', $restriction_id) + ->findOne(); + } + + /** + * Get all project column restrictions + * + * @param int $project_id + * @return array + */ + public function getAll($project_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.src_column_id', + self::TABLE.'.dst_column_id', + 'pr.role', + 'sc.title as src_column_title', + 'dc.title as dst_column_title' + ) + ->left(ColumnModel::TABLE, 'sc', 'id', self::TABLE, 'src_column_id') + ->left(ColumnModel::TABLE, 'dc', 'id', self::TABLE, 'dst_column_id') + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') + ->eq(self::TABLE.'.project_id', $project_id) + ->findAll(); + } + + /** + * Get all sortable column Ids + * + * @param int $project_id + * @param string $role + * @return array + */ + public function getSortableColumns($project_id, $role) + { + return $this->db + ->table(self::TABLE) + ->columns(self::TABLE.'.src_column_id', self::TABLE.'.dst_column_id') + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') + ->eq(self::TABLE.'.project_id', $project_id) + ->eq('pr.role', $role) + ->findAll(); + } + + /** + * Create a new column restriction + * + * @param int $project_id + * @param int $role_id + * @param int $src_column_id + * @param int $dst_column_id + * @return bool|int + */ + public function create($project_id, $role_id, $src_column_id, $dst_column_id) + { + return $this->db + ->table(self::TABLE) + ->persist(array( + 'project_id' => $project_id, + 'role_id' => $role_id, + 'src_column_id' => $src_column_id, + 'dst_column_id' => $dst_column_id, + )); + } + + /** + * Remove a permission + * + * @param int $restriction_id + * @return bool + */ + public function remove($restriction_id) + { + return $this->db->table(self::TABLE)->eq('restriction_id', $restriction_id)->remove(); + } +} diff --git a/app/Model/ColumnRestrictionModel.php b/app/Model/ColumnRestrictionModel.php new file mode 100644 index 00000000..92b2ac60 --- /dev/null +++ b/app/Model/ColumnRestrictionModel.php @@ -0,0 +1,152 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Class ColumnRestrictionModel + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class ColumnRestrictionModel extends Base +{ + const TABLE = 'column_has_restrictions'; + + const RULE_ALLOW_TASK_CREATION = 'allow.task_creation'; + const RULE_ALLOW_TASK_OPEN_CLOSE = 'allow.task_open_close'; + const RULE_BLOCK_TASK_CREATION = 'block.task_creation'; + const RULE_BLOCK_TASK_OPEN_CLOSE = 'block.task_open_close'; + + /** + * Get rules + * + * @return array + */ + public function getRules() + { + return array( + self::RULE_ALLOW_TASK_CREATION => t('Task creation is permitted for this column'), + self::RULE_ALLOW_TASK_OPEN_CLOSE => t('Closing or opening a task is permitted for this column'), + self::RULE_BLOCK_TASK_CREATION => t('Task creation is blocked for this column'), + self::RULE_BLOCK_TASK_OPEN_CLOSE => t('Closing or opening a task is blocked for this column'), + ); + } + + /** + * Fetch one restriction + * + * @param int $project_id + * @param int $restriction_id + * @return array|null + */ + public function getById($project_id, $restriction_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.column_id', + self::TABLE.'.rule', + 'pr.role', + 'c.title as column_title' + ) + ->left(ColumnModel::TABLE, 'c', 'id', self::TABLE, 'column_id') + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') + ->eq(self::TABLE.'.project_id', $project_id) + ->eq(self::TABLE.'.restriction_id', $restriction_id) + ->findOne(); + } + + /** + * Get all project column restrictions + * + * @param int $project_id + * @return array + */ + public function getAll($project_id) + { + $rules = $this->getRules(); + $restrictions = $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.column_id', + self::TABLE.'.rule', + 'pr.role', + 'c.title as column_title' + ) + ->left(ColumnModel::TABLE, 'c', 'id', self::TABLE, 'column_id') + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') + ->eq(self::TABLE.'.project_id', $project_id) + ->findAll(); + + foreach ($restrictions as &$restriction) { + $restriction['title'] = $rules[$restriction['rule']]; + } + + return $restrictions; + } + + /** + * Get restrictions + * + * @param int $project_id + * @param string $role + * @return array + */ + public function getAllByRole($project_id, $role) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.column_id', + self::TABLE.'.rule', + 'pr.role' + ) + ->eq(self::TABLE.'.project_id', $project_id) + ->eq('pr.role', $role) + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') + ->findAll(); + } + + /** + * Create a new column restriction + * + * @param int $project_id + * @param int $role_id + * @param int $column_id + * @param int $rule + * @return bool|int + */ + public function create($project_id, $role_id, $column_id, $rule) + { + return $this->db + ->table(self::TABLE) + ->persist(array( + 'project_id' => $project_id, + 'role_id' => $role_id, + 'column_id' => $column_id, + 'rule' => $rule, + )); + } + + /** + * Remove a permission + * + * @param int $restriction_id + * @return bool + */ + public function remove($restriction_id) + { + return $this->db->table(self::TABLE)->eq('restriction_id', $restriction_id)->remove(); + } +} diff --git a/app/Model/CommentModel.php b/app/Model/CommentModel.php index 4231f29d..e44a5ecd 100644 --- a/app/Model/CommentModel.php +++ b/app/Model/CommentModel.php @@ -2,7 +2,6 @@ namespace Kanboard\Model; -use Kanboard\Event\CommentEvent; use Kanboard\Core\Base; /** @@ -27,6 +26,7 @@ class CommentModel extends Base */ const EVENT_UPDATE = 'comment.update'; const EVENT_CREATE = 'comment.create'; + const EVENT_DELETE = 'comment.delete'; const EVENT_USER_MENTION = 'comment.user.mention'; /** @@ -60,6 +60,7 @@ class CommentModel extends Base ->columns( self::TABLE.'.id', self::TABLE.'.date_creation', + self::TABLE.'.date_modification', self::TABLE.'.task_id', self::TABLE.'.user_id', self::TABLE.'.comment', @@ -69,7 +70,7 @@ class CommentModel extends Base UserModel::TABLE.'.avatar_path' ) ->join(UserModel::TABLE, 'id', 'user_id') - ->orderBy(self::TABLE.'.date_creation', $sorting) + ->orderBy(self::TABLE.'.date_modification', $sorting) ->eq(self::TABLE.'.task_id', $task_id) ->findAll(); } @@ -90,6 +91,7 @@ class CommentModel extends Base self::TABLE.'.task_id', self::TABLE.'.user_id', self::TABLE.'.date_creation', + self::TABLE.'.date_modification', self::TABLE.'.comment', self::TABLE.'.reference', UserModel::TABLE.'.username', @@ -127,12 +129,11 @@ class CommentModel extends Base public function create(array $values) { $values['date_creation'] = time(); + $values['date_modification'] = time(); $comment_id = $this->db->table(self::TABLE)->persist($values); if ($comment_id !== false) { - $event = new CommentEvent(array('id' => $comment_id) + $values); - $this->dispatcher->dispatch(self::EVENT_CREATE, $event); - $this->userMentionModel->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event); + $this->queueManager->push($this->commentEventJob->withParams($comment_id, self::EVENT_CREATE)); } return $comment_id; @@ -150,10 +151,10 @@ class CommentModel extends Base $result = $this->db ->table(self::TABLE) ->eq('id', $values['id']) - ->update(array('comment' => $values['comment'])); + ->update(array('comment' => $values['comment'], 'date_modification' => time())); if ($result) { - $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values)); + $this->queueManager->push($this->commentEventJob->withParams($values['id'], self::EVENT_UPDATE)); } return $result; @@ -168,6 +169,7 @@ class CommentModel extends Base */ public function remove($comment_id) { + $this->commentEventJob->execute($comment_id, self::EVENT_DELETE); return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove(); } } diff --git a/app/Model/CurrencyModel.php b/app/Model/CurrencyModel.php index bfd9697c..55a5b2fe 100644 --- a/app/Model/CurrencyModel.php +++ b/app/Model/CurrencyModel.php @@ -42,6 +42,8 @@ class CurrencyModel extends Base 'NOK' => t('NOK - Norwegian Krone'), 'BAM' => t('BAM - Konvertible Mark'), 'RUB' => t('RUB - Russian Ruble'), + 'CNY' => t('CNY - Chinese Yuan'), + 'TRL' => t('TRL - Turkish Lira'), ); } diff --git a/app/Model/FileModel.php b/app/Model/FileModel.php index 8cdea9a0..b5852b08 100644 --- a/app/Model/FileModel.php +++ b/app/Model/FileModel.php @@ -5,7 +5,6 @@ namespace Kanboard\Model; use Exception; use Kanboard\Core\Base; use Kanboard\Core\Thumbnail; -use Kanboard\Event\FileEvent; use Kanboard\Core\ObjectStorage\ObjectStorageException; /** @@ -44,13 +43,13 @@ abstract class FileModel extends Base abstract protected function getPathPrefix(); /** - * Get event name + * Fire file creation event * * @abstract * @access protected - * @return string + * @param integer $file_id */ - abstract protected function getEventName(); + abstract protected function fireCreationEvent($file_id); /** * Get PicoDb query to get all files @@ -130,16 +129,16 @@ abstract class FileModel extends Base * Create a file entry in the database * * @access public - * @param integer $id Foreign key - * @param string $name Filename - * @param string $path Path on the disk - * @param integer $size File size + * @param integer $foreign_key_id Foreign key + * @param string $name Filename + * @param string $path Path on the disk + * @param integer $size File size * @return bool|integer */ - public function create($id, $name, $path, $size) + public function create($foreign_key_id, $name, $path, $size) { $values = array( - $this->getForeignKey() => $id, + $this->getForeignKey() => $foreign_key_id, 'name' => substr($name, 0, 255), 'path' => $path, 'is_image' => $this->isImage($name) ? 1 : 0, @@ -152,8 +151,7 @@ abstract class FileModel extends Base if ($result) { $file_id = (int) $this->db->getLastId(); - $event = new FileEvent($values + array('file_id' => $file_id)); - $this->dispatcher->dispatch($this->getEventName(), $event); + $this->fireCreationEvent($file_id); return $file_id; } @@ -212,9 +210,7 @@ abstract class FileModel extends Base */ public function isImage($filename) { - $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - - switch ($extension) { + switch (get_file_extension($filename)) { case 'jpeg': case 'jpg': case 'png': diff --git a/app/Model/GroupModel.php b/app/Model/GroupModel.php index 0a975570..b43423b3 100644 --- a/app/Model/GroupModel.php +++ b/app/Model/GroupModel.php @@ -116,4 +116,23 @@ class GroupModel extends Base { 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($name, $external_id); + } + + return $group_id; + } } diff --git a/app/Model/InviteModel.php b/app/Model/InviteModel.php new file mode 100644 index 00000000..13d75f69 --- /dev/null +++ b/app/Model/InviteModel.php @@ -0,0 +1,73 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; +use Kanboard\Core\Security\Token; + +/** + * Class InviteModel + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class InviteModel extends Base +{ + const TABLE = 'invites'; + + public function createInvites(array $emails, $projectId) + { + $emails = array_unique($emails); + $nb = 0; + + foreach ($emails as $email) { + $email = trim($email); + + if (! empty($email) && $this->createInvite($email, $projectId)) { + $nb++; + } + } + + return $nb; + } + + protected function createInvite($email, $projectId) + { + $values = array( + 'email' => $email, + 'project_id' => $projectId, + 'token' => Token::getToken(), + ); + + if ($this->db->table(self::TABLE)->insert($values)) { + $this->sendInvite($values); + return true; + } + + return false; + } + + protected function sendInvite(array $values) + { + $this->emailClient->send( + $values['email'], + $values['email'], + e('Kanboard Invitation'), + $this->template->render('user_invite/email', array('token' => $values['token'])) + ); + } + + public function getByToken($token) + { + return $this->db->table(self::TABLE) + ->eq('token', $token) + ->findOne(); + } + + public function remove($email) + { + return $this->db->table(self::TABLE) + ->eq('email', $email) + ->remove(); + } +} diff --git a/app/Model/MetadataModel.php b/app/Model/MetadataModel.php index 6177e5f3..0ab94092 100644 --- a/app/Model/MetadataModel.php +++ b/app/Model/MetadataModel.php @@ -101,7 +101,8 @@ abstract class MetadataModel extends Base if ($this->exists($entity_id, $key)) { $results[] = $this->db->table($this->getTable()) ->eq($this->getEntityKey(), $entity_id) - ->eq('name', $key)->update(array( + ->eq('name', $key) + ->update(array( 'value' => $value, 'changed_on' => $timestamp, 'changed_by' => $user_id, diff --git a/app/Model/NotificationModel.php b/app/Model/NotificationModel.php index 4d697b5e..803d4f18 100644 --- a/app/Model/NotificationModel.php +++ b/app/Model/NotificationModel.php @@ -3,9 +3,15 @@ namespace Kanboard\Model; use Kanboard\Core\Base; +use Kanboard\EventBuilder\CommentEventBuilder; +use Kanboard\EventBuilder\EventIteratorBuilder; +use Kanboard\EventBuilder\SubtaskEventBuilder; +use Kanboard\EventBuilder\TaskEventBuilder; +use Kanboard\EventBuilder\TaskFileEventBuilder; +use Kanboard\EventBuilder\TaskLinkEventBuilder; /** - * Notification + * Notification Model * * @package Kanboard\Model * @author Frederic Guillot @@ -16,158 +22,79 @@ class NotificationModel extends Base * Get the event title with author * * @access public - * @param string $event_author - * @param string $event_name - * @param array $event_data + * @param string $eventAuthor + * @param string $eventName + * @param array $eventData * @return string */ - public function getTitleWithAuthor($event_author, $event_name, array $event_data) + public function getTitleWithAuthor($eventAuthor, $eventName, array $eventData) { - switch ($event_name) { - case TaskModel::EVENT_ASSIGNEE_CHANGE: - $assignee = $event_data['task']['assignee_name'] ?: $event_data['task']['assignee_username']; + foreach ($this->getIteratorBuilder() as $builder) { + $title = $builder->buildTitleWithAuthor($eventAuthor, $eventName, $eventData); - if (! empty($assignee)) { - return e('%s change the assignee of the task #%d to %s', $event_author, $event_data['task']['id'], $assignee); - } - - return e('%s remove the assignee of the task %s', $event_author, e('#%d', $event_data['task']['id'])); - case TaskModel::EVENT_UPDATE: - return e('%s updated the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_CREATE: - return e('%s created the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_CLOSE: - return e('%s closed the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_OPEN: - return e('%s open the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_MOVE_COLUMN: - return e( - '%s moved the task #%d to the column "%s"', - $event_author, - $event_data['task']['id'], - $event_data['task']['column_title'] - ); - case TaskModel::EVENT_MOVE_POSITION: - return e( - '%s moved the task #%d to the position %d in the column "%s"', - $event_author, - $event_data['task']['id'], - $event_data['task']['position'], - $event_data['task']['column_title'] - ); - case TaskModel::EVENT_MOVE_SWIMLANE: - if ($event_data['task']['swimlane_id'] == 0) { - return e('%s moved the task #%d to the first swimlane', $event_author, $event_data['task']['id']); - } - - return e( - '%s moved the task #%d to the swimlane "%s"', - $event_author, - $event_data['task']['id'], - $event_data['task']['swimlane_name'] - ); - case SubtaskModel::EVENT_UPDATE: - return e('%s updated a subtask for the task #%d', $event_author, $event_data['task']['id']); - case SubtaskModel::EVENT_CREATE: - return e('%s created a subtask for the task #%d', $event_author, $event_data['task']['id']); - case CommentModel::EVENT_UPDATE: - return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']); - case CommentModel::EVENT_CREATE: - return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); - case TaskFileModel::EVENT_CREATE: - return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); - case TaskModel::EVENT_USER_MENTION: - return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']); - case CommentModel::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'); + if ($title !== '') { + return $title; + } } + + return e('Notification'); } /** * Get the event title without author * * @access public - * @param string $event_name - * @param array $event_data + * @param string $eventName + * @param array $eventData * @return string */ - public function getTitleWithoutAuthor($event_name, array $event_data) + public function getTitleWithoutAuthor($eventName, array $eventData) { - switch ($event_name) { - case TaskFileModel::EVENT_CREATE: - return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); - case CommentModel::EVENT_CREATE: - return e('New comment on task #%d', $event_data['comment']['task_id']); - case CommentModel::EVENT_UPDATE: - return e('Comment updated on task #%d', $event_data['comment']['task_id']); - case SubtaskModel::EVENT_CREATE: - return e('New subtask on task #%d', $event_data['subtask']['task_id']); - case SubtaskModel::EVENT_UPDATE: - return e('Subtask updated on task #%d', $event_data['subtask']['task_id']); - case TaskModel::EVENT_CREATE: - return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); - case TaskModel::EVENT_UPDATE: - return e('Task updated #%d', $event_data['task']['id']); - case TaskModel::EVENT_CLOSE: - return e('Task #%d closed', $event_data['task']['id']); - case TaskModel::EVENT_OPEN: - return e('Task #%d opened', $event_data['task']['id']); - case TaskModel::EVENT_MOVE_COLUMN: - return e('Column changed for task #%d', $event_data['task']['id']); - case TaskModel::EVENT_MOVE_POSITION: - return e('New position for task #%d', $event_data['task']['id']); - case TaskModel::EVENT_MOVE_SWIMLANE: - return e('Swimlane changed for task #%d', $event_data['task']['id']); - case TaskModel::EVENT_ASSIGNEE_CHANGE: - return e('Assignee changed on task #%d', $event_data['task']['id']); - case TaskModel::EVENT_OVERDUE: - $nb = count($event_data['tasks']); - return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); - case TaskModel::EVENT_USER_MENTION: - return e('You were mentioned in the task #%d', $event_data['task']['id']); - case CommentModel::EVENT_USER_MENTION: - return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']); - default: - return e('Notification'); + foreach ($this->getIteratorBuilder() as $builder) { + $title = $builder->buildTitleWithoutAuthor($eventName, $eventData); + + if ($title !== '') { + return $title; + } } + + return e('Notification'); } /** * Get task id from event * * @access public - * @param string $event_name - * @param array $event_data + * @param string $eventName + * @param array $eventData * @return integer */ - public function getTaskIdFromEvent($event_name, array $event_data) + public function getTaskIdFromEvent($eventName, array $eventData) { - switch ($event_name) { - case TaskFileModel::EVENT_CREATE: - return $event_data['file']['task_id']; - case CommentModel::EVENT_CREATE: - case CommentModel::EVENT_UPDATE: - return $event_data['comment']['task_id']; - case SubtaskModel::EVENT_CREATE: - case SubtaskModel::EVENT_UPDATE: - return $event_data['subtask']['task_id']; - case TaskModel::EVENT_CREATE: - case TaskModel::EVENT_UPDATE: - case TaskModel::EVENT_CLOSE: - case TaskModel::EVENT_OPEN: - case TaskModel::EVENT_MOVE_COLUMN: - case TaskModel::EVENT_MOVE_POSITION: - case TaskModel::EVENT_MOVE_SWIMLANE: - case TaskModel::EVENT_ASSIGNEE_CHANGE: - case CommentModel::EVENT_USER_MENTION: - case TaskModel::EVENT_USER_MENTION: - return $event_data['task']['id']; - case TaskModel::EVENT_OVERDUE: - return $event_data['tasks'][0]['id']; - default: - return 0; + if ($eventName === TaskModel::EVENT_OVERDUE) { + return $eventData['tasks'][0]['id']; } + + return isset($eventData['task']['id']) ? $eventData['task']['id'] : 0; + } + + /** + * Get iterator builder + * + * @access protected + * @return EventIteratorBuilder + */ + protected function getIteratorBuilder() + { + $iterator = new EventIteratorBuilder(); + $iterator + ->withBuilder(TaskEventBuilder::getInstance($this->container)) + ->withBuilder(CommentEventBuilder::getInstance($this->container)) + ->withBuilder(SubtaskEventBuilder::getInstance($this->container)) + ->withBuilder(TaskFileEventBuilder::getInstance($this->container)) + ->withBuilder(TaskLinkEventBuilder::getInstance($this->container)) + ; + + return $iterator; } } diff --git a/app/Model/ProjectDuplicationModel.php b/app/Model/ProjectDuplicationModel.php index 94b83c80..d32fa367 100644 --- a/app/Model/ProjectDuplicationModel.php +++ b/app/Model/ProjectDuplicationModel.php @@ -159,7 +159,7 @@ class ProjectDuplicationModel extends Base } /** - * Make sure that the creator of the duplicated project is alsp owner + * Make sure that the creator of the duplicated project is also owner * * @access private * @param integer $dst_project_id diff --git a/app/Model/ProjectFileModel.php b/app/Model/ProjectFileModel.php index b464bb2a..4de4d66d 100644 --- a/app/Model/ProjectFileModel.php +++ b/app/Model/ProjectFileModel.php @@ -61,14 +61,13 @@ class ProjectFileModel extends FileModel } /** - * Get event name + * Fire file creation event * - * @abstract * @access protected - * @return string + * @param integer $file_id */ - protected function getEventName() + protected function fireCreationEvent($file_id) { - return self::EVENT_CREATE; + $this->queueManager->push($this->projectFileEventJob->withParams($file_id, self::EVENT_CREATE)); } } diff --git a/app/Model/ProjectModel.php b/app/Model/ProjectModel.php index 850531c9..aba5eee2 100644 --- a/app/Model/ProjectModel.php +++ b/app/Model/ProjectModel.php @@ -318,7 +318,7 @@ class ProjectModel extends Base public function getQueryColumnStats(array $project_ids) { if (empty($project_ids)) { - return $this->db->table(ProjectModel::TABLE)->limit(0); + return $this->db->table(ProjectModel::TABLE)->eq(ProjectModel::TABLE.'.id', 0); } return $this->db @@ -419,6 +419,14 @@ class ProjectModel extends Base $values['identifier'] = strtoupper($values['identifier']); } + if (! empty($values['start_date'])) { + $values['start_date'] = $this->dateParser->getIsoDate($values['start_date']); + } + + if (! empty($values['end_date'])) { + $values['end_date'] = $this->dateParser->getIsoDate($values['end_date']); + } + $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); return $this->exists($values['id']) && diff --git a/app/Model/ProjectPermissionModel.php b/app/Model/ProjectPermissionModel.php index a7c1857c..dabd406c 100644 --- a/app/Model/ProjectPermissionModel.php +++ b/app/Model/ProjectPermissionModel.php @@ -62,17 +62,33 @@ class ProjectPermissionModel extends Base ->withFilter(new ProjectUserRoleProjectFilter($project_id)) ->withFilter(new ProjectUserRoleUsernameFilter($input)) ->getQuery() - ->findAllByColumn('username'); + ->columns( + UserModel::TABLE.'.id', + UserModel::TABLE.'.username', + UserModel::TABLE.'.name', + UserModel::TABLE.'.email', + UserModel::TABLE.'.avatar_path' + ) + ->findAll(); $groupMembers = $this->projectGroupRoleQuery ->withFilter(new ProjectGroupRoleProjectFilter($project_id)) ->withFilter(new ProjectGroupRoleUsernameFilter($input)) ->getQuery() - ->findAllByColumn('username'); + ->columns( + UserModel::TABLE.'.id', + UserModel::TABLE.'.username', + UserModel::TABLE.'.name', + UserModel::TABLE.'.email', + UserModel::TABLE.'.avatar_path' + ) + ->findAll(); - $members = array_unique(array_merge($userMembers, $groupMembers)); + $userMembers = array_column_index_unique($userMembers, 'username'); + $groupMembers = array_column_index_unique($groupMembers, 'username'); + $members = array_merge($userMembers, $groupMembers); - sort($members); + ksort($members); return $members; } @@ -122,8 +138,13 @@ class ProjectPermissionModel extends Base */ public function isAssignable($project_id, $user_id) { - return $this->userModel->isActive($user_id) && - in_array($this->projectUserRoleModel->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); + if ($this->userModel->isActive($user_id)) { + $role = $this->projectUserRoleModel->getUserRole($project_id, $user_id); + + return ! empty($role) && $role !== Role::PROJECT_VIEWER; + } + + return false; } /** @@ -152,6 +173,18 @@ class ProjectPermissionModel extends Base } /** + * Get all project ids by user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getProjectIds($user_id) + { + return array_keys($this->projectUserRoleModel->getProjectsByUser($user_id)); + } + + /** * Copy permissions to another project * * @param integer $project_src_id Project Template diff --git a/app/Model/ProjectRoleModel.php b/app/Model/ProjectRoleModel.php new file mode 100644 index 00000000..962ff44f --- /dev/null +++ b/app/Model/ProjectRoleModel.php @@ -0,0 +1,196 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; +use Kanboard\Core\Security\Role; + +/** + * Class ProjectRoleModel + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class ProjectRoleModel extends Base +{ + const TABLE = 'project_has_roles'; + + /** + * Get list of project roles + * + * @param int $project_id + * @return array + */ + public function getList($project_id) + { + $defaultRoles = $this->role->getProjectRoles(); + $customRoles = $this->db + ->hashtable(self::TABLE) + ->eq('project_id', $project_id) + ->getAll('role', 'role'); + + return $defaultRoles + $customRoles; + } + + /** + * Get a role + * + * @param int $project_id + * @param int $role_id + * @return array|null + */ + public function getById($project_id, $role_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('role_id', $role_id) + ->findOne(); + } + + /** + * Get all project roles + * + * @param int $project_id + * @return array + */ + public function getAll($project_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->asc('role') + ->findAll(); + } + + /** + * Get all project roles with restrictions + * + * @param int $project_id + * @return array + */ + public function getAllWithRestrictions($project_id) + { + $roles = $this->getAll($project_id); + + $column_restrictions = $this->columnRestrictionModel->getAll($project_id); + $column_restrictions = array_column_index($column_restrictions, 'role_id'); + array_merge_relation($roles, $column_restrictions, 'column_restrictions', 'role_id'); + + $column_move_restrictions = $this->columnMoveRestrictionModel->getAll($project_id); + $column_move_restrictions = array_column_index($column_move_restrictions, 'role_id'); + array_merge_relation($roles, $column_move_restrictions, 'column_move_restrictions', 'role_id'); + + $project_restrictions = $this->projectRoleRestrictionModel->getAll($project_id); + $project_restrictions = array_column_index($project_restrictions, 'role_id'); + array_merge_relation($roles, $project_restrictions, 'project_restrictions', 'role_id'); + + return $roles; + } + + /** + * Create a new project role + * + * @param int $project_id + * @param string $role + * @return bool|int + */ + public function create($project_id, $role) + { + return $this->db + ->table(self::TABLE) + ->persist(array( + 'project_id' => $project_id, + 'role' => $role, + )); + } + + /** + * Update a project role + * + * @param int $role_id + * @param int $project_id + * @param string $role + * @return bool + */ + public function update($role_id, $project_id, $role) + { + $this->db->startTransaction(); + + $previousRole = $this->getById($project_id, $role_id); + + $r1 = $this->db + ->table(ProjectUserRoleModel::TABLE) + ->eq('project_id', $project_id) + ->eq('role', $previousRole['role']) + ->update(array( + 'role' => $role + )); + + $r2 = $this->db + ->table(ProjectGroupRoleModel::TABLE) + ->eq('project_id', $project_id) + ->eq('role', $previousRole['role']) + ->update(array( + 'role' => $role + )); + + $r3 = $this->db + ->table(self::TABLE) + ->eq('role_id', $role_id) + ->eq('project_id', $project_id) + ->update(array( + 'role' => $role, + )); + + if ($r1 && $r2 && $r3) { + $this->db->closeTransaction(); + return true; + } + + $this->db->cancelTransaction(); + return false; + } + + /** + * Remove a project role + * + * @param int $project_id + * @param int $role_id + * @return bool + */ + public function remove($project_id, $role_id) + { + $this->db->startTransaction(); + + $role = $this->getById($project_id, $role_id); + + $r1 = $this->db + ->table(ProjectUserRoleModel::TABLE) + ->eq('project_id', $project_id) + ->eq('role', $role['role']) + ->update(array( + 'role' => Role::PROJECT_MEMBER + )); + + $r2 = $this->db + ->table(ProjectGroupRoleModel::TABLE) + ->eq('project_id', $project_id) + ->eq('role', $role['role']) + ->update(array( + 'role' => Role::PROJECT_MEMBER + )); + + $r3 = $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('role_id', $role_id) + ->remove(); + + if ($r1 && $r2 && $r3) { + $this->db->closeTransaction(); + return true; + } + + $this->db->cancelTransaction(); + return false; + } +} diff --git a/app/Model/ProjectRoleRestrictionModel.php b/app/Model/ProjectRoleRestrictionModel.php new file mode 100644 index 00000000..8ccdcf9c --- /dev/null +++ b/app/Model/ProjectRoleRestrictionModel.php @@ -0,0 +1,130 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Class ProjectRoleRestrictionModel + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class ProjectRoleRestrictionModel extends Base +{ + const TABLE = 'project_role_has_restrictions'; + + const RULE_TASK_CREATION = 'task_creation'; + const RULE_TASK_OPEN_CLOSE = 'task_open_close'; + const RULE_TASK_MOVE = 'task_move'; + + /** + * Get rules + * + * @return array + */ + public function getRules() + { + return array( + self::RULE_TASK_CREATION => t('Task creation is not permitted'), + self::RULE_TASK_OPEN_CLOSE => t('Closing or opening a task is not permitted'), + self::RULE_TASK_MOVE => t('Moving a task is not permitted'), + ); + } + + /** + * Get a single restriction + * + * @param integer $project_id + * @param integer $restriction_id + * @return array|null + */ + public function getById($project_id, $restriction_id) + { + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('restriction_id', $restriction_id) + ->findOne(); + } + + /** + * Get restrictions + * + * @param int $project_id + * @return array + */ + public function getAll($project_id) + { + $rules = $this->getRules(); + $restrictions = $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.rule' + ) + ->eq(self::TABLE.'.project_id', $project_id) + ->findAll(); + + foreach ($restrictions as &$restriction) { + $restriction['title'] = $rules[$restriction['rule']]; + } + + return $restrictions; + } + + /** + * Get restrictions + * + * @param int $project_id + * @param string $role + * @return array + */ + public function getAllByRole($project_id, $role) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.restriction_id', + self::TABLE.'.project_id', + self::TABLE.'.role_id', + self::TABLE.'.rule', + 'pr.role' + ) + ->eq(self::TABLE.'.project_id', $project_id) + ->eq('role', $role) + ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id') + ->findAll(); + } + + /** + * Create a new restriction + * + * @param int $project_id + * @param int $role_id + * @param string $rule + * @return bool|int + */ + public function create($project_id, $role_id, $rule) + { + return $this->db->table(self::TABLE) + ->persist(array( + 'project_id' => $project_id, + 'role_id' => $role_id, + 'rule' => $rule, + )); + } + + /** + * Remove a restriction + * + * @param integer $restriction_id + * @return bool + */ + public function remove($restriction_id) + { + return $this->db->table(self::TABLE)->eq('restriction_id', $restriction_id)->remove(); + } +} diff --git a/app/Model/ProjectUserRoleModel.php b/app/Model/ProjectUserRoleModel.php index a0df0cfa..76094431 100644 --- a/app/Model/ProjectUserRoleModel.php +++ b/app/Model/ProjectUserRoleModel.php @@ -166,7 +166,7 @@ class ProjectUserRoleModel extends Base ->join(UserModel::TABLE, 'id', 'user_id') ->eq(UserModel::TABLE.'.is_active', 1) ->eq(self::TABLE.'.project_id', $project_id) - ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) + ->neq(self::TABLE.'.role', Role::PROJECT_VIEWER) ->findAll(); $groupMembers = $this->projectGroupRoleModel->getAssignableUsers($project_id); diff --git a/app/Model/SubtaskModel.php b/app/Model/SubtaskModel.php index a97bddbf..737a933d 100644 --- a/app/Model/SubtaskModel.php +++ b/app/Model/SubtaskModel.php @@ -4,7 +4,6 @@ namespace Kanboard\Model; use PicoDb\Database; use Kanboard\Core\Base; -use Kanboard\Event\SubtaskEvent; /** * Subtask Model @@ -22,25 +21,13 @@ class SubtaskModel extends Base const TABLE = 'subtasks'; /** - * Task "done" status - * - * @var integer - */ - const STATUS_DONE = 2; - - /** - * Task "in progress" status - * - * @var integer - */ - const STATUS_INPROGRESS = 1; - - /** - * Task "todo" status + * Subtask status * * @var integer */ const STATUS_TODO = 0; + const STATUS_INPROGRESS = 1; + const STATUS_DONE = 2; /** * Events @@ -66,7 +53,7 @@ class SubtaskModel extends Base ->join(TaskModel::TABLE, 'id', 'task_id') ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; } - + /** * Get available status * @@ -83,26 +70,6 @@ class SubtaskModel extends Base } /** - * Add subtask status status to the resultset - * - * @access public - * @param array $subtasks Subtasks - * @return array - */ - public function addStatusName(array $subtasks) - { - $status = $this->getStatusList(); - - foreach ($subtasks as &$subtask) { - $subtask['status_name'] = $status[$subtask['status']]; - $subtask['timer_start_date'] = isset($subtask['timer_start_date']) ? $subtask['timer_start_date'] : 0; - $subtask['is_timer_started'] = ! empty($subtask['timer_start_date']); - } - - return $subtasks; - } - - /** * Get the query to fetch subtasks assigned to a user * * @access public @@ -178,35 +145,6 @@ class SubtaskModel extends Base } /** - * Prepare data before insert/update - * - * @access public - * @param array $values Form values - */ - public function prepare(array &$values) - { - $this->helper->model->removeFields($values, array('another_subtask')); - $this->helper->model->resetFields($values, array('time_estimated', 'time_spent')); - } - - /** - * Prepare data before insert - * - * @access public - * @param array $values Form values - */ - public function prepareCreation(array &$values) - { - $this->prepare($values); - - $values['position'] = $this->getLastPosition($values['task_id']) + 1; - $values['status'] = isset($values['status']) ? $values['status'] : self::STATUS_TODO; - $values['time_estimated'] = isset($values['time_estimated']) ? $values['time_estimated'] : 0; - $values['time_spent'] = isset($values['time_spent']) ? $values['time_spent'] : 0; - $values['user_id'] = isset($values['user_id']) ? $values['user_id'] : 0; - } - - /** * Get the position of the last column for a given project * * @access public @@ -235,10 +173,8 @@ class SubtaskModel extends Base $subtask_id = $this->db->table(self::TABLE)->persist($values); if ($subtask_id !== false) { - $this->container['dispatcher']->dispatch( - self::EVENT_CREATE, - new SubtaskEvent(array('id' => $subtask_id) + $values) - ); + $this->subtaskTimeTrackingModel->updateTaskTimeTracking($values['task_id']); + $this->queueManager->push($this->subtaskEventJob->withParams($subtask_id, self::EVENT_CREATE)); } return $subtask_id; @@ -248,124 +184,24 @@ class SubtaskModel extends Base * Update * * @access public - * @param array $values Form values - * @param bool $fire_events If true, will be called an event + * @param array $values + * @param bool $fire_event * @return bool */ - public function update(array $values, $fire_events = true) + public function update(array $values, $fire_event = true) { $this->prepare($values); - $subtask = $this->getById($values['id']); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); - if ($result && $fire_events) { - $event = $subtask; - $event['changes'] = array_diff_assoc($values, $subtask); - $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new SubtaskEvent($event)); - } - - return $result; - } - - /** - * Close all subtasks of a task - * - * @access public - * @param integer $task_id - * @return boolean - */ - public function closeAll($task_id) - { - return $this->db->table(self::TABLE)->eq('task_id', $task_id)->update(array('status' => self::STATUS_DONE)); - } - - /** - * Save subtask position - * - * @access public - * @param integer $task_id - * @param integer $subtask_id - * @param integer $position - * @return boolean - */ - public function changePosition($task_id, $subtask_id, $position) - { - if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('task_id', $task_id)->count()) { - return false; - } - - $subtask_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->neq('id', $subtask_id)->asc('position')->findAllByColumn('id'); - $offset = 1; - $results = array(); + if ($result) { + $this->subtaskTimeTrackingModel->updateTaskTimeTracking($values['task_id']); - foreach ($subtask_ids as $current_subtask_id) { - if ($offset == $position) { - $offset++; + if ($fire_event) { + $this->queueManager->push($this->subtaskEventJob->withParams($values['id'], self::EVENT_UPDATE, $values)); } - - $results[] = $this->db->table(self::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset)); - $offset++; - } - - $results[] = $this->db->table(self::TABLE)->eq('id', $subtask_id)->update(array('position' => $position)); - - return !in_array(false, $results, true); - } - - /** - * Change the status of subtask - * - * @access public - * @param integer $subtask_id - * @return boolean|integer - */ - public function toggleStatus($subtask_id) - { - $subtask = $this->getById($subtask_id); - $status = ($subtask['status'] + 1) % 3; - - $values = array( - 'id' => $subtask['id'], - 'status' => $status, - 'task_id' => $subtask['task_id'], - ); - - if (empty($subtask['user_id']) && $this->userSession->isLogged()) { - $values['user_id'] = $this->userSession->getId(); } - return $this->update($values) ? $status : false; - } - - /** - * Get the subtask in progress for this user - * - * @access public - * @param integer $user_id - * @return array - */ - public function getSubtaskInProgress($user_id) - { - return $this->db->table(self::TABLE) - ->eq('status', self::STATUS_INPROGRESS) - ->eq('user_id', $user_id) - ->findOne(); - } - - /** - * Return true if the user have a subtask in progress - * - * @access public - * @param integer $user_id - * @return boolean - */ - public function hasSubtaskInProgress($user_id) - { - return $this->configModel->get('subtask_restriction') == 1 && - $this->db->table(self::TABLE) - ->eq('status', self::STATUS_INPROGRESS) - ->eq('user_id', $user_id) - ->exists(); + return $result; } /** @@ -377,14 +213,8 @@ class SubtaskModel extends Base */ public function remove($subtask_id) { - $subtask = $this->getById($subtask_id); - $result = $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); - - if ($result) { - $this->container['dispatcher']->dispatch(self::EVENT_DELETE, new SubtaskEvent($subtask)); - } - - return $result; + $this->subtaskEventJob->execute($subtask_id, self::EVENT_DELETE); + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); } /** @@ -416,29 +246,53 @@ class SubtaskModel extends Base } /** - * Convert a subtask to a task + * Prepare data before insert/update * - * @access public - * @param integer $project_id - * @param integer $subtask_id - * @return integer + * @access protected + * @param array $values Form values */ - public function convertToTask($project_id, $subtask_id) + protected function prepare(array &$values) { - $subtask = $this->getById($subtask_id); + $this->helper->model->removeFields($values, array('another_subtask')); + $this->helper->model->resetFields($values, array('time_estimated', 'time_spent')); + $this->hook->reference('model:subtask:modification:prepare', $values); + } - $task_id = $this->taskCreationModel->create(array( - 'project_id' => $project_id, - 'title' => $subtask['title'], - 'time_estimated' => $subtask['time_estimated'], - 'time_spent' => $subtask['time_spent'], - 'owner_id' => $subtask['user_id'], - )); + /** + * Prepare data before insert + * + * @access protected + * @param array $values Form values + */ + protected function prepareCreation(array &$values) + { + $this->prepare($values); - if ($task_id !== false) { - $this->remove($subtask_id); + $values['position'] = $this->getLastPosition($values['task_id']) + 1; + $values['status'] = isset($values['status']) ? $values['status'] : self::STATUS_TODO; + $values['time_estimated'] = isset($values['time_estimated']) ? $values['time_estimated'] : 0; + $values['time_spent'] = isset($values['time_spent']) ? $values['time_spent'] : 0; + $values['user_id'] = isset($values['user_id']) ? $values['user_id'] : 0; + $this->hook->reference('model:subtask:creation:prepare', $values); + } + + /** + * Add subtask status status to the resultset + * + * @access public + * @param array $subtasks Subtasks + * @return array + */ + public function addStatusName(array $subtasks) + { + $status = $this->getStatusList(); + + foreach ($subtasks as &$subtask) { + $subtask['status_name'] = $status[$subtask['status']]; + $subtask['timer_start_date'] = isset($subtask['timer_start_date']) ? $subtask['timer_start_date'] : 0; + $subtask['is_timer_started'] = ! empty($subtask['timer_start_date']); } - return $task_id; + return $subtasks; } } diff --git a/app/Model/SubtaskPositionModel.php b/app/Model/SubtaskPositionModel.php new file mode 100644 index 00000000..3c26465d --- /dev/null +++ b/app/Model/SubtaskPositionModel.php @@ -0,0 +1,47 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Class SubtaskPositionModel + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class SubtaskPositionModel extends Base +{ + /** + * Change subtask position + * + * @access public + * @param integer $task_id + * @param integer $subtask_id + * @param integer $position + * @return boolean + */ + public function changePosition($task_id, $subtask_id, $position) + { + if ($position < 1 || $position > $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->count()) { + return false; + } + + $subtask_ids = $this->db->table(SubtaskModel::TABLE)->eq('task_id', $task_id)->neq('id', $subtask_id)->asc('position')->findAllByColumn('id'); + $offset = 1; + $results = array(); + + foreach ($subtask_ids as $current_subtask_id) { + if ($offset == $position) { + $offset++; + } + + $results[] = $this->db->table(SubtaskModel::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset)); + $offset++; + } + + $results[] = $this->db->table(SubtaskModel::TABLE)->eq('id', $subtask_id)->update(array('position' => $position)); + + return !in_array(false, $results, true); + } +} diff --git a/app/Model/SubtaskStatusModel.php b/app/Model/SubtaskStatusModel.php new file mode 100644 index 00000000..c99d6055 --- /dev/null +++ b/app/Model/SubtaskStatusModel.php @@ -0,0 +1,88 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Class SubtaskStatusModel + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class SubtaskStatusModel extends Base +{ + /** + * Get the subtask in progress for this user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getSubtaskInProgress($user_id) + { + return $this->db->table(SubtaskModel::TABLE) + ->eq('status', SubtaskModel::STATUS_INPROGRESS) + ->eq('user_id', $user_id) + ->findOne(); + } + + /** + * Return true if the user have a subtask in progress + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function hasSubtaskInProgress($user_id) + { + return $this->configModel->get('subtask_restriction') == 1 && + $this->db->table(SubtaskModel::TABLE) + ->eq('status', SubtaskModel::STATUS_INPROGRESS) + ->eq('user_id', $user_id) + ->exists(); + } + + /** + * Change the status of subtask + * + * @access public + * @param integer $subtask_id + * @return boolean|integer + */ + public function toggleStatus($subtask_id) + { + $subtask = $this->subtaskModel->getById($subtask_id); + $status = ($subtask['status'] + 1) % 3; + + $values = array( + 'id' => $subtask['id'], + 'status' => $status, + 'task_id' => $subtask['task_id'], + ); + + if (empty($subtask['user_id']) && $this->userSession->isLogged()) { + $values['user_id'] = $this->userSession->getId(); + $subtask['user_id'] = $values['user_id']; + } + + $this->subtaskTimeTrackingModel->toggleTimer($subtask_id, $subtask['user_id'], $status); + + return $this->subtaskModel->update($values) ? $status : false; + } + + /** + * Close all subtasks of a task + * + * @access public + * @param integer $task_id + * @return boolean + */ + public function closeAll($task_id) + { + return $this->db + ->table(SubtaskModel::TABLE) + ->eq('task_id', $task_id) + ->update(array('status' => SubtaskModel::STATUS_DONE)); + } +} diff --git a/app/Model/SubtaskTaskConversionModel.php b/app/Model/SubtaskTaskConversionModel.php new file mode 100644 index 00000000..8bf83d76 --- /dev/null +++ b/app/Model/SubtaskTaskConversionModel.php @@ -0,0 +1,41 @@ +<?php + +namespace Kanboard\Model; + +use Kanboard\Core\Base; + +/** + * Class SubtaskTaskConversionModel + * + * @package Kanboard\Model + * @author Frederic Guillot + */ +class SubtaskTaskConversionModel extends Base +{ + /** + * Convert a subtask to a task + * + * @access public + * @param integer $project_id + * @param integer $subtask_id + * @return integer + */ + public function convertToTask($project_id, $subtask_id) + { + $subtask = $this->subtaskModel->getById($subtask_id); + + $task_id = $this->taskCreationModel->create(array( + 'project_id' => $project_id, + 'title' => $subtask['title'], + 'time_estimated' => $subtask['time_estimated'], + 'time_spent' => $subtask['time_spent'], + 'owner_id' => $subtask['user_id'], + )); + + if ($task_id !== false) { + $this->subtaskModel->remove($subtask_id); + } + + return $task_id; + } +} diff --git a/app/Model/SubtaskTimeTrackingModel.php b/app/Model/SubtaskTimeTrackingModel.php index 062e594a..3b1b97e4 100644 --- a/app/Model/SubtaskTimeTrackingModel.php +++ b/app/Model/SubtaskTimeTrackingModel.php @@ -160,6 +160,28 @@ class SubtaskTimeTrackingModel extends Base } /** + * Start or stop timer according to subtask status + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @param integer $status + * @return boolean + */ + public function toggleTimer($subtask_id, $user_id, $status) + { + if ($this->configModel->get('subtask_time_tracking') == 1) { + if ($status == SubtaskModel::STATUS_INPROGRESS) { + return $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $user_id); + } elseif ($status == SubtaskModel::STATUS_DONE) { + return $this->subtaskTimeTrackingModel->logEndTime($subtask_id, $user_id); + } + } + + return false; + } + + /** * Log start time * * @access public @@ -252,7 +274,6 @@ class SubtaskTimeTrackingModel extends Base { $subtask = $this->subtaskModel->getById($subtask_id); - // Fire the event subtask.update return $this->subtaskModel->update(array( 'id' => $subtask['id'], 'time_spent' => $subtask['time_spent'] + $time_spent, diff --git a/app/Model/TaskCreationModel.php b/app/Model/TaskCreationModel.php index cd70a028..bd95c1a4 100644 --- a/app/Model/TaskCreationModel.php +++ b/app/Model/TaskCreationModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Creation @@ -42,7 +41,10 @@ class TaskCreationModel extends Base $this->taskTagModel->save($values['project_id'], $task_id, $tags); } - $this->fireEvents($task_id, $values); + $this->queueManager->push($this->taskEventJob->withParams( + $task_id, + array(TaskModel::EVENT_CREATE_UPDATE, TaskModel::EVENT_CREATE) + )); } return (int) $task_id; @@ -51,15 +53,15 @@ class TaskCreationModel extends Base /** * Prepare data * - * @access public + * @access protected * @param array $values Form values */ - public function prepare(array &$values) + protected function prepare(array &$values) { $values = $this->dateParser->convert($values, array('date_due')); $values = $this->dateParser->convert($values, array('date_started'), true); - $this->helper->model->removeFields($values, array('another_task')); + $this->helper->model->removeFields($values, array('another_task', 'duplicate_multiple_projects')); $this->helper->model->resetFields($values, array('creator_id', 'owner_id', 'swimlane_id', 'date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); if (empty($values['column_id'])) { @@ -83,27 +85,7 @@ class TaskCreationModel extends Base $values['date_modification'] = $values['date_creation']; $values['date_moved'] = $values['date_creation']; $values['position'] = $this->taskFinderModel->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1; - } - - /** - * Fire events - * - * @access private - * @param integer $task_id Task id - * @param array $values Form values - */ - private function fireEvents($task_id, array $values) - { - $event = new TaskEvent(array('task_id' => $task_id) + $values); - - $this->logger->debug('Event fired: '.TaskModel::EVENT_CREATE_UPDATE); - $this->logger->debug('Event fired: '.TaskModel::EVENT_CREATE); - $this->dispatcher->dispatch(TaskModel::EVENT_CREATE_UPDATE, $event); - $this->dispatcher->dispatch(TaskModel::EVENT_CREATE, $event); - - if (! empty($values['description'])) { - $this->userMentionModel->fireEvents($values['description'], TaskModel::EVENT_USER_MENTION, $event); - } + $this->hook->reference('model:task:creation:prepare', $values); } } diff --git a/app/Model/TaskFileModel.php b/app/Model/TaskFileModel.php index 7603019a..0163da28 100644 --- a/app/Model/TaskFileModel.php +++ b/app/Model/TaskFileModel.php @@ -61,18 +61,6 @@ class TaskFileModel extends FileModel } /** - * Get event name - * - * @abstract - * @access protected - * @return string - */ - protected function getEventName() - { - return self::EVENT_CREATE; - } - - /** * Get projectId from fileId * * @access public @@ -101,4 +89,15 @@ class TaskFileModel extends FileModel $original_filename = e('Screenshot taken %s', $this->helper->dt->datetime(time())).'.png'; return $this->uploadContent($task_id, $original_filename, $blob); } + + /** + * Fire file creation event + * + * @access protected + * @param integer $file_id + */ + protected function fireCreationEvent($file_id) + { + $this->queueManager->push($this->taskFileEventJob->withParams($file_id, self::EVENT_CREATE)); + } } diff --git a/app/Model/TaskFinderModel.php b/app/Model/TaskFinderModel.php index 7268052c..3185afb7 100644 --- a/app/Model/TaskFinderModel.php +++ b/app/Model/TaskFinderModel.php @@ -2,7 +2,6 @@ namespace Kanboard\Model; -use PDO; use Kanboard\Core\Base; /** @@ -63,19 +62,20 @@ class TaskFinderModel extends Base return $this->db ->table(TaskModel::TABLE) ->columns( - 'tasks.id', - 'tasks.title', - 'tasks.date_due', - 'tasks.date_creation', - 'tasks.project_id', - 'tasks.color_id', - 'tasks.priority', - 'tasks.time_spent', - 'tasks.time_estimated', - 'tasks.is_active', - 'tasks.creator_id', - 'projects.name AS project_name', - 'columns.title AS column_title' + TaskModel::TABLE.'.id', + TaskModel::TABLE.'.title', + TaskModel::TABLE.'.date_due', + TaskModel::TABLE.'.date_creation', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.column_id', + TaskModel::TABLE.'.color_id', + TaskModel::TABLE.'.priority', + TaskModel::TABLE.'.time_spent', + TaskModel::TABLE.'.time_estimated', + TaskModel::TABLE.'.is_active', + TaskModel::TABLE.'.creator_id', + ProjectModel::TABLE.'.name AS project_name', + ColumnModel::TABLE.'.title AS column_title' ) ->join(ProjectModel::TABLE, 'id', 'project_id') ->join(ColumnModel::TABLE, 'id', 'column_id') @@ -103,36 +103,36 @@ class TaskFinderModel extends Base '(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', - 'tasks.id', - 'tasks.reference', - 'tasks.title', - 'tasks.description', - 'tasks.date_creation', - 'tasks.date_modification', - 'tasks.date_completed', - 'tasks.date_started', - 'tasks.date_due', - 'tasks.color_id', - 'tasks.project_id', - 'tasks.column_id', - 'tasks.swimlane_id', - 'tasks.owner_id', - 'tasks.creator_id', - 'tasks.position', - 'tasks.is_active', - 'tasks.score', - 'tasks.category_id', - 'tasks.priority', - 'tasks.date_moved', - 'tasks.recurrence_status', - 'tasks.recurrence_trigger', - 'tasks.recurrence_factor', - 'tasks.recurrence_timeframe', - 'tasks.recurrence_basedate', - 'tasks.recurrence_parent', - 'tasks.recurrence_child', - 'tasks.time_estimated', - 'tasks.time_spent', + 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', @@ -298,59 +298,30 @@ class TaskFinderModel extends Base */ public function getDetails($task_id) { - $sql = ' - SELECT - tasks.id, - tasks.reference, - tasks.title, - tasks.description, - tasks.date_creation, - tasks.date_completed, - tasks.date_modification, - tasks.date_due, - tasks.date_started, - tasks.time_estimated, - tasks.time_spent, - tasks.color_id, - tasks.project_id, - tasks.column_id, - tasks.owner_id, - tasks.creator_id, - tasks.position, - tasks.is_active, - tasks.score, - tasks.category_id, - tasks.priority, - tasks.swimlane_id, - tasks.date_moved, - tasks.recurrence_status, - tasks.recurrence_trigger, - tasks.recurrence_factor, - tasks.recurrence_timeframe, - tasks.recurrence_basedate, - tasks.recurrence_parent, - tasks.recurrence_child, - project_has_categories.name AS category_name, - swimlanes.name AS swimlane_name, - projects.name AS project_name, - projects.default_swimlane, - columns.title AS column_title, - users.username AS assignee_username, - users.name AS assignee_name, - creators.username AS creator_username, - creators.name AS creator_name - FROM tasks - LEFT JOIN users ON users.id = tasks.owner_id - LEFT JOIN users AS creators ON creators.id = tasks.creator_id - LEFT JOIN project_has_categories ON project_has_categories.id = tasks.category_id - LEFT JOIN projects ON projects.id = tasks.project_id - LEFT JOIN columns ON columns.id = tasks.column_id - LEFT JOIN swimlanes ON swimlanes.id = tasks.swimlane_id - WHERE tasks.id = ? - '; - - $rq = $this->db->execute($sql, array($task_id)); - return $rq->fetch(PDO::FETCH_ASSOC); + 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', + ProjectModel::TABLE.'.default_swimlane', + 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', + ProjectModel::TABLE.'.default_swimlane' + ) + ->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) + ->eq(TaskModel::TABLE.'.id', $task_id) + ->findOne(); } /** diff --git a/app/Model/TaskLinkModel.php b/app/Model/TaskLinkModel.php index 09978eae..e8d3c5df 100644 --- a/app/Model/TaskLinkModel.php +++ b/app/Model/TaskLinkModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskLinkEvent; /** * TaskLink model @@ -26,7 +25,8 @@ class TaskLinkModel extends Base * * @var string */ - const EVENT_CREATE_UPDATE = 'tasklink.create_update'; + const EVENT_CREATE_UPDATE = 'task_internal_link.create_update'; + const EVENT_DELETE = 'task_internal_link.delete'; /** * Get projectId from $task_link_id @@ -53,7 +53,19 @@ class TaskLinkModel extends Base */ public function getById($task_link_id) { - return $this->db->table(self::TABLE)->eq('id', $task_link_id)->findOne(); + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.opposite_task_id', + self::TABLE.'.task_id', + self::TABLE.'.link_id', + LinkModel::TABLE.'.label', + LinkModel::TABLE.'.opposite_id AS opposite_link_id' + ) + ->eq(self::TABLE.'.id', $task_link_id) + ->join(LinkModel::TABLE, 'id', 'link_id') + ->findOne(); } /** @@ -140,62 +152,31 @@ class TaskLinkModel extends Base } /** - * Publish events - * - * @access private - * @param array $events - */ - private function fireEvents(array $events) - { - foreach ($events as $event) { - $event['project_id'] = $this->taskFinderModel->getProjectId($event['task_id']); - $this->container['dispatcher']->dispatch(self::EVENT_CREATE_UPDATE, new TaskLinkEvent($event)); - } - } - - /** * Create a new link * * @access public * @param integer $task_id Task id * @param integer $opposite_task_id Opposite task id * @param integer $link_id Link id - * @return integer Task link id + * @return integer|boolean */ public function create($task_id, $opposite_task_id, $link_id) { - $events = array(); $this->db->startTransaction(); - // Get opposite link $opposite_link_id = $this->linkModel->getOppositeLinkId($link_id); + $task_link_id1 = $this->createTaskLink($task_id, $opposite_task_id, $link_id); + $task_link_id2 = $this->createTaskLink($opposite_task_id, $task_id, $opposite_link_id); - $values = array( - 'task_id' => $task_id, - 'opposite_task_id' => $opposite_task_id, - 'link_id' => $link_id, - ); - - // Create the original task link - $this->db->table(self::TABLE)->insert($values); - $task_link_id = $this->db->getLastId(); - $events[] = $values; - - // Create the opposite task link - $values = array( - 'task_id' => $opposite_task_id, - 'opposite_task_id' => $task_id, - 'link_id' => $opposite_link_id, - ); - - $this->db->table(self::TABLE)->insert($values); - $events[] = $values; + if ($task_link_id1 === false || $task_link_id2 === false) { + $this->db->cancelTransaction(); + return false; + } $this->db->closeTransaction(); + $this->fireEvents(array($task_link_id1, $task_link_id2), self::EVENT_CREATE_UPDATE); - $this->fireEvents($events); - - return (int) $task_link_id; + return $task_link_id1; } /** @@ -210,46 +191,24 @@ class TaskLinkModel extends Base */ public function update($task_link_id, $task_id, $opposite_task_id, $link_id) { - $events = array(); $this->db->startTransaction(); - // Get original task link $task_link = $this->getById($task_link_id); - - // Find opposite task link $opposite_task_link = $this->getOppositeTaskLink($task_link); - - // Get opposite link $opposite_link_id = $this->linkModel->getOppositeLinkId($link_id); - // Update the original task link - $values = array( - 'task_id' => $task_id, - 'opposite_task_id' => $opposite_task_id, - 'link_id' => $link_id, - ); - - $rs1 = $this->db->table(self::TABLE)->eq('id', $task_link_id)->update($values); - $events[] = $values; + $result1 = $this->updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id); + $result2 = $this->updateTaskLink($opposite_task_link['id'], $opposite_task_id, $task_id, $opposite_link_id); - // Update the opposite link - $values = array( - 'task_id' => $opposite_task_id, - 'opposite_task_id' => $task_id, - 'link_id' => $opposite_link_id, - ); - - $rs2 = $this->db->table(self::TABLE)->eq('id', $opposite_task_link['id'])->update($values); - $events[] = $values; + if ($result1 === false || $result2 === false) { + $this->db->cancelTransaction(); + return false; + } $this->db->closeTransaction(); + $this->fireEvents(array($task_link_id, $opposite_task_link['id']), self::EVENT_CREATE_UPDATE); - if ($rs1 && $rs2) { - $this->fireEvents($events); - return true; - } - - return false; + return true; } /** @@ -261,21 +220,83 @@ class TaskLinkModel extends Base */ public function remove($task_link_id) { + $this->taskLinkEventJob->execute($task_link_id, self::EVENT_DELETE); + $this->db->startTransaction(); $link = $this->getById($task_link_id); $link_id = $this->linkModel->getOppositeLinkId($link['link_id']); - $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove(); + $result1 = $this->db + ->table(self::TABLE) + ->eq('id', $task_link_id) + ->remove(); - $this->db + $result2 = $this->db ->table(self::TABLE) ->eq('opposite_task_id', $link['task_id']) ->eq('task_id', $link['opposite_task_id']) - ->eq('link_id', $link_id)->remove(); + ->eq('link_id', $link_id) + ->remove(); + + if ($result1 === false || $result2 === false) { + $this->db->cancelTransaction(); + return false; + } $this->db->closeTransaction(); return true; } + + /** + * Publish events + * + * @access protected + * @param integer[] $task_link_ids + * @param string $eventName + */ + protected function fireEvents(array $task_link_ids, $eventName) + { + foreach ($task_link_ids as $task_link_id) { + $this->queueManager->push($this->taskLinkEventJob->withParams($task_link_id, $eventName)); + } + } + + /** + * Create task link + * + * @access protected + * @param integer $task_id + * @param integer $opposite_task_id + * @param integer $link_id + * @return integer|boolean + */ + protected function createTaskLink($task_id, $opposite_task_id, $link_id) + { + return $this->db->table(self::TABLE)->persist(array( + 'task_id' => $task_id, + 'opposite_task_id' => $opposite_task_id, + 'link_id' => $link_id, + )); + } + + /** + * Update task link + * + * @access protected + * @param integer $task_link_id + * @param integer $task_id + * @param integer $opposite_task_id + * @param integer $link_id + * @return boolean + */ + protected function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id) + { + return $this->db->table(self::TABLE)->eq('id', $task_link_id)->update(array( + 'task_id' => $task_id, + 'opposite_task_id' => $opposite_task_id, + 'link_id' => $link_id, + )); + } } diff --git a/app/Model/TaskModificationModel.php b/app/Model/TaskModificationModel.php index be5f53c8..0ae29293 100644 --- a/app/Model/TaskModificationModel.php +++ b/app/Model/TaskModificationModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Modification @@ -23,14 +22,14 @@ class TaskModificationModel extends Base */ public function update(array $values, $fire_events = true) { - $original_task = $this->taskFinderModel->getById($values['id']); + $task = $this->taskFinderModel->getById($values['id']); - $this->updateTags($values, $original_task); + $this->updateTags($values, $task); $this->prepare($values); - $result = $this->db->table(TaskModel::TABLE)->eq('id', $original_task['id'])->update($values); + $result = $this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values); if ($fire_events && $result) { - $this->fireEvents($original_task, $values); + $this->fireEvents($task, $values); } return $result; @@ -39,43 +38,56 @@ class TaskModificationModel extends Base /** * Fire events * - * @access public - * @param array $task - * @param array $new_values + * @access protected + * @param array $task + * @param array $changes */ - public function fireEvents(array $task, array $new_values) + protected function fireEvents(array $task, array $changes) { $events = array(); - $event_data = array_merge($task, $new_values, array('task_id' => $task['id'])); - // Values changed - $event_data['changes'] = array_diff_assoc($new_values, $task); - unset($event_data['changes']['date_modification']); - - if ($this->isFieldModified('owner_id', $event_data['changes'])) { + if ($this->isAssigneeChanged($task, $changes)) { $events[] = TaskModel::EVENT_ASSIGNEE_CHANGE; - } elseif (! empty($event_data['changes'])) { + } elseif ($this->isModified($task, $changes)) { $events[] = TaskModel::EVENT_CREATE_UPDATE; $events[] = TaskModel::EVENT_UPDATE; } - foreach ($events as $event) { - $this->logger->debug('Event fired: '.$event); - $this->dispatcher->dispatch($event, new TaskEvent($event_data)); + if (! empty($events)) { + $this->queueManager->push($this->taskEventJob + ->withParams($task['id'], $events, $changes, array(), $task) + ); } } /** + * Return true if the task have been modified + * + * @access protected + * @param array $task + * @param array $changes + * @return bool + */ + protected function isModified(array $task, array $changes) + { + $diff = array_diff_assoc($changes, $task); + unset($diff['date_modification']); + return count($diff) > 0; + } + + /** * Return true if the field is the only modified value * - * @access public - * @param string $field - * @param array $changes - * @return boolean + * @access protected + * @param array $task + * @param array $changes + * @return bool */ - public function isFieldModified($field, array $changes) + protected function isAssigneeChanged(array $task, array $changes) { - return isset($changes[$field]) && count($changes) === 1; + $diff = array_diff_assoc($changes, $task); + unset($diff['date_modification']); + return isset($changes['owner_id']) && $task['owner_id'] != $changes['owner_id'] && count($diff) === 1; } /** @@ -89,11 +101,13 @@ class TaskModificationModel extends Base $values = $this->dateParser->convert($values, array('date_due')); $values = $this->dateParser->convert($values, array('date_started'), true); - $this->helper->model->removeFields($values, array('another_task', 'id')); + $this->helper->model->removeFields($values, array('id')); $this->helper->model->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); $this->helper->model->convertIntegerFields($values, array('priority', 'is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate')); $values['date_modification'] = time(); + + $this->hook->reference('model:task:modification:prepare', $values); } /** diff --git a/app/Model/TaskPositionModel.php b/app/Model/TaskPositionModel.php index 9fdb8f7d..aeb7edde 100644 --- a/app/Model/TaskPositionModel.php +++ b/app/Model/TaskPositionModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Position @@ -17,15 +16,16 @@ class TaskPositionModel extends Base * Move a task to another column or to another position * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param integer $column_id Column id - * @param integer $position Position (must be >= 1) - * @param integer $swimlane_id Swimlane id - * @param boolean $fire_events Fire events - * @return boolean + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $column_id Column id + * @param integer $position Position (must be >= 1) + * @param integer $swimlane_id Swimlane id + * @param boolean $fire_events Fire events + * @param bool $onlyOpen Do not move closed tasks + * @return bool */ - public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true) + public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true, $onlyOpen = true) { if ($position < 1) { return false; @@ -33,7 +33,7 @@ class TaskPositionModel extends Base $task = $this->taskFinderModel->getById($task_id); - if ($task['is_active'] == TaskModel::STATUS_CLOSED) { + if ($onlyOpen && $task['is_active'] == TaskModel::STATUS_CLOSED) { return true; } @@ -72,9 +72,10 @@ class TaskPositionModel extends Base $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); + $r3 = $this->saveTaskTimestamps($task_id); $this->db->closeTransaction(); - return $r1 && $r2; + return $r1 && $r2 && $r3; } /** @@ -94,9 +95,10 @@ class TaskPositionModel extends Base $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); + $r3 = $this->saveTaskTimestamps($task_id); $this->db->closeTransaction(); - return $r1 && $r2; + return $r1 && $r2 && $r3; } /** @@ -167,6 +169,18 @@ class TaskPositionModel extends Base return false; } + return true; + } + + /** + * Update task timestamps + * + * @access private + * @param integer $task_id + * @return bool + */ + private function saveTaskTimestamps($task_id) + { $now = time(); return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array( @@ -212,8 +226,7 @@ class TaskPositionModel extends Base */ private function fireEvents(array $task, $new_column_id, $new_position, $new_swimlane_id) { - $event_data = array( - 'task_id' => $task['id'], + $changes = array( 'project_id' => $task['project_id'], 'position' => $new_position, 'column_id' => $new_column_id, @@ -226,14 +239,26 @@ class TaskPositionModel extends Base ); if ($task['swimlane_id'] != $new_swimlane_id) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_SWIMLANE); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_SWIMLANE), + $changes, + $changes + )); } elseif ($task['column_id'] != $new_column_id) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_COLUMN); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_COLUMN), + $changes, + $changes + )); } elseif ($task['position'] != $new_position) { - $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_POSITION); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_POSITION, new TaskEvent($event_data)); + $this->queueManager->push($this->taskEventJob->withParams( + $task['id'], + array(TaskModel::EVENT_MOVE_POSITION), + $changes, + $changes + )); } } } diff --git a/app/Model/TaskProjectMoveModel.php b/app/Model/TaskProjectMoveModel.php index eda23c0b..ae3ae084 100644 --- a/app/Model/TaskProjectMoveModel.php +++ b/app/Model/TaskProjectMoveModel.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use Kanboard\Event\TaskEvent; - /** * Task Project Move * @@ -32,9 +30,8 @@ class TaskProjectMoveModel extends TaskDuplicationModel $this->checkDestinationProjectValues($values); $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id); - if ($this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values)) { - $event = new TaskEvent(array_merge($task, $values, array('task_id' => $task['id']))); - $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_PROJECT, $event); + 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; diff --git a/app/Model/TaskStatusModel.php b/app/Model/TaskStatusModel.php index 4d573f0e..dc114698 100644 --- a/app/Model/TaskStatusModel.php +++ b/app/Model/TaskStatusModel.php @@ -3,7 +3,6 @@ namespace Kanboard\Model; use Kanboard\Core\Base; -use Kanboard\Event\TaskEvent; /** * Task Status @@ -46,7 +45,7 @@ class TaskStatusModel extends Base */ public function close($task_id) { - $this->subtaskModel->closeAll($task_id); + $this->subtaskStatusModel->closeAll($task_id); return $this->changeStatus($task_id, TaskModel::STATUS_CLOSED, time(), TaskModel::EVENT_CLOSE); } @@ -101,10 +100,10 @@ class TaskStatusModel extends Base * @param integer $task_id Task id * @param integer $status Task status * @param integer $date_completed Timestamp - * @param string $event Event name + * @param string $event_name Event name * @return boolean */ - private function changeStatus($task_id, $status, $date_completed, $event) + private function changeStatus($task_id, $status, $date_completed, $event_name) { if (! $this->taskFinderModel->exists($task_id)) { return false; @@ -120,8 +119,7 @@ class TaskStatusModel extends Base )); if ($result) { - $this->logger->debug('Event fired: '.$event); - $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinderModel->getById($task_id))); + $this->queueManager->push($this->taskEventJob->withParams($task_id, array($event_name))); } return $result; diff --git a/app/Model/UserMentionModel.php b/app/Model/UserMentionModel.php deleted file mode 100644 index cdb9949e..00000000 --- a/app/Model/UserMentionModel.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -namespace Kanboard\Model; - -use Kanboard\Core\Base; -use Kanboard\Event\GenericEvent; - -/** - * User Mention - * - * @package Kanboard\Model - * @author Frederic Guillot - */ -class UserMentionModel 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(UserModel::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->taskFinderModel->getProjectId($event['task_id']); - } - - $users = $this->getMentionedUsers($content); - - foreach ($users as $user) { - if ($this->projectPermissionModel->isMember($event['project_id'], $user['id'])) { - $event['mention'] = $user; - $this->dispatcher->dispatch($eventName, $event); - } - } - } -} diff --git a/app/Model/UserMetadataModel.php b/app/Model/UserMetadataModel.php index e931d3ba..42fe4c6d 100644 --- a/app/Model/UserMetadataModel.php +++ b/app/Model/UserMetadataModel.php @@ -10,6 +10,9 @@ namespace Kanboard\Model; */ class UserMetadataModel extends MetadataModel { + const KEY_COMMENT_SORTING_DIRECTION = 'comment.sorting.direction'; + const KEY_BOARD_COLLAPSED = 'board.collapsed.'; + /** * Get the table * diff --git a/app/Model/UserModel.php b/app/Model/UserModel.php index f7a051c5..56b1a960 100644 --- a/app/Model/UserModel.php +++ b/app/Model/UserModel.php @@ -65,17 +65,6 @@ class UserModel extends Base } /** - * Return the full name - * - * @param array $user User properties - * @return string - */ - public function getFullname(array $user) - { - return $user['name'] ?: $user['username']; - } - - /** * Return true is the given user id is administrator * * @access public @@ -230,7 +219,7 @@ class UserModel extends Base $result = array(); foreach ($users as $user) { - $result[$user['id']] = $this->getFullname($user); + $result[$user['id']] = $this->helper->user->getFullname($user); } asort($result); |
