diff options
Diffstat (limited to 'app/Model')
-rw-r--r-- | app/Model/Board.php | 23 | ||||
-rw-r--r-- | app/Model/Swimlane.php | 474 | ||||
-rw-r--r-- | app/Model/Task.php | 1 | ||||
-rw-r--r-- | app/Model/TaskCreation.php | 2 | ||||
-rw-r--r-- | app/Model/TaskDuplication.php | 16 | ||||
-rw-r--r-- | app/Model/TaskFinder.php | 10 | ||||
-rw-r--r-- | app/Model/TaskPosition.php | 67 | ||||
-rw-r--r-- | app/Model/TaskValidator.php | 1 |
8 files changed, 563 insertions, 31 deletions
diff --git a/app/Model/Board.php b/app/Model/Board.php index 9ba2e066..8208b99d 100644 --- a/app/Model/Board.php +++ b/app/Model/Board.php @@ -227,29 +227,30 @@ class Board extends Base } /** - * Get all columns and tasks for a given project + * Get all tasks sorted by columns and swimlanes * * @access public * @param integer $project_id Project id * @return array */ - public function get($project_id) + public function getBoard($project_id) { + $swimlanes = $this->swimlane->getSwimlanes($project_id); $columns = $this->getColumns($project_id); - $tasks = $this->taskFinder->getTasksOnBoard($project_id); + $nb_columns = count($columns); - foreach ($columns as &$column) { + foreach ($swimlanes as &$swimlane) { - $column['tasks'] = array(); - - foreach ($tasks as &$task) { - if ($task['column_id'] == $column['id']) { - $column['tasks'][] = $task; - } + foreach ($columns as &$column) { + $column['tasks'] = $this->taskFinder->getTasksByColumnAndSwimlane($project_id, $column['id'], $swimlane['id']); + $column['nb_tasks'] = count($column['tasks']); } + + $swimlane['columns'] = $columns; + $swimlane['nb_columns'] = $nb_columns; } - return $columns; + return $swimlanes; } /** diff --git a/app/Model/Swimlane.php b/app/Model/Swimlane.php new file mode 100644 index 00000000..ffcfb474 --- /dev/null +++ b/app/Model/Swimlane.php @@ -0,0 +1,474 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Swimlanes + * + * @package model + * @author Frederic Guillot + */ +class Swimlane extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'swimlanes'; + + /** + * Value for active swimlanes + * + * @var integer + */ + const ACTIVE = 1; + + /** + * Value for inactive swimlanes + * + * @var integer + */ + const INACTIVE = 0; + + /** + * Get a swimlane by the id + * + * @access public + * @param integer $swimlane_id Swimlane id + * @return array + */ + public function getById($swimlane_id) + { + return $this->db->table(self::TABLE)->eq('id', $swimlane_id)->findOne(); + } + + /** + * Get the swimlane name by the id + * + * @access public + * @param integer $swimlane_id Swimlane id + * @return string + */ + public function getNameById($swimlane_id) + { + return $this->db->table(self::TABLE)->eq('id', $swimlane_id)->findOneColumn('name') ?: ''; + } + + /** + * Get a swimlane id by the project and the name + * + * @access public + * @param integer $project_id Project id + * @param string $name Name + * @return integer + */ + public function getIdByName($project_id, $name) + { + return (int) $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('name', $name) + ->findOneColumn('id'); + } + + /** + * Get default swimlane properties + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getDefault($project_id) + { + return $this->db->table(Project::TABLE) + ->eq('id', $project_id) + ->columns('id', 'default_swimlane', 'show_default_swimlane') + ->findOne(); + } + + /** + * Get all swimlanes for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAll($project_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->orderBy('position', 'asc') + ->findAll(); + } + + /** + * Get the list of swimlanes by status + * + * @access public + * @param integer $project_id Project id + * @param integer $status Status + * @return array + */ + public function getAllByStatus($project_id, $status = self::ACTIVE) + { + $query = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', $status); + + if ($status == self::ACTIVE) { + $query->asc('position'); + } + else { + $query->asc('name'); + } + + return $query->findAll(); + } + + /** + * Get active swimlanes + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getSwimlanes($project_id) + { + $swimlanes = $this->db->table(self::TABLE) + ->columns('id', 'name') + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->orderBy('position', 'asc') + ->findAll(); + + $default_swimlane = $this->db->table(Project::TABLE) + ->eq('id', $project_id) + ->eq('show_default_swimlane', 1) + ->findOneColumn('default_swimlane'); + + if ($default_swimlane) { + array_unshift($swimlanes, array('id' => 0, 'name' => $default_swimlane)); + } + + return $swimlanes; + } + + /** + * Add a new swimlane + * + * @access public + * @param integer $project_id + * @param string $name + * @return bool + */ + public function create($project_id, $name) + { + return $this->persist(self::TABLE, array( + 'project_id' => $project_id, + 'name' => $name, + 'position' => $this->getLastPosition($project_id), + )); + } + + /** + * Rename a swimlane + * + * @access public + * @param integer $swimlane_id Swimlane id + * @param string $name Swimlane name + * @return bool + */ + public function rename($swimlane_id, $name) + { + return $this->db->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array('name' => $name)); + } + + /** + * Update the default swimlane + * + * @access public + * @param array $values Form values + * @return bool + */ + public function updateDefault(array $values) + { + return $this->db + ->table(Project::TABLE) + ->eq('id', $values['id']) + ->update(array( + 'default_swimlane' => $values['default_swimlane'], + 'show_default_swimlane' => $values['show_default_swimlane'], + )); + } + + /** + * Get the last position of a swimlane + * + * @access public + * @param integer $project_id + * @return bool + */ + public function getLastPosition($project_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->count() + 1; + } + + /** + * Disable a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function disable($project_id, $swimlane_id) + { + $result = $this->db + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::INACTIVE, + 'position' => 0, + )); + + if ($result) { + // Re-order positions + $this->updatePositions($project_id); + } + + return $result; + } + + /** + * Enable a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function enable($project_id, $swimlane_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::ACTIVE, + 'position' => $this->getLastPosition($project_id), + )); + } + + /** + * Remove a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function remove($project_id, $swimlane_id) + { + $this->db->startTransaction(); + + // Tasks should not be assigned anymore to this swimlane + $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->update(array('swimlane_id' => 0)); + + if (! $this->db->table(self::TABLE)->eq('id', $swimlane_id)->remove()) { + $this->db->cancelTransaction(); + return false; + } + + // Re-order positions + $this->updatePositions($project_id); + + $this->db->closeTransaction(); + + return true; + } + + /** + * Update swimlane positions after disabling or removing a swimlane + * + * @access public + * @param integer $project_id Project id + * @return boolean + */ + public function updatePositions($project_id) + { + $position = 0; + $swimlanes = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->asc('position') + ->findAllByColumn('id'); + + if (! $swimlanes) { + return false; + } + + foreach ($swimlanes as $swimlane_id) { + $this->db->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array('position' => ++$position)); + } + + return true; + } + + /** + * Move a swimlane down, increment the position value + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + public function moveDown($project_id, $swimlane_id) + { + $swimlanes = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->asc('position') + ->listing('id', 'position'); + + $positions = array_flip($swimlanes); + + if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] < count($swimlanes)) { + + $position = ++$swimlanes[$swimlane_id]; + $swimlanes[$positions[$position]]--; + + $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); + $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); + $this->db->closeTransaction(); + + return true; + } + + return false; + } + + /** + * Move a swimlane up, decrement the position value + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + public function moveUp($project_id, $swimlane_id) + { + $swimlanes = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->asc('position') + ->listing('id', 'position'); + + $positions = array_flip($swimlanes); + + if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] > 1) { + + $position = --$swimlanes[$swimlane_id]; + $swimlanes[$positions[$position]]++; + + $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); + $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); + $this->db->closeTransaction(); + + return true; + } + + return false; + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $rules = array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Required('name', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + new Validators\Required('name', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate default swimlane modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateDefaultModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + new Validators\Required('default_swimlane', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('The id must be an integer')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) + ); + } +} diff --git a/app/Model/Task.php b/app/Model/Task.php index a745f30f..3cd03741 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -32,6 +32,7 @@ class Task extends Base */ const EVENT_MOVE_COLUMN = 'task.move.column'; const EVENT_MOVE_POSITION = 'task.move.position'; + const EVENT_MOVE_SWIMLANE = 'task.move.swimlane'; const EVENT_UPDATE = 'task.update'; const EVENT_CREATE = 'task.create'; const EVENT_CLOSE = 'task.close'; diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php index 320bcb93..de9f7ce1 100644 --- a/app/Model/TaskCreation.php +++ b/app/Model/TaskCreation.php @@ -39,7 +39,7 @@ class TaskCreation extends Base { $this->dateParser->convert($values, array('date_due', 'date_started')); $this->removeFields($values, array('another_task')); - $this->resetFields($values, array('owner_id', 'owner_id', 'date_due', 'score', 'category_id', 'time_estimated')); + $this->resetFields($values, array('owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); if (empty($values['column_id'])) { $values['column_id'] = $this->board->getFirstColumn($values['project_id']); diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php index ab7a57f1..2410213b 100644 --- a/app/Model/TaskDuplication.php +++ b/app/Model/TaskDuplication.php @@ -27,6 +27,7 @@ class TaskDuplication extends Base 'score', 'category_id', 'time_estimated', + 'swimlane_id', ); /** @@ -79,6 +80,7 @@ class TaskDuplication extends Base $values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1; $values['owner_id'] = $task['owner_id']; $values['category_id'] = $task['category_id']; + $values['swimlane_id'] = $task['swimlane_id']; $this->checkDestinationProjectValues($values); @@ -100,8 +102,18 @@ class TaskDuplication extends Base // Check if the category exists for the destination project if ($values['category_id'] > 0) { - $category_name = $this->category->getNameById($values['category_id']); - $values['category_id'] = $this->category->getIdByName($values['project_id'], $category_name); + $values['category_id'] = $this->category->getIdByName( + $values['project_id'], + $this->category->getNameById($values['category_id']) + ); + } + + // Check if the swimlane exists for the destination project + if ($values['swimlane_id'] > 0) { + $values['swimlane_id'] = $this->swimlane->getIdByName( + $values['project_id'], + $this->swimlane->getNameById($values['swimlane_id']) + ); } } diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index 0e581025..117edae8 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -38,6 +38,7 @@ class TaskFinder extends Base 'tasks.color_id', 'tasks.project_id', 'tasks.column_id', + 'tasks.swimlane_id', 'tasks.owner_id', 'tasks.creator_id', 'tasks.position', @@ -54,13 +55,17 @@ class TaskFinder extends Base * Get all tasks shown on the board (sorted by position) * * @access public - * @param integer $project_id Project id + * @param integer $project_id Project id + * @param integer $column_id Column id + * @param integer $swimlane_id Swimlane id * @return array */ - public function getTasksOnBoard($project_id) + public function getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id = 0) { return $this->getQuery() ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->eq('swimlane_id', $swimlane_id) ->eq('is_active', Task::STATUS_OPEN) ->asc('tasks.position') ->findAll(); @@ -167,6 +172,7 @@ class TaskFinder extends Base tasks.is_active, tasks.score, tasks.category_id, + tasks.swimlane_id, project_has_categories.name AS category_name, projects.name AS project_name, columns.title AS column_title, diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php index c23bc3b5..9a9642e5 100644 --- a/app/Model/TaskPosition.php +++ b/app/Model/TaskPosition.php @@ -18,20 +18,25 @@ class TaskPosition extends Base * @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 * @return boolean */ - public function movePosition($project_id, $task_id, $column_id, $position) + public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0) { $original_task = $this->taskFinder->getById($task_id); - $positions = $this->calculatePositions($project_id, $task_id, $column_id, $position); - if ($positions === false || ! $this->savePositions($positions)) { - return false; - } + $result = $this->calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id); - $this->fireEvents($original_task, $column_id, $position); + if ($result) { - return true; + if ($original_task['swimlane_id'] != $swimlane_id) { + $this->calculateAndSave($project_id, 0, $column_id, 1, $original_task['swimlane_id']); + } + + $this->fireEvents($original_task, $column_id, $position, $swimlane_id); + } + + return $result; } /** @@ -42,9 +47,10 @@ class TaskPosition extends Base * @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 * @return array|boolean */ - public function calculatePositions($project_id, $task_id, $column_id, $position) + public function calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id = 0) { // The position can't be lower than 1 if ($position < 1) { @@ -59,6 +65,7 @@ class TaskPosition extends Base $columns[$board_column_id] = $this->db->table(Task::TABLE) ->eq('is_active', 1) + ->eq('swimlane_id', $swimlane_id) ->eq('project_id', $project_id) ->eq('column_id', $board_column_id) ->neq('id', $task_id) @@ -72,7 +79,9 @@ class TaskPosition extends Base } // We put our task to the new position - array_splice($columns[$column_id], $position - 1, 0, $task_id); + if ($task_id) { + array_splice($columns[$column_id], $position - 1, 0, $task_id); + } return $columns; } @@ -84,9 +93,9 @@ class TaskPosition extends Base * @param array $columns Sorted tasks * @return boolean */ - private function savePositions(array $columns) + private function savePositions(array $columns, $swimlane_id) { - return $this->db->transaction(function ($db) use ($columns) { + return $this->db->transaction(function ($db) use ($columns, $swimlane_id) { foreach ($columns as $column_id => $column) { @@ -96,7 +105,8 @@ class TaskPosition extends Base $result = $db->table(Task::TABLE)->eq('id', $task_id)->update(array( 'position' => $position, - 'column_id' => $column_id + 'column_id' => $column_id, + 'swimlane_id' => $swimlane_id, )); if (! $result) { @@ -112,25 +122,52 @@ class TaskPosition extends Base /** * Fire events * - * @access public + * @access private * @param array $task * @param integer $new_column_id * @param integer $new_position + * @param integer $new_swimlane_id */ - public function fireEvents(array $task, $new_column_id, $new_position) + private function fireEvents(array $task, $new_column_id, $new_position, $new_swimlane_id) { $event_data = array( 'task_id' => $task['id'], 'project_id' => $task['project_id'], 'position' => $new_position, 'column_id' => $new_column_id, + 'swimlane_id' => $new_swimlane_id, ); - if ($task['column_id'] != $new_column_id) { + if ($task['swimlane_id'] != $new_swimlane_id) { + $this->event->trigger(Task::EVENT_MOVE_SWIMLANE, $event_data); + } + else if ($task['column_id'] != $new_column_id) { $this->event->trigger(Task::EVENT_MOVE_COLUMN, $event_data); } else if ($task['position'] != $new_position) { $this->event->trigger(Task::EVENT_MOVE_POSITION, $event_data); } } + + /** + * Calculate the new position of all tasks + * + * @access private + * @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 + * @return boolean + */ + private function calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id) + { + $positions = $this->calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id); + + if ($positions === false || ! $this->savePositions($positions, $swimlane_id)) { + return false; + } + + return true; + } } diff --git a/app/Model/TaskValidator.php b/app/Model/TaskValidator.php index ecaf0902..ae21ca28 100644 --- a/app/Model/TaskValidator.php +++ b/app/Model/TaskValidator.php @@ -29,6 +29,7 @@ class TaskValidator extends Base new Validators\Integer('creator_id', t('This value must be an integer')), new Validators\Integer('score', t('This value must be an integer')), new Validators\Integer('category_id', t('This value must be an integer')), + new Validators\Integer('swimlane_id', t('This value must be an integer')), new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()), new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateFormats()), |