diff options
Diffstat (limited to 'app')
49 files changed, 1574 insertions, 32 deletions
diff --git a/app/Controller/App.php b/app/Controller/App.php index ef0a08a9..46731e7c 100644 --- a/app/Controller/App.php +++ b/app/Controller/App.php @@ -2,7 +2,8 @@ namespace Controller; -use Model\Subtask as SubTaskModel; +use Model\Subtask as SubtaskModel; +use Model\Task as TaskModel; /** * Application controller @@ -39,7 +40,7 @@ class App extends Base */ public function index($user_id = 0, $action = 'index') { - $status = array(SubTaskModel::STATUS_TODO, SubTaskModel::STATUS_INPROGRESS); + $status = array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS); $user_id = $user_id ?: $this->userSession->getId(); $projects = $this->projectPermission->getActiveMemberProjects($user_id); $project_ids = array_keys($projects); @@ -88,11 +89,8 @@ class App extends Base if (empty($payload['text'])) { $this->response->html('<p>'.t('Nothing to preview...').'</p>'); } - else { - $this->response->html( - $this->template->markdown($payload['text']) - ); - } + + $this->response->html($this->template->markdown($payload['text'])); } /** @@ -104,4 +102,21 @@ class App extends Base { $this->response->css($this->color->getCss()); } + + /** + * Task autocompletion (Ajax) + * + * @access public + */ + public function autocomplete() + { + $this->response->json( + $this->taskFilter + ->create() + ->filterByProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId())) + ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id'))) + ->filterByTitle($this->request->getStringParam('term')) + ->toAutoCompletion() + ); + } } diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 7f65e882..548fdb40 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -17,11 +17,13 @@ use Symfony\Component\EventDispatcher\Event; * @package controller * @author Frederic Guillot * + * @property \Core\Helper $helper * @property \Core\Session $session * @property \Core\Template $template * @property \Core\Paginator $paginator * @property \Integration\GithubWebhook $githubWebhook * @property \Integration\GitlabWebhook $gitlabWebhook + * @property \Integration\BitbucketWebhook $bitbucketWebhook * @property \Model\Acl $acl * @property \Model\Authentication $authentication * @property \Model\Action $action @@ -43,6 +45,7 @@ use Symfony\Component\EventDispatcher\Event; * @property \Model\Subtask $subtask * @property \Model\Swimlane $swimlane * @property \Model\Task $task + * @property \Model\Link $link * @property \Model\TaskCreation $taskCreation * @property \Model\TaskModification $taskModification * @property \Model\TaskDuplication $taskDuplication @@ -54,6 +57,7 @@ use Symfony\Component\EventDispatcher\Event; * @property \Model\TaskPermission $taskPermission * @property \Model\TaskStatus $taskStatus * @property \Model\TaskValidator $taskValidator + * @property \Model\TaskLink $taskLink * @property \Model\CommentHistory $commentHistory * @property \Model\SubtaskHistory $subtaskHistory * @property \Model\SubtaskTimeTracking $subtaskTimeTracking @@ -139,7 +143,7 @@ abstract class Base private function sendHeaders($action) { // HTTP secure headers - $this->response->csp(array('style-src' => "'self' 'unsafe-inline'")); + $this->response->csp(array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '*')); $this->response->nosniff(); $this->response->xss(); @@ -199,7 +203,7 @@ abstract class Base { $project_id = $this->request->getIntegerParam('project_id'); $task_id = $this->request->getIntegerParam('task_id'); - + // Allow urls without "project_id" if ($task_id > 0 && $project_id === 0) { $project_id = $this->taskFinder->getProjectId($task_id); diff --git a/app/Controller/Board.php b/app/Controller/Board.php index e859348e..90b7f357 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -402,6 +402,20 @@ class Board extends Base } /** + * Get links on mouseover + * + * @access public + */ + public function tasklinks() + { + $task = $this->getTask(); + $this->response->html($this->template->render('board/tasklinks', array( + 'links' => $this->taskLink->getLinks($task['id']), + 'task' => $task, + ))); + } + + /** * Get subtasks on mouseover * * @access public diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php index abbcab7f..0e749558 100644 --- a/app/Controller/Calendar.php +++ b/app/Controller/Calendar.php @@ -2,7 +2,7 @@ namespace Controller; -use Model\Task; +use Model\Task as TaskModel; /** * Project Calendar controller @@ -74,7 +74,7 @@ class Calendar extends Base $this->taskFilter ->create() ->filterByOwner($user_id) - ->filterByStatus(Task::STATUS_OPEN) + ->filterByStatus(TaskModel::STATUS_OPEN) ->filterByDueDateRange( $this->request->getStringParam('start'), $this->request->getStringParam('end') diff --git a/app/Controller/Link.php b/app/Controller/Link.php new file mode 100644 index 00000000..ec9c6195 --- /dev/null +++ b/app/Controller/Link.php @@ -0,0 +1,162 @@ +<?php + +namespace Controller; + +/** + * Link controller + * + * @package controller + * @author Olivier Maridat + * @author Frederic Guillot + */ +class Link extends Base +{ + /** + * Common layout for config views + * + * @access private + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + private function layout($template, array $params) + { + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['config_content_for_layout'] = $this->template->render($template, $params); + + return $this->template->layout('config/layout', $params); + } + + /** + * Get the current link + * + * @access private + * @return array + */ + private function getLink() + { + $link = $this->link->getById($this->request->getIntegerParam('link_id')); + + if (! $link) { + $this->notfound(); + } + + return $link; + } + + /** + * List of links + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $this->response->html($this->layout('link/index', array( + 'links' => $this->link->getMergedList(), + 'values' => $values, + 'errors' => $errors, + 'title' => t('Settings').' > '.t('Task\'s links'), + ))); + } + + /** + * Validate and save a new link + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->link->validateCreation($values); + + if ($valid) { + + if ($this->link->create($values['label'], $values['opposite_label'])) { + $this->session->flash(t('Link added successfully.')); + $this->response->redirect($this->helper->url('link', 'index')); + } + else { + $this->session->flashError(t('Unable to create your link.')); + } + } + + $this->index($values, $errors); + } + + /** + * Edit form + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $link = $this->getLink(); + $link['label'] = t($link['label']); + + $this->response->html($this->layout('link/edit', array( + 'values' => $values ?: $link, + 'errors' => $errors, + 'labels' => $this->link->getList($link['id']), + 'link' => $link, + 'title' => t('Link modification') + ))); + } + + /** + * Edit a link (validate the form and update the database) + * + * @access public + */ + public function update() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->link->validateModification($values); + + if ($valid) { + if ($this->link->update($values)) { + $this->session->flash(t('Link updated successfully.')); + $this->response->redirect($this->helper->url('link', 'index')); + } + else { + $this->session->flashError(t('Unable to update your link.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Confirmation dialog before removing a link + * + * @access public + */ + public function confirm() + { + $link = $this->getLink(); + + $this->response->html($this->layout('link/remove', array( + 'link' => $link, + 'title' => t('Remove a link') + ))); + } + + /** + * Remove a link + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $link = $this->getLink(); + + if ($this->link->remove($link['id'])) { + $this->session->flash(t('Link removed successfully.')); + } + else { + $this->session->flashError(t('Unable to remove this link.')); + } + + $this->response->redirect($this->helper->url('link', 'index')); + } +} diff --git a/app/Controller/Task.php b/app/Controller/Task.php index fdd20b5e..e561d5f7 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -36,6 +36,7 @@ class Task extends Base 'project' => $project, 'comments' => $this->comment->getAll($task['id']), 'subtasks' => $this->subtask->getAll($task['id']), + 'links' => $this->taskLink->getLinks($task['id']), 'task' => $task, 'columns_list' => $this->board->getColumnsList($task['project_id']), 'colors_list' => $this->color->getList(), @@ -70,6 +71,7 @@ class Task extends Base 'files' => $this->file->getAll($task['id']), 'comments' => $this->comment->getAll($task['id']), 'subtasks' => $subtasks, + 'links' => $this->taskLink->getLinks($task['id']), 'task' => $task, 'values' => $values, 'columns_list' => $this->board->getColumnsList($task['project_id']), diff --git a/app/Controller/Tasklink.php b/app/Controller/Tasklink.php new file mode 100644 index 00000000..61b7fab8 --- /dev/null +++ b/app/Controller/Tasklink.php @@ -0,0 +1,116 @@ +<?php + +namespace Controller; + +/** + * TaskLink controller + * + * @package controller + * @author Olivier Maridat + * @author Frederic Guillot + */ +class Tasklink extends Base +{ + /** + * Get the current link + * + * @access private + * @return array + */ + private function getTaskLink() + { + $link = $this->taskLink->getById($this->request->getIntegerParam('link_id')); + + if (! $link) { + $this->notfound(); + } + + return $link; + } + + /** + * Creation form + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + + if (empty($values)) { + $values = array( + 'task_id' => $task['id'], + ); + } + + $this->response->html($this->taskLayout('tasklink/create', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'labels' => $this->link->getList(0, false), + 'title' => t('Add a new link') + ))); + } + + /** + * Validation and creation + * + * @access public + */ + public function save() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskLink->validateCreation($values); + + if ($valid) { + + if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) { + $this->session->flash(t('Link added successfully.')); + $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + } + else { + $this->session->flashError(t('Unable to create your link.')); + } + } + + $this->create($values, $errors); + } + + /** + * Confirmation dialog before removing a link + * + * @access public + */ + public function confirm() + { + $task = $this->getTask(); + $link = $this->getTaskLink(); + + $this->response->html($this->taskLayout('tasklink/remove', array( + 'link' => $link, + 'task' => $task, + ))); + } + + /** + * Remove a link + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $task = $this->getTask(); + + if ($this->taskLink->remove($this->request->getIntegerParam('link_id'))) { + $this->session->flash(t('Link removed successfully.')); + } + else { + $this->session->flashError(t('Unable to remove this link.')); + } + + $this->response->redirect($this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } +} diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index d0bebe0c..db91895f 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 42db9bcd..967d5f62 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index e921c10f..c69d9084 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 0ed1031b..0dd29c37 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index 7fc5e842..7872dec4 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -688,4 +688,37 @@ return array( 'Task age in days' => 'Age de la tâche en jours', 'Days in this column' => 'Jours dans cette colonne', '%dd' => '%dj', + 'Add a link' => 'Ajouter un lien', + 'Add a new link' => 'Ajouter un nouveau lien', + 'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien : « %s » ?', + 'Do you really want to remove this link with task #%d?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche n°%s ?', + 'Field required' => 'Champ obligatoire', + 'Link added successfully.' => 'Lien créé avec succès.', + 'Link updated successfully.' => 'Lien mis à jour avec succès.', + 'Link removed successfully.' => 'Lien supprimé avec succès.', + 'Link labels' => 'Libellé des liens', + 'Link modification' => 'Modification d\'un lien', + 'Links' => 'Liens', + 'Link settings' => 'Paramètres des liens', + 'Opposite label' => 'Nom du libellé opposé', + 'Remove a link' => 'Supprimer un lien', + 'Task\'s links' => 'Liens des tâches', + 'The labels must be different' => 'Les libellés doivent être différents', + 'There is no link.' => 'Il n\'y a aucun lien.', + 'This label must be unique' => 'Ce libellé doit être unique', + 'Unable to create your link.' => 'Impossible d\'ajouter ce lien.', + 'Unable to update your link.' => 'Impossible de mettre à jour ce lien.', + 'Unable to remove this link.' => 'Impossible de supprimer ce lien.', + 'relates to' => 'est liée à', + 'blocks' => 'bloque', + 'is blocked by' => 'est bloquée par', + 'duplicates' => 'duplique', + 'is duplicated by' => 'est dupliquée par', + 'is a child of' => 'est un enfant de', + 'is a parent of' => 'est un parent de', + 'targets milestone' => 'vise l\'étape importante', + 'is a milestone of' => 'est une étape importante de', + 'fixes' => 'corrige', + 'is fixed by' => 'est corrigée par', + 'This task' => 'Cette tâche', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 3ca6cf28..1f8de0ea 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 5c3ef47b..afd41d5c 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 91030bee..9d06b83f 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 7acb1497..2ebfc219 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index ac865046..ded6e0db 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 624a17f1..de792516 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index b77a8a5a..838b426a 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 64e0170e..66c7712b 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index addf7fa6..32278e21 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -686,4 +686,37 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', ); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index b6c0e6b8..e713f2e1 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -38,6 +38,7 @@ class Acl extends Base 'project' => array('show', 'tasks', 'search', 'activity'), 'subtask' => '*', 'task' => '*', + 'tasklink' => '*', 'calendar' => array('show', 'events', 'save'), ); @@ -67,6 +68,7 @@ class Acl extends Base 'app' => array('dashboard'), 'user' => array('index', 'create', 'save', 'remove'), 'config' => '*', + 'link' => '*', 'project' => array('remove'), ); diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php index 92898cd5..86c1c43f 100644 --- a/app/Model/Authentication.php +++ b/app/Model/Authentication.php @@ -42,6 +42,13 @@ class Authentication extends Base // If the user is already logged it's ok if ($this->userSession->isLogged()) { + // Check if the user session match an existing user + if (! $this->user->exists($this->userSession->getId())) { + $this->backend('rememberMe')->destroy($this->userSession->getId()); + $this->session->close(); + return false; + } + // We update each time the RememberMe cookie tokens if ($this->backend('rememberMe')->hasCookie()) { $this->backend('rememberMe')->refresh(); 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..87ba49c4 --- /dev/null +++ b/app/Model/Link.php @@ -0,0 +1,234 @@ +<?php + +namespace Model; + +use PDO; +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Link model + * + * @package model + * @author Olivier Maridat + * @author Frederic Guillot + */ +class Link extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'links'; + + /** + * Get a link by id + * + * @access public + * @param integer $link_id Link id + * @return array + */ + public function getById($link_id) + { + return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne(); + } + + /** + * Get a link by name + * + * @access public + * @param string $label + * @return array + */ + public function getByLabel($label) + { + return $this->db->table(self::TABLE)->eq('label', $label)->findOne(); + } + + /** + * Get the opposite link id + * + * @access public + * @param integer $link_id Link id + * @return integer + */ + public function getOppositeLinkId($link_id) + { + $link = $this->getById($link_id); + return $link['opposite_id'] ?: $link_id; + } + + /** + * Get all links + * + * @access public + * @return array + */ + public function getAll() + { + return $this->db->table(self::TABLE)->findAll(); + } + + /** + * Get merged links + * + * @access public + * @return array + */ + public function getMergedList() + { + return $this->db + ->execute(' + SELECT + links.id, links.label, opposite.label as opposite_label + FROM links + LEFT JOIN links AS opposite ON opposite.id=links.opposite_id + ') + ->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Get label list + * + * @access public + * @param integer $exclude_id Exclude this link + * @param booelan $prepend Prepend default value + * @return array + */ + public function getList($exclude_id = 0, $prepend = true) + { + $labels = $this->db->hashtable(self::TABLE)->neq('id', $exclude_id)->asc('id')->getAll('id', 'label'); + + foreach ($labels as &$value) { + $value = t($value); + } + + return $prepend ? array('') + $labels : $labels; + } + + /** + * Create a new link label + * + * @access public + * @param string $label + * @param string $opposite_label + * @return boolean + */ + public function create($label, $opposite_label = '') + { + $this->db->startTransaction(); + + if (! $this->db->table(self::TABLE)->insert(array('label' => $label))) { + $this->db->cancelTransaction(); + return false; + } + + if ($opposite_label !== '') { + $this->createOpposite($opposite_label); + } + + $this->db->closeTransaction(); + + return true; + } + + /** + * Create the opposite label (executed inside create() method) + * + * @access private + * @param string $label + */ + private function createOpposite($label) + { + $label_id = $this->db->getConnection()->getLastId(); + + $this->db + ->table(self::TABLE) + ->insert(array( + 'label' => $label, + 'opposite_id' => $label_id, + )); + + $this->db + ->table(self::TABLE) + ->eq('id', $label_id) + ->update(array( + 'opposite_id' => $this->db->getConnection()->getLastId() + )); + } + + /** + * Update a link + * + * @access public + * @param array $values + * @return boolean + */ + public function update(array $values) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $values['id']) + ->update(array( + 'label' => $values['label'], + 'opposite_id' => $values['opposite_id'], + )); + } + + /** + * Remove a link a the relation to its opposite + * + * @access public + * @param integer $link_id + * @return boolean + */ + public function remove($link_id) + { + $this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0)); + return $this->db->table(self::TABLE)->eq('id', $link_id)->remove(); + } + + /** + * 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) + { + $v = new Validator($values, array( + new Validators\Required('label', t('Field required')), + new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), + new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')), + )); + + 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) + { + $v = new Validator($values, array( + new Validators\Required('id', t('Field required')), + new Validators\Required('opposite_id', t('Field required')), + new Validators\Required('label', t('Field required')), + new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 7762d0fd..652cc842 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -62,7 +62,7 @@ class ProjectActivity extends Base */ public function getProject($project_id, $limit = 50, $start = null, $end = null) { - return $this->getProjects(array($project_id), $limit, $start = null, $end = null); + return $this->getProjects(array($project_id), $limit, $start, $end); } /** @@ -91,15 +91,15 @@ class ProjectActivity extends Base ->join(User::TABLE, 'id', 'creator_id') ->desc(self::TABLE.'.id') ->limit($limit); - + if(!is_null($start)){ $query->gte('date_creation', $start); } - + if(!is_null($end)){ $query->lte('date_creation', $end); } - + $events = $query->findAll(); foreach ($events as &$event) { diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php index fb6316b6..12bd9309 100644 --- a/app/Model/ProjectPermission.php +++ b/app/Model/ProjectPermission.php @@ -326,7 +326,7 @@ class ProjectPermission extends Base * * @access public * @param integer $user_id User id - * @return []integer + * @return array */ public function getMemberProjectIds($user_id) { @@ -338,6 +338,23 @@ class ProjectPermission extends Base } /** + * Return a list of active project ids where the user is member + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getActiveMemberProjectIds($user_id) + { + return $this->db + ->table(Project::TABLE) + ->eq('user_id', $user_id) + ->eq(Project::TABLE.'.is_active', Project::ACTIVE) + ->join(self::TABLE, 'project_id', 'id') + ->findAllByColumn('projects.id'); + } + + /** * Return a list of active projects where the user is member * * @access public diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php index b5a90154..31de2795 100644 --- a/app/Model/TaskFilter.php +++ b/app/Model/TaskFilter.php @@ -18,6 +18,24 @@ class TaskFilter extends Base return $this; } + public function excludeTasks(array $task_ids) + { + $this->query->notin('id', $task_ids); + return $this; + } + + public function filterByTitle($title) + { + $this->query->ilike('title', '%'.$title.'%'); + return $this; + } + + public function filterByProjects(array $project_ids) + { + $this->query->in('project_id', $project_ids); + return $this; + } + public function filterByProject($project_id) { if ($project_id > 0) { @@ -94,6 +112,20 @@ class TaskFilter extends Base return $this->query->findAll(); } + public function toAutoCompletion() + { + return $this->query->columns('id', 'title')->filter(function(array $results) { + + foreach ($results as &$result) { + $result['value'] = $result['title']; + $result['label'] = '#'.$result['id'].' - '.$result['title']; + } + + return $results; + + })->findAll(); + } + public function toCalendarEvents() { $events = array(); diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index 27fa8150..98ece4e1 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -84,6 +84,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', diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php new file mode 100644 index 00000000..c712e5a8 --- /dev/null +++ b/app/Model/TaskLink.php @@ -0,0 +1,142 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * TaskLink model + * + * @package model + * @author Olivier Maridat + * @author Frederic Guillot + */ +class TaskLink extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_links'; + + /** + * Get a task link + * + * @access public + * @param integer $task_link_id Task link id + * @return array + */ + public function getById($task_link_id) + { + return $this->db->table(self::TABLE)->eq('id', $task_link_id)->findOne(); + } + + /** + * Get all links attached to a task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getLinks($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.opposite_task_id AS task_id', + Link::TABLE.'.label', + Task::TABLE.'.title', + Task::TABLE.'.is_active', + Task::TABLE.'.project_id' + ) + ->eq(self::TABLE.'.task_id', $task_id) + ->join(Link::TABLE, 'id', 'link_id') + ->join(Task::TABLE, 'id', 'opposite_task_id') + ->findAll(); + } + + /** + * 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 boolean + */ + public function create($task_id, $opposite_task_id, $link_id) + { + $this->db->startTransaction(); + + // Create the original link + $this->db->table(self::TABLE)->insert(array( + 'task_id' => $task_id, + 'opposite_task_id' => $opposite_task_id, + 'link_id' => $link_id, + )); + + $link_id = $this->link->getOppositeLinkId($link_id); + + // Create the opposite link + $this->db->table(self::TABLE)->insert(array( + 'task_id' => $opposite_task_id, + 'opposite_task_id' => $task_id, + 'link_id' => $link_id, + )); + + $this->db->closeTransaction(); + + return true; + } + + /** + * Remove a link between two tasks + * + * @access public + * @param integer $task_link_id + * @return boolean + */ + public function remove($task_link_id) + { + $this->db->startTransaction(); + + $link = $this->getById($task_link_id); + $link_id = $this->link->getOppositeLinkId($link['link_id']); + + $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove(); + + $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(); + + $this->db->closeTransaction(); + + return true; + } + + /** + * 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) + { + $v = new Validator($values, array( + new Validators\Required('task_id', t('Field required')), + new Validators\Required('link_id', t('Field required')), + new Validators\Required('title', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/User.php b/app/Model/User.php index 01be8597..7586f3c4 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -29,6 +29,18 @@ class User extends Base const EVERYBODY_ID = -1; /** + * Return true if the user exists + * + * @access public + * @param integer $user_id User id + * @return boolean + */ + public function exists($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->count() === 1; + } + + /** * Get query to fetch all users * * @access public diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 24bc2baf..947a62b3 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -4,13 +4,52 @@ namespace Schema; use PDO; use Core\Security; - -const VERSION = 45; +use Model\Link; + +const VERSION = 46; + +function version_46($pdo) +{ + $pdo->exec("CREATE TABLE links ( + id INT NOT NULL AUTO_INCREMENT, + label VARCHAR(255) NOT NULL, + opposite_id INT DEFAULT 0, + PRIMARY KEY(id), + UNIQUE(label) + ) ENGINE=InnoDB CHARSET=utf8"); + + $pdo->exec("CREATE TABLE task_has_links ( + id INT NOT NULL AUTO_INCREMENT, + link_id INT NOT NULL, + task_id INT NOT NULL, + opposite_task_id INT NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} function version_45($pdo) { $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INT DEFAULT 0'); - + /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0. * We take max project_activities.date_creation where event_name in task.create','task.move.column * since creation date is always less than task moves @@ -122,7 +161,7 @@ function version_38($pdo) "); $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INT DEFAULT 0'); - $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT '".t('Default swimlane')."'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'"); $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INT DEFAULT 1"); } diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index d3fb9fc4..027401ff 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -4,8 +4,45 @@ namespace Schema; use PDO; use Core\Security; +use Model\Link; -const VERSION = 26; +const VERSION = 27; + +function version_27($pdo) +{ + $pdo->exec('CREATE TABLE links ( + "id" SERIAL PRIMARY KEY, + "label" VARCHAR(255) NOT NULL, + "opposite_id" INTEGER DEFAULT 0, + UNIQUE("label") + )'); + + $pdo->exec("CREATE TABLE task_has_links ( + id SERIAL PRIMARY KEY, + link_id INTEGER NOT NULL, + task_id INTEGER NOT NULL, + opposite_task_id INTEGER NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE + )"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} function version_26($pdo) { @@ -66,7 +103,7 @@ function version_24($pdo) function version_23($pdo) { - $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); + $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); } function version_22($pdo) @@ -120,7 +157,7 @@ function version_19($pdo) "); $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0'); - $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT '".t('Default swimlane')."'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'"); $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane BOOLEAN DEFAULT '1'"); } diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index f027cf91..c6dec33f 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -4,13 +4,50 @@ namespace Schema; use Core\Security; use PDO; - -const VERSION = 44; +use Model\Link; + +const VERSION = 45; + +function version_45($pdo) +{ + $pdo->exec("CREATE TABLE links ( + id INTEGER PRIMARY KEY, + label TEXT NOT NULL, + opposite_id INTEGER DEFAULT 0, + UNIQUE(label) + )"); + + $pdo->exec("CREATE TABLE task_has_links ( + id INTEGER PRIMARY KEY, + link_id INTEGER NOT NULL, + task_id INTEGER NOT NULL, + opposite_task_id INTEGER NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE + )"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} function version_44($pdo) { $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INTEGER DEFAULT 0'); - + /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0. * We take max project_activities.date_creation where event_name in task.create','task.move.column * since creation date is always less than task moves @@ -66,7 +103,7 @@ function version_42($pdo) function version_41($pdo) { - $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); + $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); } function version_40($pdo) @@ -120,7 +157,7 @@ function version_37($pdo) "); $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0'); - $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane TEXT DEFAULT '".t('Default swimlane')."'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane TEXT DEFAULT 'Default swimlane'"); $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INTEGER DEFAULT 1"); } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index bee03184..213972ed 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface 'DateParser', 'File', 'LastLogin', + 'Link', 'Notification', 'Project', 'ProjectActivity', @@ -41,6 +42,7 @@ class ClassProvider implements ServiceProviderInterface 'TaskExport', 'TaskFinder', 'TaskFilter', + 'TaskLink', 'TaskModification', 'TaskPermission', 'TaskPosition', diff --git a/app/Subscriber/Base.php b/app/Subscriber/Base.php index 489d49b0..f90d9604 100644 --- a/app/Subscriber/Base.php +++ b/app/Subscriber/Base.php @@ -10,6 +10,7 @@ use Pimple\Container; * @package subscriber * @author Frederic Guillot * + * @property \Model\Board $board * @property \Model\Config $config * @property \Model\Comment $comment * @property \Model\LastLogin $lastLogin diff --git a/app/Template/board/task.php b/app/Template/board/task.php index 1608e337..075eab92 100644 --- a/app/Template/board/task.php +++ b/app/Template/board/task.php @@ -100,7 +100,7 @@ <?php endif ?> -<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description']) || ! empty($task['nb_subtasks'])): ?> +<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description']) || ! empty($task['nb_subtasks']) || ! empty($task['nb_links'])): ?> <div class="task-board-footer"> <?php if (! empty($task['date_due'])): ?> @@ -110,7 +110,10 @@ <?php endif ?> <div class="task-board-icons"> - + <?php if (! empty($task['nb_links'])): ?> + <span title="<?= t('Links') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= $task['nb_links'] ?> <i class="fa fa-code-fork"></i></span> + <?php endif ?> + <?php if (! empty($task['nb_subtasks'])): ?> <span title="<?= t('Sub-Tasks') ?>" class="task-board-tooltip" data-href="<?= $this->u('board', 'subtasks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= round($task['nb_completed_subtasks']/$task['nb_subtasks']*100, 0).'%' ?> <i class="fa fa-bars"></i></span> <?php endif ?> @@ -132,4 +135,4 @@ </div> <?php endif ?> -</div>
\ No newline at end of file +</div> diff --git a/app/Template/board/tasklinks.php b/app/Template/board/tasklinks.php new file mode 100644 index 00000000..9c4f52ca --- /dev/null +++ b/app/Template/board/tasklinks.php @@ -0,0 +1,15 @@ +<div class="tooltip-tasklinks"> + <ul> + <?php foreach($links as $link): ?> + <li> + <strong><?= t($link['label']) ?></strong> + <?= $this->a( + $this->e('#'.$link['task_id'].' - '.$link['title']), + 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + </li> + <?php endforeach ?> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php index 8e6fa379..89f2c203 100644 --- a/app/Template/config/sidebar.php +++ b/app/Template/config/sidebar.php @@ -11,6 +11,9 @@ <?= $this->a(t('Board settings'), 'config', 'board') ?> </li> <li> + <?= $this->a(t('Link settings'), 'link', 'index') ?> + </li> + <li> <?= $this->a(t('Webhooks'), 'config', 'webhook') ?> </li> <li> diff --git a/app/Template/link/create.php b/app/Template/link/create.php new file mode 100644 index 00000000..12589574 --- /dev/null +++ b/app/Template/link/create.php @@ -0,0 +1,18 @@ +<div class="page-header"> + <h2><?= t('Add a new link') ?></h2> +</div> + +<form action="<?= $this->u('link', 'save') ?>" method="post" autocomplete="off"> + + <?= $this->formCsrf() ?> + + <?= $this->formLabel(t('Label'), 'label') ?> + <?= $this->formText('label', $values, $errors, array('required')) ?> + + <?= $this->formLabel(t('Opposite label'), 'opposite_label') ?> + <?= $this->formText('opposite_label', $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/link/edit.php b/app/Template/link/edit.php new file mode 100644 index 00000000..d9ce280c --- /dev/null +++ b/app/Template/link/edit.php @@ -0,0 +1,21 @@ +<div class="page-header"> + <h2><?= t('Link modification') ?></h2> +</div> + +<form action="<?= $this->u('link', 'update', array('link_id' => $link['id'])) ?>" method="post" autocomplete="off"> + + <?= $this->formCsrf() ?> + <?= $this->formHidden('id', $values) ?> + + <?= $this->formLabel(t('Label'), 'label') ?> + <?= $this->formText('label', $values, $errors, array('required')) ?> + + <?= $this->formLabel(t('Opposite label'), 'opposite_id') ?> + <?= $this->formSelect('opposite_id', $labels, $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->a(t('cancel'), 'link', 'index') ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/link/index.php b/app/Template/link/index.php new file mode 100644 index 00000000..90d1c357 --- /dev/null +++ b/app/Template/link/index.php @@ -0,0 +1,33 @@ +<div class="page-header"> + <h2><?= t('Link labels') ?></h2> +</div> +<?php if (! empty($links)): ?> +<table> + <tr> + <th class="column-70"><?= t('Link labels') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($links as $link): ?> + <tr> + <td> + <strong><?= t($link['label']) ?></strong> + + <?php if (! empty($link['opposite_label'])): ?> + | <?= t($link['opposite_label']) ?> + <?php endif ?> + </td> + <td> + <ul> + <?= $this->a(t('Edit'), 'link', 'edit', array('link_id' => $link['id'])) ?> + <?= t('or') ?> + <?= $this->a(t('Remove'), 'link', 'confirm', array('link_id' => $link['id'])) ?> + </ul> + </td> + </tr> + <?php endforeach ?> +</table> +<?php else: ?> + <?= t('There is no link.') ?> +<?php endif ?> + +<?= $this->render('link/create', array('values' => $values, 'errors' => $errors)) ?>
\ No newline at end of file diff --git a/app/Template/link/remove.php b/app/Template/link/remove.php new file mode 100644 index 00000000..a802feb0 --- /dev/null +++ b/app/Template/link/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove a link') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this link: "%s"?', $link['label']) ?> + </p> + + <div class="form-actions"> + <?= $this->a(t('Yes'), 'link', 'remove', array('link_id' => $link['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->a(t('cancel'), 'link', 'index') ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/task/public.php b/app/Template/task/public.php index 2d95e6db..d7acef9f 100644 --- a/app/Template/task/public.php +++ b/app/Template/task/public.php @@ -10,6 +10,13 @@ 'is_public' => true )) ?> + <?= $this->render('tasklink/show', array( + 'task' => $task, + 'links' => $links, + 'project' => $project, + 'not_editable' => true + )) ?> + <?= $this->render('subtask/show', array( 'task' => $task, 'subtasks' => $subtasks, diff --git a/app/Template/task/show.php b/app/Template/task/show.php index b8243cc6..1ff2ef43 100644 --- a/app/Template/task/show.php +++ b/app/Template/task/show.php @@ -1,7 +1,8 @@ <?= $this->render('task/details', array('task' => $task, 'project' => $project)) ?> <?= $this->render('task/time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?> <?= $this->render('task/show_description', array('task' => $task)) ?> +<?= $this->render('tasklink/show', array('task' => $task, 'links' => $links)) ?> <?= $this->render('subtask/show', array('task' => $task, 'subtasks' => $subtasks)) ?> <?= $this->render('task/timesheet', array('task' => $task)) ?> <?= $this->render('file/show', array('task' => $task, 'files' => $files)) ?> -<?= $this->render('task/comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?>
\ No newline at end of file +<?= $this->render('task/comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?> diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php index e85a1671..f41be14d 100644 --- a/app/Template/task/sidebar.php +++ b/app/Template/task/sidebar.php @@ -19,6 +19,9 @@ <?= $this->a(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <li> + <?= $this->a(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> <?= $this->a(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <li> diff --git a/app/Template/tasklink/create.php b/app/Template/tasklink/create.php new file mode 100644 index 00000000..fb438cd8 --- /dev/null +++ b/app/Template/tasklink/create.php @@ -0,0 +1,27 @@ +<div class="page-header"> + <h2><?= t('Add a new link') ?></h2> +</div> + +<form action="<?= $this->u('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off"> + + <?= $this->formCsrf() ?> + <?= $this->formHidden('task_id', $values) ?> + <?= $this->formHidden('opposite_task_id', $values) ?> + + <?= $this->formLabel(t('Label'), 'link_id') ?> + <?= $this->formSelect('link_id', $labels, $values, $errors) ?> + + <?= $this->formLabel(t('Task'), 'title') ?> + <?= $this->formText( + 'title', + $values, + $errors, + array('required', 'data-dst-field="opposite_task_id"', 'data-search-url="'.$this->u('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"'), + 'task-autocomplete') ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/tasklink/remove.php b/app/Template/tasklink/remove.php new file mode 100644 index 00000000..9322ec24 --- /dev/null +++ b/app/Template/tasklink/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove a link') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this link with task #%d?', $link['opposite_task_id']) ?> + </p> + + <div class="form-actions"> + <?= $this->a(t('Yes'), 'tasklink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/tasklink/show.php b/app/Template/tasklink/show.php new file mode 100644 index 00000000..ca4e4383 --- /dev/null +++ b/app/Template/tasklink/show.php @@ -0,0 +1,41 @@ +<?php if (! empty($links)): ?> +<div class="page-header"> + <h2><?= t('Links') ?></h2> +</div> +<table class="table-fixed" id="links"> + <tr> + <th class="column-30"><?= t('Label') ?></th> + <th class="column-60"><?= t('Task') ?></th> + <?php if (! isset($not_editable)): ?> + <th><?= t('Action') ?></th> + <?php endif ?> + </tr> + <?php foreach ($links as $link): ?> + <tr> + <td><?= t('This task') ?> <strong><?= t($link['label']) ?></strong></td> + <?php if (! isset($not_editable)): ?> + <td> + <?= $this->a( + $this->e('#'.$link['task_id'].' - '.$link['title']), + 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + </td> + <td> + <?= $this->a(t('Remove'), 'tasklink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </td> + <?php else: ?> + <td> + <?= $this->a( + $this->e('#'.$link['task_id'].' - '.$link['title']), + 'task', 'readonly', array('task_id' => $link['task_id'], 'token' => $project['token']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> +</table> +<?php endif ?>
\ No newline at end of file |