Add pagination/column sorting for search and completed tasks

This commit is contained in:
Frédéric Guillot 2014-10-12 14:28:08 -04:00
parent deeebd8e72
commit b7060b33ef
14 changed files with 329 additions and 275 deletions

View File

@ -30,6 +30,7 @@ use Model\LastLogin;
* @property \Model\Task $task
* @property \Model\TaskHistory $taskHistory
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskPermission $taskPermission
* @property \Model\TaskValidator $taskValidator
* @property \Model\CommentHistory $commentHistory

View File

@ -395,26 +395,31 @@ class Project extends Base
{
$project = $this->getProject();
$search = $this->request->getStringParam('search');
$direction = $this->request->getStringParam('direction', 'DESC');
$order = $this->request->getStringParam('order', 'tasks.id');
$offset = $this->request->getIntegerParam('offset', 0);
$tasks = array();
$nb_tasks = 0;
$limit = 25;
if ($search !== '') {
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => $project['id']),
'or' => array(
array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'),
//array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'),
)
);
$tasks = $this->task->find($filters);
$nb_tasks = count($tasks);
$tasks = $this->taskFinder->search($project['id'], $search, $offset, $limit, $order, $direction);
$nb_tasks = $this->taskFinder->countSearch($project['id'], $search);
}
$this->response->html($this->template->layout('project_search', array(
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'pagination' => array(
'controller' => 'project',
'action' => 'search',
'params' => array('search' => $search, 'project_id' => $project['id']),
'direction' => $direction,
'order' => $order,
'total' => $nb_tasks,
'offset' => $offset,
'limit' => $limit,
),
'values' => array(
'search' => $search,
'controller' => 'project',
@ -436,16 +441,25 @@ class Project extends Base
public function tasks()
{
$project = $this->getProject();
$direction = $this->request->getStringParam('direction', 'DESC');
$order = $this->request->getStringParam('order', 'tasks.date_completed');
$offset = $this->request->getIntegerParam('offset', 0);
$limit = 25;
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => $project['id']),
array('column' => 'is_active', 'operator' => 'eq', 'value' => TaskModel::STATUS_CLOSED),
);
$tasks = $this->task->find($filters);
$nb_tasks = count($tasks);
$tasks = $this->taskFinder->getClosedTasks($project['id'], $offset, $limit, $order, $direction);
$nb_tasks = $this->task->countByProjectId($project['id'], array(TaskModel::STATUS_CLOSED));
$this->response->html($this->template->layout('project_tasks', array(
'pagination' => array(
'controller' => 'project',
'action' => 'tasks',
'params' => array('project_id' => $project['id']),
'direction' => $direction,
'order' => $order,
'total' => $nb_tasks,
'offset' => $offset,
'limit' => $limit,
),
'project' => $project,
'columns' => $this->board->getColumnsList($project['id']),
'categories' => $this->category->getList($project['id'], false),

View File

@ -365,7 +365,7 @@ class Task extends Base
if ($this->request->getStringParam('confirmation') === 'yes') {
$this->checkCSRFParam();
$task_id = $this->task->duplicateSameProject($task);
$task_id = $this->task->duplicateToSameProject($task);
if ($task_id) {
$this->session->flash(t('Task created successfully.'));

View File

@ -32,6 +32,7 @@ use PicoDb\Database;
* @property \Model\SubtaskHistory $subtaskHistory
* @property \Model\Task $task
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskHistory $taskHistory
* @property \Model\TaskValidator $taskValidator
* @property \Model\TimeTracking $timeTracking

View File

@ -234,14 +234,8 @@ class Board extends Base
*/
public function get($project_id, array $filters = array())
{
$this->db->startTransaction();
$columns = $this->getColumns($project_id);
$filters[] = array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id);
$filters[] = array('column' => 'is_active', 'operator' => 'eq', 'value' => Task::STATUS_OPEN);
$tasks = $this->task->find($filters);
$tasks = $this->taskFinder->getOpenTasks($project_id);
foreach ($columns as &$column) {
@ -254,8 +248,6 @@ class Board extends Base
}
}
$this->db->closeTransaction();
return $columns;
}

View File

@ -146,7 +146,7 @@ class Task extends Base
}
/**
* Count all tasks for a given project and status
* Get all tasks for a given project and status
*
* @access public
* @param integer $project_id Project id
@ -198,166 +198,6 @@ class Task extends Base
->count();
}
/**
* Get tasks that match defined filters
*
* @access public
* @param array $filters Filters: [ ['column' => '...', 'operator' => '...', 'value' => '...'], ... ]
* @param array $sorting Sorting: [ 'column' => 'date_creation', 'direction' => 'asc']
* @return array
*/
public function find(array $filters, array $sorting = array())
{
$table = $this->db
->table(self::TABLE)
->columns(
'(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments',
'(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',
'tasks.id',
'tasks.reference',
'tasks.title',
'tasks.description',
'tasks.date_creation',
'tasks.date_modification',
'tasks.date_completed',
'tasks.date_due',
'tasks.color_id',
'tasks.project_id',
'tasks.column_id',
'tasks.owner_id',
'tasks.creator_id',
'tasks.position',
'tasks.is_active',
'tasks.score',
'tasks.category_id',
'users.username AS assignee_username',
'users.name AS assignee_name'
)
->join(User::TABLE, 'id', 'owner_id');
foreach ($filters as $key => $filter) {
if ($key === 'or') {
$table->beginOr();
foreach ($filter as $subfilter) {
$table->$subfilter['operator']($subfilter['column'], $subfilter['value']);
}
$table->closeOr();
}
else if (isset($filter['operator']) && isset($filter['column']) && isset($filter['value'])) {
$table->$filter['operator']($filter['column'], $filter['value']);
}
}
if (empty($sorting)) {
$table->orderBy('tasks.position', 'ASC');
}
else {
$table->orderBy($sorting['column'], $sorting['direction']);
}
return $table->findAll();
}
/**
* Generic method to duplicate a task
*
* @access public
* @param array $task Task data
* @param array $override Task properties to override
* @return integer|boolean
*/
public function copy(array $task, array $override = array())
{
// Values to override
if (! empty($override)) {
$task = $override + $task;
}
$this->db->startTransaction();
// Assign new values
$values = array();
$values['title'] = $task['title'];
$values['description'] = $task['description'];
$values['date_creation'] = time();
$values['date_modification'] = $values['date_creation'];
$values['date_due'] = $task['date_due'];
$values['color_id'] = $task['color_id'];
$values['project_id'] = $task['project_id'];
$values['column_id'] = $task['column_id'];
$values['owner_id'] = 0;
$values['creator_id'] = $task['creator_id'];
$values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']) + 1;
$values['score'] = $task['score'];
$values['category_id'] = 0;
// Check if the assigned user is allowed for the new project
if ($task['owner_id'] && $this->projectPermission->isUserAllowed($values['project_id'], $task['owner_id'])) {
$values['owner_id'] = $task['owner_id'];
}
// Check if the category exists
if ($task['category_id'] && $this->category->exists($task['category_id'], $task['project_id'])) {
$values['category_id'] = $task['category_id'];
}
// Save task
if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
}
$task_id = $this->db->getConnection()->getLastId();
// Duplicate subtasks
if (! $this->subTask->duplicate($task['id'], $task_id)) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
// Trigger events
$this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
$this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values);
return $task_id;
}
/**
* Duplicate a task to the same project
*
* @access public
* @param array $task Task data
* @return integer|boolean
*/
public function duplicateSameProject($task)
{
return $this->copy($task);
}
/**
* Duplicate a task to another project (always copy to the first column)
*
* @access public
* @param integer $project_id Destination project id
* @param array $task Task data
* @return integer|boolean
*/
public function duplicateToAnotherProject($project_id, array $task)
{
return $this->copy($task, array(
'project_id' => $project_id,
'column_id' => $this->board->getFirstColumn($project_id),
));
}
/**
* Prepare data before task creation or modification
*
@ -714,6 +554,100 @@ class Task extends Base
return false;
}
/**
* Generic method to duplicate a task
*
* @access public
* @param array $task Task data
* @param array $override Task properties to override
* @return integer|boolean
*/
public function copy(array $task, array $override = array())
{
// Values to override
if (! empty($override)) {
$task = $override + $task;
}
$this->db->startTransaction();
// Assign new values
$values = array();
$values['title'] = $task['title'];
$values['description'] = $task['description'];
$values['date_creation'] = time();
$values['date_modification'] = $values['date_creation'];
$values['date_due'] = $task['date_due'];
$values['color_id'] = $task['color_id'];
$values['project_id'] = $task['project_id'];
$values['column_id'] = $task['column_id'];
$values['owner_id'] = 0;
$values['creator_id'] = $task['creator_id'];
$values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']) + 1;
$values['score'] = $task['score'];
$values['category_id'] = 0;
// Check if the assigned user is allowed for the new project
if ($task['owner_id'] && $this->projectPermission->isUserAllowed($values['project_id'], $task['owner_id'])) {
$values['owner_id'] = $task['owner_id'];
}
// Check if the category exists
if ($task['category_id'] && $this->category->exists($task['category_id'], $task['project_id'])) {
$values['category_id'] = $task['category_id'];
}
// Save task
if (! $this->db->table(Task::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
}
$task_id = $this->db->getConnection()->getLastId();
// Duplicate subtasks
if (! $this->subTask->duplicate($task['id'], $task_id)) {
$this->db->cancelTransaction();
return false;
}
$this->db->closeTransaction();
// Trigger events
$this->event->trigger(Task::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
$this->event->trigger(Task::EVENT_CREATE, array('task_id' => $task_id) + $values);
return $task_id;
}
/**
* Duplicate a task to the same project
*
* @access public
* @param array $task Task data
* @return integer|boolean
*/
public function duplicateToSameProject($task)
{
return $this->copy($task);
}
/**
* Duplicate a task to another project (always copy to the first column)
*
* @access public
* @param integer $project_id Destination project id
* @param array $task Task data
* @return integer|boolean
*/
public function duplicateToAnotherProject($project_id, array $task)
{
return $this->copy($task, array(
'project_id' => $project_id,
'column_id' => $this->board->getFirstColumn($project_id),
));
}
/**
* Get a the task id from a text
*

83
app/Model/TaskFinder.php Normal file
View File

@ -0,0 +1,83 @@
<?php
namespace Model;
/**
* Task Finder model
*
* @package model
* @author Frederic Guillot
*/
class TaskFinder extends Base
{
private function prepareRequest()
{
return $this->db
->table(Task::TABLE)
->columns(
'(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments',
'(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',
'tasks.id',
'tasks.reference',
'tasks.title',
'tasks.description',
'tasks.date_creation',
'tasks.date_modification',
'tasks.date_completed',
'tasks.date_due',
'tasks.color_id',
'tasks.project_id',
'tasks.column_id',
'tasks.owner_id',
'tasks.creator_id',
'tasks.position',
'tasks.is_active',
'tasks.score',
'tasks.category_id',
'users.username AS assignee_username',
'users.name AS assignee_name'
)
->join(User::TABLE, 'id', 'owner_id');
}
public function search($project_id, $search, $offset = 0, $limit = 25, $column = 'tasks.id', $direction = 'DESC')
{
return $this->prepareRequest()
->eq('project_id', $project_id)
->like('title', '%'.$search.'%')
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
}
public function countSearch($project_id, $search)
{
return $this->db->table(Task::TABLE)
->eq('project_id', $project_id)
->like('title', '%'.$search.'%')
->count();
}
public function getClosedTasks($project_id, $offset = 0, $limit = 25, $column = 'tasks.date_completed', $direction = 'DESC')
{
return $this->prepareRequest()
->eq('project_id', $project_id)
->eq('is_active', Task::STATUS_CLOSED)
->offset($offset)
->limit($limit)
->orderBy($column, $direction)
->findAll();
}
public function getOpenTasks($project_id, $column = 'tasks.position', $direction = 'ASC')
{
return $this->prepareRequest()
->eq('project_id', $project_id)
->eq('is_active', Task::STATUS_OPEN)
->orderBy($column, $direction)
->findAll();
}
}

View File

@ -7,10 +7,10 @@
<?php endif ?>
</h2>
<ul>
<li><a href="?controller=board&amp;action=show&amp;project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li>
<li><a href="?controller=project&amp;action=tasks&amp;project_id=<?= $project['id'] ?>"><?= t('Completed tasks') ?></a></li>
<li><a href="?controller=project&amp;action=activity&amp;project_id=<?= $project['id'] ?>"><?= t('Activity') ?></a></li>
<li><a href="?controller=project&amp;action=index"><?= t('List of projects') ?></a></li>
<li><?= Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?></li>
<li><?= Helper\a(t('Completed tasks'), 'project', 'tasks', array('project_id' => $project['id'])) ?></li>
<li><?= Helper\a(t('Activity'), 'project', 'activity', array('project_id' => $project['id'])) ?></li>
<li><?= Helper\a(t('List of projects'), 'project', 'index') ?></li>
</ul>
</div>
<section>
@ -25,7 +25,12 @@
<?php if (empty($tasks) && ! empty($values['search'])): ?>
<p class="alert"><?= t('Nothing found.') ?></p>
<?php elseif (! empty($tasks)): ?>
<?= Helper\template('task_table', array('tasks' => $tasks, 'categories' => $categories, 'columns' => $columns)) ?>
<?= Helper\template('task_table', array(
'tasks' => $tasks,
'categories' => $categories,
'columns' => $columns,
'pagination' => $pagination,
)) ?>
<?php endif ?>
</section>

View File

@ -12,7 +12,12 @@
<?php if (empty($tasks)): ?>
<p class="alert"><?= t('No task') ?></p>
<?php else: ?>
<?= Helper\template('task_table', array('tasks' => $tasks, 'categories' => $categories, 'columns' => $columns)) ?>
<?= Helper\template('task_table', array(
'tasks' => $tasks,
'categories' => $categories,
'columns' => $columns,
'pagination' => $pagination,
)) ?>
<?php endif ?>
</section>
</section>

View File

@ -1,14 +1,14 @@
<table>
<tr>
<th><?= t('Id') ?></th>
<th><?= t('Column') ?></th>
<th><?= t('Category') ?></th>
<th><?= t('Title') ?></th>
<th><?= t('Assignee') ?></th>
<th><?= t('Due date') ?></th>
<th><?= t('Date created') ?></th>
<th><?= t('Date completed') ?></th>
<th><?= t('Status') ?></th>
<th><?= Helper\order(t('Id'), 'tasks.id', $pagination) ?></th>
<th><?= Helper\order(t('Column'), 'tasks.column_id', $pagination) ?></th>
<th><?= Helper\order(t('Category'), 'tasks.category_id', $pagination) ?></th>
<th><?= Helper\order(t('Title'), 'tasks.title', $pagination) ?></th>
<th><?= Helper\order(t('Assignee'), 'users.username', $pagination) ?></th>
<th><?= Helper\order(t('Due date'), 'tasks.date_due', $pagination) ?></th>
<th><?= Helper\order(t('Date created'), 'tasks.date_creation', $pagination) ?></th>
<th><?= Helper\order(t('Date completed'), 'tasks.date_completed', $pagination) ?></th>
<th><?= Helper\order(t('Status'), 'tasks.is_active', $pagination) ?></th>
</tr>
<?php foreach ($tasks as $task): ?>
<tr>
@ -51,4 +51,6 @@
</td>
</tr>
<?php endforeach ?>
</table>
</table>
<?= Helper\paginate($pagination) ?>

View File

@ -590,3 +590,65 @@ function u($controller, $action, array $params = array(), $csrf = false)
return $html;
}
/**
* Pagination links
*
* @param array $pagination Pagination information
* @return string
*/
function paginate(array $pagination)
{
extract($pagination);
$html = '<div id="pagination">';
$html .= '<span id="pagination-previous">';
if ($pagination['offset'] > 0) {
$offset = $pagination['offset'] - $limit;
$html .= a('&larr; '.t('Previous'), $controller, $action, $params + compact('offset', 'order', 'direction'));
}
else {
$html .= '&larr; '.t('Previous');
}
$html .= '</span>';
$html .= '<span id="pagination-next">';
if (($total - $pagination['offset']) > $limit) {
$offset = $pagination['offset'] + $limit;
$html .= a(t('Next').' &rarr;', $controller, $action, $params + compact('offset', 'order', 'direction'));
}
else {
$html .= t('Next').' &rarr;';
}
$html .= '</span>';
$html .= '</div>';
return $html;
}
/**
* Column sorting (work with pagination)
*
* @param string $label Column title
* @param string $column SQL column name
* @param array $pagination Pagination information
* @return string
*/
function order($label, $column, array $pagination)
{
extract($pagination);
$prefix = '';
if ($order === $column) {
$prefix = $direction === 'DESC' ? '&#9660; ' : '&#9650; ';
$direction = $direction === 'DESC' ? 'ASC' : 'DESC';
}
$order = $column;
return $prefix.a($label, $controller, $action, $params + compact('offset', 'order', 'direction'));
}

View File

@ -125,6 +125,16 @@ td li {
background: rgb(219, 235, 255)
}
th a {
text-decoration: none;
color: #333;
}
th a:focus,
th a:hover {
text-decoration: underline;
}
/* forms */
form {
padding: 10px;
@ -1129,6 +1139,19 @@ tr td.task-orange,
font-size: 0.8em;
}
/* pagination */
#pagination {
text-align: center;
}
#pagination-next {
margin-left: 5px;
}
#pagination-previous {
margin-right: 5px;
}
/* responsive design */
@media only screen and (min-width : 768px) and (max-width : 1024px) {

View File

@ -19,6 +19,7 @@ foreach (array(1, 2, 3, 4) as $column_id) {
'owner_id' => rand(0, 1),
'color_id' => rand(0, 1) === 0 ? 'green' : 'purple',
'score' => rand(0, 21),
'is_active' => rand(0, 1),
);
$taskModel->create($task);

View File

@ -493,75 +493,6 @@ class TaskTest extends Base
$this->assertEquals($task_per_column + 1, $t->countByColumnId(1, 4));
}
public function testFilter()
{
$t = new Task($this->registry);
$p = new Project($this->registry);
$this->assertEquals(1, $p->create(array('name' => 'test1')));
$this->assertEquals(1, $t->create(array('title' => 'test a', 'project_id' => 1, 'column_id' => 3, 'owner_id' => 1, 'description' => 'biloute')));
$this->assertEquals(2, $t->create(array('title' => 'test b', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2, 'description' => 'toto et titi sont dans un bateau')));
$tasks = $t->find(array(array('column' => 'project_id', 'operator' => 'eq', 'value' => '1')));
$this->assertNotFalse($tasks);
$this->assertEquals(2, count($tasks));
$this->assertEquals(1, $tasks[0]['id']);
$this->assertEquals(2, $tasks[1]['id']);
$tasks = $t->find(array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => '1'),
array('column' => 'owner_id', 'operator' => 'eq', 'value' => '2'),
));
$this->assertEquals(1, count($tasks));
$this->assertEquals(2, $tasks[0]['id']);
$tasks = $t->find(array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => '1'),
array('column' => 'title', 'operator' => 'like', 'value' => '%b%'),
));
$this->assertEquals(1, count($tasks));
$this->assertEquals(2, $tasks[0]['id']);
// Condition with OR
$search = 'bateau';
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => 1),
'or' => array(
array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'),
array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'),
)
);
$tasks = $t->find($filters);
$this->assertEquals(1, count($tasks));
$this->assertEquals(2, $tasks[0]['id']);
$search = 'toto et titi';
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => 1),
'or' => array(
array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'),
array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'),
)
);
$tasks = $t->find($filters);
$this->assertEquals(1, count($tasks));
$this->assertEquals(2, $tasks[0]['id']);
$search = 'john';
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => 1),
'or' => array(
array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'),
array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'),
)
);
$tasks = $t->find($filters);
$this->assertEquals(0, count($tasks));
}
public function testDuplicateToTheSameProject()
{
$t = new Task($this->registry);
@ -584,7 +515,7 @@ class TaskTest extends Base
$this->assertEquals(1, $task['position']);
// We duplicate our task
$this->assertEquals(2, $t->duplicateSameProject($task));
$this->assertEquals(2, $t->duplicateToSameProject($task));
$this->assertTrue($this->registry->shared('event')->isEventTriggered(Task::EVENT_CREATE));
// Check the values of the duplicated task