diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/Controller/Base.php | 2 | ||||
-rw-r--r-- | app/Controller/Project.php | 17 | ||||
-rw-r--r-- | app/Event/TaskHistoryListener.php | 52 | ||||
-rw-r--r-- | app/Model/Acl.php | 2 | ||||
-rw-r--r-- | app/Model/Base.php | 5 | ||||
-rw-r--r-- | app/Model/Project.php | 13 | ||||
-rw-r--r-- | app/Model/Task.php | 9 | ||||
-rw-r--r-- | app/Model/TaskHistory.php | 192 | ||||
-rw-r--r-- | app/Schema/Sqlite.php | 19 | ||||
-rw-r--r-- | app/Templates/board_index.php | 1 | ||||
-rw-r--r-- | app/Templates/event_task_close.php | 6 | ||||
-rw-r--r-- | app/Templates/event_task_create.php | 6 | ||||
-rw-r--r-- | app/Templates/event_task_move_column.php | 6 | ||||
-rw-r--r-- | app/Templates/event_task_move_position.php | 6 | ||||
-rw-r--r-- | app/Templates/event_task_open.php | 6 | ||||
-rw-r--r-- | app/Templates/event_task_update.php | 6 | ||||
-rw-r--r-- | app/Templates/project_activity.php | 28 | ||||
-rw-r--r-- | app/Templates/project_search.php | 1 | ||||
-rw-r--r-- | app/Templates/project_tasks.php | 1 |
19 files changed, 370 insertions, 8 deletions
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 @@ -407,6 +407,23 @@ class Project extends Base } /** + * 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 * * @access public 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 @@ +<?php + +namespace Event; + +use Core\Listener; +use Model\TaskHistory; + +/** + * Task history listener + * + * @package event + * @author Frederic Guillot + */ +class TaskHistoryListener implements Listener +{ + /** + * TaskHistory model + * + * @accesss private + * @var \Model\TaskHistory + */ + private $model; + + /** + * Constructor + * + * @access public + * @param \Model\TaskHistory $model TaskHistory model instance + */ + public function __construct(TaskHistory $model) + { + $this->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 @@ +<?php + +namespace Model; + +use PDO; +use Core\Template; +use Event\TaskHistoryListener; + +/** + * Task history model + * + * @package model + * @author Frederic Guillot + */ +class TaskHistory extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_events'; + + /** + * Maximum number of events + * + * @var integer + */ + const MAX_EVENTS = 5000; + + /** + * Create a new event + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $creator_id Author of the event (user id) + * @param string $event_name Task event name + * @return boolean + */ + public function create($project_id, $task_id, $creator_id, $event_name) + { + $values = array( + 'project_id' => $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 @@ <li><a href="#" id="filter-due-date"><?= t('Filter by due date') ?></a></li> <li><a href="?controller=project&action=search&project_id=<?= $current_project_id ?>"><?= t('Search') ?></a></li> <li><a href="?controller=project&action=tasks&project_id=<?= $current_project_id ?>"><?= t('Completed tasks') ?></a></li> + <li><a href="?controller=project&action=activity&project_id=<?= $current_project_id ?>"><?= t('Activity') ?></a></li> </ul> </div> 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 @@ +<p class="activity-title"> + <?= t('%s closed the task #%d', $author, $task_id) ?> +</p> +<p class="activity-description"> + <em><?= Helper\escape($task_title) ?></em> +</p>
\ 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 @@ +<p class="activity-title"> + <?= t('%s created the task #%d', $author, $task_id) ?> +</p> +<p class="activity-description"> + <em><?= Helper\escape($task_title) ?></em> +</p>
\ 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 @@ +<p class="activity-title"> + <?= t('%s moved the task #%d to the column %s', $author, $task_id, $task_column_name) ?> +</p> +<p class="activity-description"> + <em><?= Helper\escape($task_title) ?></em> +</p>
\ 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 @@ +<p class="activity-title"> + <?= t('%s moved the task #%d to the position %d in the column %s', $author, $task_id, $task_position, $task_column_name) ?> +</p> +<p class="activity-description"> + <em><?= Helper\escape($task_title) ?></em> +</p>
\ 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 @@ +<p class="activity-title"> + <?= t('%s open the task #%d', $author, $task_id) ?> +</p> +<p class="activity-description"> + <em><?= Helper\escape($task_title ?></em> +</p>
\ 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 @@ +<p class="activity-title"> + <?= t('%s updated the task #%d', $author, $task_id) ?> +</p> +<p class="activity-description"> + <em><?= Helper\escape($task_title) ?></em> +</p>
\ 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 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('%s\'s activity', $project['name']) ?></h2> + <ul> + <li><a href="?controller=board&action=show&project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li> + <li><a href="?controller=project&action=search&project_id=<?= $project['id'] ?>"><?= t('Search') ?></a></li> + <li><a href="?controller=project&action=tasks&project_id=<?= $project['id'] ?>"><?= t('Completed tasks') ?></a></li> + <li><a href="?controller=project&action=index"><?= t('List of projects') ?></a></li> + </ul> + </div> + <section> + <?php if (empty($events)): ?> + <p class="alert"><?= t('No activity.') ?></p> + <?php else: ?> + <?php foreach ($events as $event): ?> + <div class="activity-event"> + <p class="activity-datetime"> + <?php if ($event['event_type'] === 'task'): ?> + <i class="fa fa-newspaper-o"></i> + <?php endif ?> + <?= dt('%B %e, %Y at %k:%M %p', $event['date_creation']) ?> + </p> + <div class="activity-content"><?= $event['event_content'] ?></div> + </div> + <?php endforeach ?> + <?php endif ?> + </section> +</section>
\ 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 @@ <ul> <li><a href="?controller=board&action=show&project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li> <li><a href="?controller=project&action=tasks&project_id=<?= $project['id'] ?>"><?= t('Completed tasks') ?></a></li> + <li><a href="?controller=project&action=activity&project_id=<?= $project['id'] ?>"><?= t('Activity') ?></a></li> <li><a href="?controller=project&action=index"><?= t('List of projects') ?></a></li> </ul> </div> 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 @@ <ul> <li><a href="?controller=board&action=show&project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li> <li><a href="?controller=project&action=search&project_id=<?= $project['id'] ?>"><?= t('Search') ?></a></li> + <li><a href="?controller=project&action=activity&project_id=<?= $project['id'] ?>"><?= t('Activity') ?></a></li> <li><a href="?controller=project&action=index"><?= t('List of projects') ?></a></li> </ul> </div> |