diff options
Diffstat (limited to 'app/Model')
-rw-r--r-- | app/Model/Base.php | 2 | ||||
-rw-r--r-- | app/Model/Link.php | 437 | ||||
-rw-r--r-- | app/Model/TaskDuplication.php | 1 | ||||
-rw-r--r-- | app/Model/TaskFinder.php | 25 | ||||
-rw-r--r-- | app/Model/TaskLink.php | 361 |
5 files changed, 826 insertions, 0 deletions
diff --git a/app/Model/Base.php b/app/Model/Base.php index 319e53fc..f836231c 100644 --- a/app/Model/Base.php +++ b/app/Model/Base.php @@ -25,6 +25,7 @@ use Pimple\Container; * @property \Model\File $file * @property \Model\Helper $helper * @property \Model\LastLogin $lastLogin + * @property \Model\Link $link * @property \Model\Notification $notification * @property \Model\Project $project * @property \Model\ProjectDuplication $projectDuplication @@ -38,6 +39,7 @@ use Pimple\Container; * @property \Model\TaskExport $taskExport * @property \Model\TaskFinder $taskFinder * @property \Model\TaskHistory $taskHistory + * @property \Model\TaskLink $taskLink * @property \Model\TaskPosition $taskPosition * @property \Model\TaskValidator $taskValidator * @property \Model\TimeTracking $timeTracking diff --git a/app/Model/Link.php b/app/Model/Link.php new file mode 100644 index 00000000..eddf1c6c --- /dev/null +++ b/app/Model/Link.php @@ -0,0 +1,437 @@ +<?php +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; +use PDO; + +/** + * Link model + * A link is made of one bidirectional (<->), or two sided (<- and ->) link labels. + * + * @package model + * @author Olivier Maridat + */ +class Link extends Base +{ + + /** + * SQL table name + * + * @var string + */ + const TABLE = 'link'; + + const TABLE_LABEL = 'link_label'; + + /** + * Direction: left to right -> + * + * @var integer + */ + const BEHAVIOUR_LEFT2RIGTH = 0; + + /** + * Direction: right to left <- + * + * @var integer + */ + const BEHAVIOUR_RIGHT2LEFT = 1; + + /** + * Bidirectional <-> + * + * @var integer + */ + const BEHAVIOUR_BOTH = 2; + + /** + * Get a link by the id + * + * @access public + * @param integer $link_id + * Link id + * @param integer $project_id + * Specify a project id. Default: -1 to target all projects + * @return array + */ + public function getById($link_id, $project_id = -1) + { + return $this->db->table(self::TABLE) + ->eq(self::TABLE . '.link_id', $link_id) + ->in('project_id', array( + - 1, + $project_id + )) + ->join(self::TABLE_LABEL, 'link_id', 'link_id') + ->findAll(); + } + + /** + * Get the id of the inverse link label by a link label id + * + * @access public + * @param integer $link_id + * Link id + * @param integer $link_label_id + * Link label id + * @return integer + */ + public function getInverseLinkLabelId($link_label_id) + { + $sql = 'SELECT + la2.id + FROM ' . self::TABLE_LABEL . ' la1 + JOIN '.self::TABLE_LABEL.' la2 ON la2.link_id = la1.link_id AND (la2.behaviour=2 OR la2.id != la1.id) + WHERE la1.id = ? + '; + $rq = $this->db->execute($sql, array( + $link_label_id + )); + return $rq->fetchColumn(0); + } + + /** + * Return all link labels for a given project + * + * @access public + * @param integer $project_id + * Specify a project id. Default: -1 to target all projects + * @return array + */ + public function getLinkLabels($project_id = -1) + { + return $this->db->table(self::TABLE_LABEL) + ->in(self::TABLE . '.project_id', array( + - 1, + $project_id + )) + ->join(self::TABLE, 'link_id', 'link_id') + ->asc(self::TABLE_LABEL.'.link_id', 'behaviour') + ->columns('id', self::TABLE . '.project_id', self::TABLE_LABEL.'.link_id', 'label', 'behaviour') + ->findAll(); + } + + /** + * Return the list of all link labels + * Used to select a link label + * + * @access public + * @param integer $project_id + * Specify a project id. Default: -1 to target all projects + * @return array + */ + public function getLinkLabelList($project_id = -1) + { + $listing = $this->getLinkLabels($project_id); + $mergedListing = array(); + foreach ($listing as $link) { + $suffix = ''; + $prefix = ''; + if (self::BEHAVIOUR_BOTH == $link['behaviour'] || self::BEHAVIOUR_LEFT2RIGTH == $link['behaviour']) { + $suffix = ' »'; + } + if (self::BEHAVIOUR_BOTH == $link['behaviour'] || self::BEHAVIOUR_RIGHT2LEFT == $link['behaviour']) { + $prefix = '« '; + } + $mergedListing[$link['id']] = $prefix . t($link['label']) . $suffix; + } + $listing = $mergedListing; + return $listing; + } + + /** + * Return the list of all links (label + inverse label) + * + * @access public + * @param integer $project_id + * Specify a project id. Default: -1 to target all projects + * @return array + */ + public function getMergedList($project_id = -1) + { + $listing = $this->getLinkLabels($project_id); + $mergedListing = array(); + $current = null; + foreach ($listing as $link) { + if (self::BEHAVIOUR_BOTH == $link['behaviour'] || self::BEHAVIOUR_LEFT2RIGTH == $link['behaviour']) { + $current = $link; + } + else { + $current['label_inverse'] = $link['label']; + } + if (self::BEHAVIOUR_BOTH == $link['behaviour'] || self::BEHAVIOUR_RIGHT2LEFT == $link['behaviour']) { + $mergedListing[] = $current; + $current = null; + } + } + $listing = $mergedListing; + return $listing; + } + + /** + * Prepare data before insert/update + * + * @access public + * @param array $values + * Form values + */ + public function prepare(array &$values) + { + // Prepare label 1 + $link = array( + 'project_id' => $values['project_id'] + ); + $label1 = array( + 'label' => @$values['label'][0], + 'behaviour' => (isset($values['behaviour'][0]) || !isset($values['label'][1]) || null == $values['label'][1]) ? self::BEHAVIOUR_BOTH : self::BEHAVIOUR_LEFT2RIGTH + ); + $label2 = array( + 'label' => @$values['label'][1], + 'behaviour' => self::BEHAVIOUR_RIGHT2LEFT + ); + if (isset($values['link_id'])) { + $link['link_id'] = $values['link_id']; + $label1['id'] = $values['id'][0]; + $label2['id'] = @$values['id'][1]; + $label1['link_id'] = $values['link_id']; + $label2['link_id'] = $values['link_id']; + } + + $values = $link; + $values[] = $label1;
+ $values[] = $label2; + return array( + $link, + $label1, + $label2 + ); + } + + /** + * Create a link + * + * @access public + * @param array $values + * Form values + * @return bool integer + */ + public function create(array $values) + { + list ($link, $label1, $label2) = $this->prepare($values); + // Create link + $this->db->startTransaction(); + $res = $this->db->table(self::TABLE)->save($link); + if (! $res) { + $this->db->cancelTransaction(); + return false; + } + + // Create label 1 + $label1['link_id'] = $this->db->getConnection()->lastInsertId(self::TABLE); + $res = $this->db->table(self::TABLE_LABEL)->save($label1); + if (! $res) { + $this->db->cancelTransaction(); + return false; + } + + // Create label 2 if any + if (null != $label2 && self::BEHAVIOUR_BOTH != $label1['behaviour']) { + $label2['link_id'] = $label1['link_id']; + $res = $this->db->table(self::TABLE_LABEL)->save($label2); + if (! $res) { + $this->db->cancelTransaction(); + return false; + } + } + $this->db->closeTransaction(); + return $res; + } + + /** + * Update a link + * + * @access public + * @param array $values + * Form values + * @return bool + */ + public function update(array $values) + { + list($link, $label1, $label2) = $this->prepare($values); + // Update link + $this->db->startTransaction(); + $res = $this->db->table(self::TABLE) + ->eq('link_id', $link['link_id']) + ->save($link); + if (! $res) { + $this->db->cancelTransaction(); + return false; + } + + // Update label 1 + $this->db->startTransaction(); + $res = $this->db->table(self::TABLE_LABEL) + ->eq('id', $label1['id']) + ->save($label1); + if (! $res) { + $this->db->cancelTransaction(); + return false; + } + + // Update label 2 (if label 1 not bidirectional) + if (null != $label2 && self::BEHAVIOUR_BOTH != $label1['behaviour']) { + // Create + if (! isset($label2['id']) || null == $label2['id']) { + unset($label2['id']); + $res = $this->db->table(self::TABLE_LABEL)->save($label2); + if (! $res) { + $this->db->cancelTransaction(); + return false; + } + $label2['id'] = $this->db->getConnection()->lastInsertId(self::TABLE_LABEL); + $this->taskLink->changeLinkLabel($link['link_id'], $label2['id'], true); + } + // Update + else { + $res = $this->db->table(self::TABLE_LABEL) + ->eq('id', $label2['id']) + ->save($label2); + if (! $res) { + $this->db->cancelTransaction(); + return false; + } + } + } + // Remove label 2 (if label 1 bidirectional) + else { + $this->taskLink->changeLinkLabel($link['link_id'], $label1['id']); + $this->db->table(self::TABLE_LABEL) + ->eq('link_id', $link['link_id']) + ->neq('id', $label1['id']) + ->remove(); + } + $this->db->closeTransaction(); + return $res; + } + + /** + * Remove a link + * + * @access public + * @param integer $link_id + * Link id + * @return bool + */ + public function remove($link_id) + { + $this->db->startTransaction(); + if (! $this->db->table(self::TABLE) + ->eq('link_id', $link_id) + ->remove()) { + $this->db->cancelTransaction(); + return false; + } + $this->db->closeTransaction(); + return true; + } + + /** + * Duplicate links from a project to another one, must be executed inside a transaction + * + * @param integer $src_project_id + * Source project id + * @return integer $dst_project_id Destination project id + * @return boolean + */ + public function duplicate($src_project_id, $dst_project_id) + { + $labels = $this->db->table(self::TABLE_LABEL) + ->columns(self::TABLE_LABEL.'.link_id', 'label', 'behaviour') + ->eq('project_id', $src_project_id) + ->join(self::TABLE, 'link_id', 'link_id') + ->asc(self::TABLE_LABEL.'.link_id', 'behaviour') + ->findAll(); + + $this->db->startTransaction(); + $link = array('project_id' => $dst_project_id);
+ if (! $this->db->table(self::TABLE)->save($link)) { + $this->db->cancelTransaction();
+ return false;
+ } + $link['id'] = $this->db->getConnection()->lastInsertId(self::TABLE); + + foreach ($labels as $label) { + $label['link_id'] = $link['id']; + if (! $this->db->table(self::TABLE_LABEL)->save($label)) { + $this->db->cancelTransaction(); + return false; + } + } + $this->db->closeTransaction(); + return true; + } + + /** + * Validate link 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) + { + $v = new Validator($values, $this->commonValidationRules()); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate link 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('link_id', t('The id is required')), +// new Validators\Required('id[0]', t('The label id 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() + { + // TODO Update simple-validator to support array in forms + return array( + new Validators\Required('project_id', t('The project id required')), + // new Validators\Required('label[0]', t('The link label is required')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\Integer('link_id', t('The link id must be an integer')), +// new Validators\Integer('id[0]', t('The link label id must be an integer')), +// new Validators\Integer('id[1]', t('The link label id must be an integer')), +// new Validators\Integer('behaviour[0]', t('The link label id must be an integer')), +// new Validators\Integer('behaviour[1]', t('The link label id must be an integer')), +// new Validators\MaxLength('label[0]', t('The maximum length is %d characters', 200), 200), +// new Validators\MaxLength('label[1]', t('The maximum length is %d characters', 200), 200) + ); + } +} diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php index bd593dc1..faa5467f 100644 --- a/app/Model/TaskDuplication.php +++ b/app/Model/TaskDuplication.php @@ -159,6 +159,7 @@ class TaskDuplication extends Base if ($new_task_id) { $this->subtask->duplicate($task_id, $new_task_id); + $this->taskLink->duplicate($task_id, $new_task_id); } return $new_task_id; diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index 27fa8150..cf756cd8 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -3,6 +3,7 @@ namespace Model; use PDO; +use Model\TaskLink; /** * Task Finder model @@ -84,6 +85,7 @@ class TaskFinder extends Base '(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files', '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id) AS nb_subtasks', '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id AND status=2) AS nb_completed_subtasks', + '(SELECT count(*) FROM ' . TaskLink::TABLE . ' WHERE ' . TaskLink::TABLE . '.task_id = tasks.id) AS nb_links', 'tasks.id', 'tasks.reference', 'tasks.title', @@ -128,6 +130,29 @@ class TaskFinder extends Base ->asc('tasks.position') ->findAll(); } + + /** + * Get ids and names of all (limited by $limit) tasks for a given project and status + * + * @access public + * @param integer $project_id Project id + * @param integer $status_id Status id + * @param integer $exclude_id Exclude this task id in the result + * @param integer $limit Number of tasks to list + * @return array + */ + public function getList($project_id, $status_id = Task::STATUS_OPEN, $exclude_id=null, $limit=50) + { + $sql = $this->db + ->hashtable(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', $status_id) + ->limit($limit); + if (null != $exclude_id) { + $sql->neq('id', $exclude_id); + } + return $sql->getAll('id', 'title'); + } /** * Get all tasks for a given project and status diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php new file mode 100644 index 00000000..09f37d2e --- /dev/null +++ b/app/Model/TaskLink.php @@ -0,0 +1,361 @@ +<?php +namespace Model; + +use Core\Helper; +use SimpleValidator\Validator; +use SimpleValidator\Validators; +use PDO; + +/** + * TaskLink model + * + * @package model + * @author Olivier Maridat + */ +class TaskLink extends Base +{ + + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_links'; + + /** + * Get a link by the task link id + * + * @access public + * @param integer $task_link_id + * Task link id + * @return array + */ + public function getById($task_link_id) + { + $sql = 'SELECT + tl1.id AS id, + tl1.link_label_id AS link_label_id, + tl1.task_id AS task_id, + tl1.task_inverse_id AS task_inverse_id, + tl2.id AS task_link_inverse_id + FROM ' . self::TABLE . ' tl1 + LEFT JOIN ' . Link::TABLE_LABEL . ' l1 ON l1.id = tl1.link_label_id + LEFT JOIN ' . Link::TABLE_LABEL . ' l2 ON l2.link_id = l1.link_id + LEFT JOIN ' . self::TABLE . ' tl2 ON tl2.task_id = tl1.task_inverse_id + AND ( (l1.behaviour = 2 AND tl2.link_label_id = l1.id) OR (tl2.link_label_id = l2.id) ) + WHERE tl1.id = ? + '; + $rq = $this->db->execute($sql, array( + $task_link_id + )); + return $rq->fetch(); + } + + /** + * Get the id of the inverse task link by a task link id + * + * @access public + * @param integer $link_id + * Task link id + * @return integer + */ + public function getInverseTaskLinkId($task_link_id) + { + $sql = 'SELECT + tl2.id + FROM ' . self::TABLE . ' tl1 + LEFT JOIN ' . Link::TABLE_LABEL . ' l1 ON l1.id = tl1.link_label_id + LEFT JOIN ' . Link::TABLE_LABEL . ' l2 ON l2.link_id = l1.link_id + LEFT JOIN ' . self::TABLE . ' tl2 ON tl2.task_id = tl1.task_inverse_id + AND ( (l1.behaviour = 2 AND tl2.link_label_id = l1.id) OR (tl2.link_label_id = l2.id) ) + WHERE tl1.id = ? + '; + $rq = $this->db->execute($sql, array( + $task_link_id + )); + return $rq->fetchColumn(0); + } + + /** + * Return all links for a given task + * + * @access public + * @param integer $task_id + * Task id + * @return array + */ + public function getAll($task_id) + { + $sql = 'SELECT + tl1.id, + l.label AS label, + t2.id AS task_inverse_id, + t2.project_id AS task_inverse_project_id, + t2.title AS task_inverse_title, + t2.is_active AS task_inverse_is_active, + t2cat.name AS task_inverse_category + FROM ' . self::TABLE . ' tl1 + LEFT JOIN ' . Link::TABLE_LABEL . ' l ON l.id = tl1.link_label_id + LEFT JOIN ' . Task::TABLE . ' t2 ON t2.id = tl1.task_inverse_id + LEFT JOIN ' . Category::TABLE . ' t2cat ON t2cat.id = t2.category_id + WHERE tl1.task_id = ? + ORDER BY l.label, t2cat.name, t2.id + '; + $rq = $this->db->execute($sql, array( + $task_id + )); + $res = $rq->fetchAll(PDO::FETCH_ASSOC); + return $res; + } + + /** + * Prepare data before insert/update + * + * @access public + * @param array $values + * Form values + */ + public function prepare(array &$values) + { + $this->removeFields($values, array( + 'another_link' + )); + $taskLink1 = array( + 'link_label_id' => $values['link_label_id'], + 'task_id' => $values['task_id'], + 'task_inverse_id' => $values['task_inverse_id'] + ); + $taskLink2 = array( + 'link_label_id' => $this->link->getInverseLinkLabelId($taskLink1['link_label_id']), + 'task_id' => $values['task_inverse_id'], + 'task_inverse_id' => $values['task_id'] + ); + if (isset($values['id']) && isset($values['task_link_inverse_id'])) { + $taskLink1['id'] = $values['id']; + $taskLink2['id'] = $values['task_link_inverse_id']; + } + return array( + $taskLink1, + $taskLink2 + ); + } + + /** + * Create a link + * + * @access public + * @param array $values + * Form values + * @return bool integer + */ + public function create(array $values) + { + list($taskLink1, $taskLink2) = $this->prepare($values); + $this->db->startTransaction(); + if (! $this->db->table(self::TABLE)->save($taskLink1)) { + $this->db->cancelTransaction(); + return false; + } + if (! $this->db->table(self::TABLE)->save($taskLink2)) { + $this->db->cancelTransaction(); + return false; + } + $this->db->closeTransaction(); + return true; + } + + /** + * Update a link + * + * @access public + * @param array $values + * Form values + * @return bool + */ + public function update(array $values) + { + list($taskLink1, $taskLink2) = $this->prepare($values); + $this->db->startTransaction(); + if (! $this->db->table(self::TABLE) + ->eq('id', $taskLink1['id']) + ->save($taskLink1)) { + $this->db->cancelTransaction(); + return false; + } + if (! $this->db->table(self::TABLE) + ->eq('id', $taskLink2['id']) + ->save($taskLink2)) { + $this->db->cancelTransaction(); + return false; + } + $this->db->closeTransaction(); + return true; + } + + /** + * Remove a link + * + * @access public + * @param integer $task_link_id + * Task Link id + * @return bool + */ + public function remove($task_link_id) + { + $task_link_inverse_id = $this->getInverseTaskLinkId($task_link_id); + $this->db->startTransaction(); + if (! $this->db->table(self::TABLE) + ->eq('id', $task_link_id) + ->remove()) { + $this->db->cancelTransaction(); + return false; + } + if (! $this->db->table(self::TABLE) + ->eq('id', $task_link_inverse_id) + ->remove()) { + $this->db->cancelTransaction(); + return false; + } + $this->db->closeTransaction(); + return true; + } + + /** + * Duplicate all links to another task + * + * @access public + * @param integer $src_task_id + * Source task id + * @param integer $dst_task_id + * Destination task id + * @return bool + */ + public function duplicate($src_task_id, $dst_task_id) + { + return $this->db->transaction(function ($db) use($src_task_id, $dst_task_id) + { + $links = $db->table(TaskLink::TABLE) + ->columns('link_label_id', 'task_id', 'task_inverse_id') + ->eq('task_id', $src_task_id) + ->asc('id') + ->findAll(); + foreach ($links as &$link) { + $link['task_id'] = $dst_task_id; + if (! $db->table(TaskLink::TABLE) + ->save($link)) { + return false; + } + } + + $links = $db->table(TaskLink::TABLE) + ->columns('link_label_id', 'task_id', 'task_inverse_id') + ->eq('task_inverse_id', $src_task_id) + ->asc('id') + ->findAll(); + foreach ($links as &$link) { + $link['task_inverse_id'] = $dst_task_id; + if (! $db->table(TaskLink::TABLE) + ->save($link)) { + return false; + } + } + }); + } + + /** + * Move a task link from a link label to an other + * + * @access public + * @param integer $link_id + * Link id + * @param integer $dst_link_label_id + * Destination link label id + * @return bool + */ + public function changeLinkLabel($link_id, $dst_link_label_id, $alternate=false) + { + $taskLinks = $this->db->table(Link::TABLE_LABEL) + ->eq('link_id', $link_id) + ->neq(Link::TABLE_LABEL.'.id', $dst_link_label_id) + ->join(self::TABLE, 'link_label_id', 'id') + ->asc(self::TABLE.'.id') + ->findAllByColumn(self::TABLE.'.id'); + foreach ($taskLinks as $i => $taskLinkId) { + if (null == $taskLinkId || ($alternate && 0 != $i % 2)) { + continue; + } + if (! $this->db->table(self::TABLE) + ->eq('id', $taskLinkId) + ->save(array('link_label_id' => $dst_link_label_id))) { + return false; + } + } + return true; + } + + /** + * Validate link 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) + { + $v = new Validator($values, $this->commonValidationRules()); + $res = array( + $v->execute(), + $v->getErrors() + ); + return $res; + } + + /** + * Validate link 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')) + ); + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + $res = array( + $v->execute(), + $v->getErrors() + ); + return $res; + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Required('link_label_id', t('The link type is required')), + new Validators\Required('task_id', t('The task id is required')), + new Validators\Required('task_inverse_id', t('The linked task id is required')), + new Validators\Integer('id', t('The id must be an integer')), + new Validators\Integer('link_label_id', t('The link id must be an integer')), + new Validators\Integer('task_id', t('The task id must be an integer')), + new Validators\Integer('task_inverse_id', t('The related task id must be an integer')), + new Validators\Integer('task_link_inverse_id', t('The related task link id must be an integer')), + new Validators\NotEquals('task_inverse_id', 'task_id', t('A task can not be linked to itself')), + new Validators\Exists('task_inverse_id', t('This linked task id doesn\'t exist'), $this->db->getConnection(), Task::TABLE, 'id'), + new Validators\Unique(array( + 'task_inverse_id', + 'link_label_id', + 'task_id' + ), t('The exact same link already exists'), $this->db->getConnection(), self::TABLE) + ); + } +} |