From 9bde377bbe85617dde280af985e033cf7de61803 Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Tue, 9 Sep 2014 20:39:45 +0200 Subject: Start to implement task history and project activity --- app/Controller/Base.php | 2 + app/Controller/Project.php | 17 +++ app/Event/TaskHistoryListener.php | 52 ++++++++ app/Model/Acl.php | 2 +- app/Model/Base.php | 5 +- app/Model/Project.php | 13 ++ app/Model/Task.php | 9 +- app/Model/TaskHistory.php | 192 +++++++++++++++++++++++++++++ app/Schema/Sqlite.php | 19 ++- app/Templates/board_index.php | 1 + app/Templates/event_task_close.php | 6 + app/Templates/event_task_create.php | 6 + app/Templates/event_task_move_column.php | 6 + app/Templates/event_task_move_position.php | 6 + app/Templates/event_task_open.php | 6 + app/Templates/event_task_update.php | 6 + app/Templates/project_activity.php | 28 +++++ app/Templates/project_search.php | 1 + app/Templates/project_tasks.php | 1 + 19 files changed, 370 insertions(+), 8 deletions(-) create mode 100644 app/Event/TaskHistoryListener.php create mode 100644 app/Model/TaskHistory.php create mode 100644 app/Templates/event_task_close.php create mode 100644 app/Templates/event_task_create.php create mode 100644 app/Templates/event_task_move_column.php create mode 100644 app/Templates/event_task_move_position.php create mode 100644 app/Templates/event_task_open.php create mode 100644 app/Templates/event_task_update.php create mode 100644 app/Templates/project_activity.php (limited to 'app') diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 2d7b0c18..4f920ab0 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -27,6 +27,7 @@ use Model\LastLogin; * @property \Model\Project $project * @property \Model\SubTask $subTask * @property \Model\Task $task + * @property \Model\TaskHistory $taskHistory * @property \Model\User $user * @property \Model\Webhook $webhook */ @@ -134,6 +135,7 @@ abstract class Base $this->project->attachEvents(); $this->webhook->attachEvents(); $this->notification->attachEvents(); + $this->taskHistory->attachEvents(); } /** diff --git a/app/Controller/Project.php b/app/Controller/Project.php index deca7e1a..26b241b9 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -406,6 +406,23 @@ class Project extends Base $this->response->redirect('?controller=project&action=show&project_id='.$project_id); } + /** + * Activity page for a project + * + * @access public + */ + public function activity() + { + $project = $this->getProject(); + + $this->response->html($this->template->layout('project_activity', array( + 'events' => $this->project->getActivity($project['id']), + 'menu' => 'projects', + 'project' => $project, + 'title' => t('%s\'s activity', $project['name']) + ))); + } + /** * Task search for a given project * diff --git a/app/Event/TaskHistoryListener.php b/app/Event/TaskHistoryListener.php new file mode 100644 index 00000000..d6709f6e --- /dev/null +++ b/app/Event/TaskHistoryListener.php @@ -0,0 +1,52 @@ +model = $model; + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function execute(array $data) + { + $creator_id = $this->model->acl->getUserId(); + + if ($creator_id && isset($data['task_id']) && isset($data['project_id'])) { + $this->model->create($data['project_id'], $data['task_id'], $creator_id, $this->model->event->getLastTriggeredEvent()); + } + + return false; + } +} diff --git a/app/Model/Acl.php b/app/Model/Acl.php index 4f7d1357..ca00a09e 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -31,7 +31,7 @@ class Acl extends Base private $user_actions = array( 'app' => array('index'), 'board' => array('index', 'show', 'save', 'check', 'changeassignee', 'updateassignee', 'changecategory', 'updatecategory'), - 'project' => array('tasks', 'index', 'forbidden', 'search', 'export', 'show'), + 'project' => array('tasks', 'index', 'forbidden', 'search', 'export', 'show', 'activity'), 'user' => array('index', 'edit', 'forbidden', 'logout', 'index', 'show', 'external', 'unlinkgoogle', 'unlinkgithub', 'sessions', 'removesession', 'last', 'notifications', 'password'), 'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'), 'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'), diff --git a/app/Model/Base.php b/app/Model/Base.php index c2e1dcb4..9cf0b766 100644 --- a/app/Model/Base.php +++ b/app/Model/Base.php @@ -26,6 +26,7 @@ use PicoDb\Database; * @property \Model\Project $project * @property \Model\SubTask $subTask * @property \Model\Task $task + * @property \Model\TaskHistory $taskHistory * @property \Model\User $user * @property \Model\Webhook $webhook */ @@ -42,10 +43,10 @@ abstract class Base /** * Event dispatcher instance * - * @access protected + * @access public * @var \Core\Event */ - protected $event; + public $event; /** * Registry instance diff --git a/app/Model/Project.php b/app/Model/Project.php index 3298b496..1da5c164 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -710,4 +710,17 @@ class Project extends Base $this->event->attach($event_name, $listener); } } + + /** + * Get project activity + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getActivity($project_id) + { + // TODO: merge comments and subtasks activity + return $this->taskHistory->getAllContentByProjectId($project_id); + } } diff --git a/app/Model/Task.php b/app/Model/Task.php index 8c1fec8e..7ebb4641 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -464,10 +464,7 @@ class Task extends Base */ public function triggerUpdateEvents(array $original_task, array $updated_task) { - $events = array( - self::EVENT_CREATE_UPDATE, - self::EVENT_UPDATE, - ); + $events = array(); if (isset($updated_task['column_id']) && $original_task['column_id'] != $updated_task['column_id']) { $events[] = self::EVENT_MOVE_COLUMN; @@ -475,6 +472,10 @@ class Task extends Base else if (isset($updated_task['position']) && $original_task['position'] != $updated_task['position']) { $events[] = self::EVENT_MOVE_POSITION; } + else { + $events[] = self::EVENT_CREATE_UPDATE; + $events[] = self::EVENT_UPDATE; + } $event_data = array_merge($original_task, $updated_task); $event_data['task_id'] = $original_task['id']; diff --git a/app/Model/TaskHistory.php b/app/Model/TaskHistory.php new file mode 100644 index 00000000..181e4181 --- /dev/null +++ b/app/Model/TaskHistory.php @@ -0,0 +1,192 @@ + $project_id, + 'task_id' => $task_id, + 'creator_id' => $creator_id, + 'event_name' => $event_name, + 'date_creation' => time(), + ); + + $this->db->startTransaction(); + + $this->cleanup(self::MAX_EVENTS - 1); + $result = $this->db->table(self::TABLE)->insert($values); + + $this->db->closeTransaction(); + + return $result; + } + + /** + * Remove old event entries to avoid a large table + * + * @access public + * @param integer $max Maximum number of items to keep in the table + */ + public function cleanup($max) + { + if ($this->db->table(self::TABLE)->count() > $max) { + + $this->db->execute(' + DELETE FROM '.self::TABLE.' + WHERE id <= ( + SELECT id FROM ( + SELECT id FROM '.self::TABLE.' ORDER BY id DESC LIMIT 1 OFFSET '.$max.' + ) foo + )'); + } + } + + /** + * Get all events for a given project + * + * @access public + * @return array + */ + public function getAllByProjectId($project_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->desc('id') + ->findAll(); + } + + /** + * Get all necessary content to display activity feed + * + * $author_name + * $author_username + * $task['id', 'title', 'position', 'column_name'] + */ + public function getAllContentByProjectId($project_id, $limit = 50) + { + $sql = ' + SELECT + task_has_events.date_creation, + task_has_events.event_name, + task_has_events.task_id, + tasks.title as task_title, + tasks.position as task_position, + columns.title as task_column_name, + users.username as author_username, + users.name as author_name + FROM task_has_events + LEFT JOIN users ON users.id=task_has_events.creator_id + LEFT JOIN tasks ON tasks.id=task_has_events.task_id + LEFT JOIN columns ON columns.id=tasks.column_id + WHERE task_has_events.project_id = ? + ORDER BY task_has_events.id DESC + LIMIT 0, '.$limit.' + '; + + $rq = $this->db->execute($sql, array($project_id)); + $events = $rq->fetchAll(PDO::FETCH_ASSOC); + + foreach ($events as &$event) { + $event['author'] = $event['author_name'] ?: $event['author_username']; + $event['event_title'] = $this->getTitle($event); + $event['event_content'] = $this->getContent($event); + $event['event_type'] = 'task'; + } + + return $events; + } + + /** + * Get the event title (translated) + * + * @access public + * @param array $event Event properties + * @return string + */ + public function getTitle(array $event) + { + $titles = array( + Task::EVENT_UPDATE => t('%s updated the task #%d', $event['author'], $event['task_id']), + Task::EVENT_CREATE => t('%s created the task #%d', $event['author'], $event['task_id']), + Task::EVENT_CLOSE => t('%s closed the task #%d', $event['author'], $event['task_id']), + Task::EVENT_OPEN => t('%s open the task #%d', $event['author'], $event['task_id']), + Task::EVENT_MOVE_COLUMN => t('%s moved the task #%d to the column %s', $event['author'], $event['task_id'], $event['task_column_name']), + Task::EVENT_MOVE_POSITION => t('%s moved the task #%d to the position %d in the column %s', $event['author'], $event['task_id'], $event['task_position'], $event['task_column_name']), + ); + + return isset($titles[$event['event_name']]) ? $titles[$event['event_name']] : ''; + } + + /** + * Get the event html content + * + * @access public + * @param array $params Event properties + * @return string + */ + public function getContent(array $params) + { + $tpl = new Template; + return $tpl->load('event_'.str_replace('.', '_', $params['event_name']), $params); + } + + /** + * Attach events to be able to record the history + * + * @access public + */ + public function attachEvents() + { + $events = array( + Task::EVENT_UPDATE, + Task::EVENT_CREATE, + Task::EVENT_CLOSE, + Task::EVENT_OPEN, + Task::EVENT_MOVE_COLUMN, + Task::EVENT_MOVE_POSITION, + ); + + $listener = new TaskHistoryListener($this); + + foreach ($events as $event_name) { + $this->event->attach($event_name, $listener); + } + } +} diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 21b6ed4d..e18d35e9 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -4,7 +4,24 @@ namespace Schema; use Core\Security; -const VERSION = 24; +const VERSION = 25; + +function version_25($pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_events ( + id INTEGER PRIMARY KEY, + date_creation INTEGER, + event_name TEXT NOT NULL, + creator_id INTEGER, + project_id INTEGER, + task_id INTEGER, + FOREIGN KEY(creator_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ); + "); +} function version_24($pdo) { diff --git a/app/Templates/board_index.php b/app/Templates/board_index.php index c168d11a..da40468d 100644 --- a/app/Templates/board_index.php +++ b/app/Templates/board_index.php @@ -19,6 +19,7 @@
  • +
  • diff --git a/app/Templates/event_task_close.php b/app/Templates/event_task_close.php new file mode 100644 index 00000000..d5952d74 --- /dev/null +++ b/app/Templates/event_task_close.php @@ -0,0 +1,6 @@ +

    + +

    +

    + +

    \ No newline at end of file diff --git a/app/Templates/event_task_create.php b/app/Templates/event_task_create.php new file mode 100644 index 00000000..f0dcf2d7 --- /dev/null +++ b/app/Templates/event_task_create.php @@ -0,0 +1,6 @@ +

    + +

    +

    + +

    \ No newline at end of file diff --git a/app/Templates/event_task_move_column.php b/app/Templates/event_task_move_column.php new file mode 100644 index 00000000..0ccad655 --- /dev/null +++ b/app/Templates/event_task_move_column.php @@ -0,0 +1,6 @@ +

    + +

    +

    + +

    \ No newline at end of file diff --git a/app/Templates/event_task_move_position.php b/app/Templates/event_task_move_position.php new file mode 100644 index 00000000..e7d6b244 --- /dev/null +++ b/app/Templates/event_task_move_position.php @@ -0,0 +1,6 @@ +

    + +

    +

    + +

    \ No newline at end of file diff --git a/app/Templates/event_task_open.php b/app/Templates/event_task_open.php new file mode 100644 index 00000000..b3bc595a --- /dev/null +++ b/app/Templates/event_task_open.php @@ -0,0 +1,6 @@ +

    + +

    +

    + +

    \ No newline at end of file diff --git a/app/Templates/event_task_update.php b/app/Templates/event_task_update.php new file mode 100644 index 00000000..e2af6776 --- /dev/null +++ b/app/Templates/event_task_update.php @@ -0,0 +1,6 @@ +

    + +

    +

    + +

    \ No newline at end of file diff --git a/app/Templates/project_activity.php b/app/Templates/project_activity.php new file mode 100644 index 00000000..7886cfa4 --- /dev/null +++ b/app/Templates/project_activity.php @@ -0,0 +1,28 @@ +
    + +
    + +

    + + +
    +

    + + + +   +

    +
    +
    + + +
    +
    \ No newline at end of file diff --git a/app/Templates/project_search.php b/app/Templates/project_search.php index 7826ba63..a810afce 100644 --- a/app/Templates/project_search.php +++ b/app/Templates/project_search.php @@ -9,6 +9,7 @@ diff --git a/app/Templates/project_tasks.php b/app/Templates/project_tasks.php index a820be13..bc27befa 100644 --- a/app/Templates/project_tasks.php +++ b/app/Templates/project_tasks.php @@ -4,6 +4,7 @@ -- cgit v1.2.3