From c8a617cfcb0bbca5e684e7635ccb9aa402c11a24 Mon Sep 17 00:00:00 2001 From: Andre Nathan Date: Wed, 26 Feb 2020 01:26:31 -0300 Subject: [PATCH] Add per-project and per-swimlane task limits This change allows projects and swimlanes to be configured with task limits that apply to their whole scope (i.e. all active tasks in a project or swimlane, respectively), as opposed to the usual per-column task limits. --- app/Controller/BaseController.php | 2 +- app/Controller/ProjectCreationController.php | 3 +- app/Controller/SwimlaneController.php | 2 +- app/Model/ProjectDuplicationModel.php | 1 + app/Model/ProjectModel.php | 37 ++++++++++++++++- app/Model/SwimlaneModel.php | 5 ++- app/Schema/Mysql.php | 12 +++++- app/Schema/Postgres.php | 12 +++++- app/Schema/Sqlite.php | 12 +++++- app/Template/board/table_container.php | 3 +- app/Template/board/table_swimlane.php | 6 ++- app/Template/board/table_tasks.php | 2 +- app/Template/header/title.php | 3 ++ app/Template/project_creation/create.php | 5 ++- app/Template/project_edit/show.php | 17 ++++---- app/Template/project_view/show.php | 6 ++- app/Template/swimlane/create.php | 3 ++ app/Template/swimlane/edit.php | 3 ++ app/Template/swimlane/table.php | 4 ++ tests/units/Model/ProjectModelTest.php | 43 +++++++++++++++++++- tests/units/Model/SwimlaneModelTest.php | 7 +++- 21 files changed, 161 insertions(+), 27 deletions(-) diff --git a/app/Controller/BaseController.php b/app/Controller/BaseController.php index 1dd7d3729..49f07e6b9 100644 --- a/app/Controller/BaseController.php +++ b/app/Controller/BaseController.php @@ -127,7 +127,7 @@ abstract class BaseController extends Base protected function getProject($project_id = 0) { $project_id = $this->request->getIntegerParam('project_id', $project_id); - $project = $this->projectModel->getByIdWithOwner($project_id); + $project = $this->projectModel->getByIdWithOwnerAndTaskCount($project_id); if (empty($project)) { throw new PageNotFoundException(); diff --git a/app/Controller/ProjectCreationController.php b/app/Controller/ProjectCreationController.php index cfc7ffe8a..ef49cb86f 100644 --- a/app/Controller/ProjectCreationController.php +++ b/app/Controller/ProjectCreationController.php @@ -97,7 +97,8 @@ class ProjectCreationController extends BaseController 'name' => $values['name'], 'is_private' => $values['is_private'], 'identifier' => $values['identifier'], - 'per_swimlane_task_limits' => $values['per_swimlane_task_limits'], + 'per_swimlane_task_limits' => array_key_exists('per_swimlane_task_limits', $values) ? $values['per_swimlane_task_limits'] : 0, + 'task_limit' => $values['task_limit'], ); return $this->projectModel->create($project, $this->userSession->getId(), true); diff --git a/app/Controller/SwimlaneController.php b/app/Controller/SwimlaneController.php index e6368b248..1a533ebbc 100644 --- a/app/Controller/SwimlaneController.php +++ b/app/Controller/SwimlaneController.php @@ -63,7 +63,7 @@ class SwimlaneController extends BaseController list($valid, $errors) = $this->swimlaneValidator->validateCreation($values); if ($valid) { - if ($this->swimlaneModel->create($project['id'], $values['name'], $values['description']) !== false) { + if ($this->swimlaneModel->create($project['id'], $values['name'], $values['description'], $values['task_limit']) !== false) { $this->flash->success(t('Your swimlane have been created successfully.')); $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id'])), true); return; diff --git a/app/Model/ProjectDuplicationModel.php b/app/Model/ProjectDuplicationModel.php index 4f79e72ee..a7de64fd8 100644 --- a/app/Model/ProjectDuplicationModel.php +++ b/app/Model/ProjectDuplicationModel.php @@ -160,6 +160,7 @@ class ProjectDuplicationModel extends Base 'priority_start' => $project['priority_start'], 'priority_end' => $project['priority_end'], 'per_swimlane_task_limits' => empty($project['per_swimlane_task_limits']) ? 0 : 1, + 'task_limit' => $project['task_limit'], 'identifier' => $identifier, ); diff --git a/app/Model/ProjectModel.php b/app/Model/ProjectModel.php index b6c113f73..74b91d172 100644 --- a/app/Model/ProjectModel.php +++ b/app/Model/ProjectModel.php @@ -79,6 +79,24 @@ class ProjectModel extends Base ->findOne(); } + /** + * Get a project by id with owner name and task count + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getByIdWithOwnerAndTaskCount($project_id) + { + return $this->db->table(self::TABLE) + ->columns(self::TABLE.'.*', UserModel::TABLE.'.username AS owner_username', UserModel::TABLE.'.name AS owner_name', 'SUM(CAST('.TaskModel::TABLE.'.is_active AS INTEGER)) AS nb_active_tasks') + ->eq(self::TABLE.'.id', $project_id) + ->join(UserModel::TABLE, 'id', 'owner_id') + ->join(TaskModel::TABLE, 'project_id', 'id') + ->groupBy(self::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name') + ->findOne(); + } + /** * Get a project by the name * @@ -372,7 +390,7 @@ class ProjectModel extends Base $values['identifier'] = strtoupper($values['identifier']); } - $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); + $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end', 'task_limit')); if (! $this->db->table(self::TABLE)->save($values)) { $this->db->cancelTransaction(); @@ -459,7 +477,7 @@ class ProjectModel extends Base $values['per_swimlane_task_limits'] = empty($values['per_swimlane_task_limits']) ? 0 : 1; - $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end')); + $this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end', 'task_limit')); return $this->exists($values['id']) && $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); @@ -568,4 +586,19 @@ class ProjectModel extends Base ->eq('id', $project_id) ->save(array('is_public' => 0, 'token' => '')); } + + /** + * Return the task count for a project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function taskCount($project_id) + { + return $this->db->table(self::TABLE) + ->eq('id', $project_id)->exists() + ->join(ColumnModel::TABLE, 'id', 'project_id') + ->count(); + } } diff --git a/app/Model/SwimlaneModel.php b/app/Model/SwimlaneModel.php index 0d204ae28..869aec311 100644 --- a/app/Model/SwimlaneModel.php +++ b/app/Model/SwimlaneModel.php @@ -178,7 +178,7 @@ class SwimlaneModel extends Base ); $swimlanes = $this->db->table(self::TABLE) - ->columns('id', 'name', 'description', 'project_id', 'position', 'is_active') + ->columns('id', 'name', 'description', 'project_id', 'position', 'is_active', 'task_limit') ->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE swimlane_id=".self::TABLE.".id AND is_active='1'", 'nb_open_tasks') ->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE swimlane_id=".self::TABLE.".id AND is_active='0'", 'nb_closed_tasks') ->eq('project_id', $project_id) @@ -231,7 +231,7 @@ class SwimlaneModel extends Base * @param string $description * @return bool|int */ - public function create($projectId, $name, $description = '') + public function create($projectId, $name, $description = '', $task_limit = 0) { if (! $this->projectModel->exists($projectId)) { return 0; @@ -243,6 +243,7 @@ class SwimlaneModel extends Base 'description' => $description, 'position' => $this->getLastPosition($projectId), 'is_active' => 1, + 'task_limit' => $task_limit, )); } diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index b759ece8d..62a28d34d 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -8,7 +8,17 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 134; +const VERSION = 136; + +function version_136(PDO $pdo) +{ + $pdo->exec('ALTER TABLE `swimlanes` ADD COLUMN `task_limit` INT DEFAULT 0'); +} + +function version_135(PDO $pdo) +{ + $pdo->exec('ALTER TABLE `projects` ADD COLUMN `task_limit` INT DEFAULT 0'); +} function version_134(PDO $pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index e2d710346..cbb885067 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -8,7 +8,17 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 112; +const VERSION = 114; + +function version_114(PDO $pdo) +{ + $pdo->exec('ALTER TABLE "swimlanes" ADD COLUMN task_limit INTEGER DEFAULT 0'); +} + +function version_113(PDO $pdo) +{ + $pdo->exec('ALTER TABLE "projects" ADD COLUMN task_limit INTEGER DEFAULT 0'); +} function version_112(PDO $pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index a24b87227..964d3032b 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -8,7 +8,17 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 121; +const VERSION = 123; + +function version_123(PDO $pdo) +{ + $pdo->exec('ALTER TABLE swimlanes ADD COLUMN task_limit INTEGER DEFAULT 0'); +} + +function version_122(PDO $pdo) +{ + $pdo->exec('ALTER TABLE projects ADD COLUMN task_limit INTEGER DEFAULT 0'); +} function version_121(PDO $pdo) { diff --git a/app/Template/board/table_container.php b/app/Template/board/table_container.php index a93e70013..bfa98eda9 100644 --- a/app/Template/board/table_container.php +++ b/app/Template/board/table_container.php @@ -1,4 +1,5 @@ -
+

diff --git a/app/Template/board/table_swimlane.php b/app/Template/board/table_swimlane.php index 33c1cf26f..d58dbf43d 100644 --- a/app/Template/board/table_swimlane.php +++ b/app/Template/board/table_swimlane.php @@ -15,7 +15,11 @@ - () + + (/) + + () + diff --git a/app/Template/board/table_tasks.php b/app/Template/board/table_tasks.php index 1b846c731..f0defe21b 100644 --- a/app/Template/board/table_tasks.php +++ b/app/Template/board/table_tasks.php @@ -1,5 +1,5 @@ - + ">text->e($project['task_limit']) ?>) + diff --git a/app/Template/project_creation/create.php b/app/Template/project_creation/create.php index 5d58daa14..a9bfffd91 100644 --- a/app/Template/project_creation/create.php +++ b/app/Template/project_creation/create.php @@ -14,7 +14,10 @@ form->text('identifier', $values, $errors, array('autofocus')) ?>

- form->checkbox('per_swimlane_task_limits', t('Task limits apply to each swimlane individually'), 1, false) ?> + form->checkbox('per_swimlane_task_limits', t('Column task limits apply to each swimlane individually'), 1, false) ?> + + form->label(t('Task limit'), 'task_limit') ?> + form->number('task_limit', $values, $errors) ?> 1): ?> form->label(t('Create from another project'), 'src_project_id') ?> diff --git a/app/Template/project_edit/show.php b/app/Template/project_edit/show.php index 69bf860a5..d27b80a38 100644 --- a/app/Template/project_edit/show.php +++ b/app/Template/project_edit/show.php @@ -28,6 +28,9 @@ form->textEditor('description', $values, $errors, array('tabindex' => 4)) ?> form->checkbox('per_swimlane_task_limits', t('Task limits apply to each swimlane individually'), 1, $project['per_swimlane_task_limits'] == 1, '', array('tabindex' => 5)) ?> + + form->label(t('Task limit'), 'task_limit') ?> + form->number('task_limit', $values, $errors, array('tabindex' => 6)) ?>
@@ -42,29 +45,29 @@
form->label(t('Project owner'), 'owner_id') ?> - form->select('owner_id', $owners, $values, $errors, array('tabindex="6"')) ?> + form->select('owner_id', $owners, $values, $errors, array('tabindex="7"')) ?>
- form->date(t('Start date'), 'start_date', $values, $errors, array('tabindex="7"')) ?> - form->date(t('End date'), 'end_date', $values, $errors, array('tabindex="8"')) ?> + form->date(t('Start date'), 'start_date', $values, $errors, array('tabindex="8"')) ?> + form->date(t('End date'), 'end_date', $values, $errors, array('tabindex="9"')) ?>
form->label(t('Default priority'), 'priority_default') ?> - form->number('priority_default', $values, $errors, array('tabindex="9"')) ?> + form->number('priority_default', $values, $errors, array('tabindex="10"')) ?> form->label(t('Lowest priority'), 'priority_start') ?> - form->number('priority_start', $values, $errors, array('tabindex="10"')) ?> + form->number('priority_start', $values, $errors, array('tabindex="11"')) ?> form->label(t('Highest priority'), 'priority_end') ?> - form->number('priority_end', $values, $errors, array('tabindex="11"')) ?> + form->number('priority_end', $values, $errors, array('tabindex="12"')) ?>
- modal->submitButtons(array('tabindex' => 12)) ?> + modal->submitButtons(array('tabindex' => 13)) ?> diff --git a/app/Template/project_view/show.php b/app/Template/project_view/show.php index d1a7c2258..45785c2aa 100644 --- a/app/Template/project_view/show.php +++ b/app/Template/project_view/show.php @@ -33,10 +33,12 @@ -
  • +
  • -
  • +
  • + +
  • diff --git a/app/Template/swimlane/create.php b/app/Template/swimlane/create.php index b769c6ce7..3a6519613 100644 --- a/app/Template/swimlane/create.php +++ b/app/Template/swimlane/create.php @@ -10,5 +10,8 @@ form->label(t('Description'), 'description') ?> form->textEditor('description', $values, $errors, array('tabindex' => 2)) ?> + form->label(t('Task limit'), 'task_limit') ?> + form->number('task_limit', $values, $errors, array('tabindex' => 3)) ?> + modal->submitButtons() ?> diff --git a/app/Template/swimlane/edit.php b/app/Template/swimlane/edit.php index f15a6dfb0..ebe6a3458 100644 --- a/app/Template/swimlane/edit.php +++ b/app/Template/swimlane/edit.php @@ -11,5 +11,8 @@ form->label(t('Description'), 'description') ?> form->textEditor('description', $values, $errors, array('tabindex' => 2)) ?> + form->label(t('Task limit'), 'task_limit') ?> + form->number('task_limit', $values, $errors, array('tabindex' => 3)) ?> + modal->submitButtons() ?> diff --git a/app/Template/swimlane/table.php b/app/Template/swimlane/table.php index eb8f591ff..c8aaa7e1c 100644 --- a/app/Template/swimlane/table.php +++ b/app/Template/swimlane/table.php @@ -4,6 +4,7 @@ + @@ -43,6 +44,9 @@ app->tooltipMarkdown($swimlane['description']) ?> + + 0 ? $swimlane['task_limit'] : '∞' ?> + diff --git a/tests/units/Model/ProjectModelTest.php b/tests/units/Model/ProjectModelTest.php index 42cb9039b..372a5e0b5 100644 --- a/tests/units/Model/ProjectModelTest.php +++ b/tests/units/Model/ProjectModelTest.php @@ -44,6 +44,7 @@ class ProjectModelTest extends Base $this->assertEquals(0, $project['is_public']); $this->assertEquals(0, $project['is_private']); $this->assertEquals(0, $project['per_swimlane_task_limits']); + $this->assertEquals(0, $project['task_limit']); $this->assertEquals(time(), $project['last_modified'], '', 1); $this->assertEmpty($project['token']); $this->assertEmpty($project['start_date']); @@ -65,6 +66,17 @@ class ProjectModelTest extends Base $this->assertEquals(0, $project['owner_id']); } + public function testCreationWithTaskLimit() + { + $projectModel = new ProjectModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest', 'task_limit' => 3))); + + $project = $projectModel->getById(1); + $this->assertNotEmpty($project); + $this->assertEquals(3, $project['task_limit']); + } + public function testProjectDate() { $projectModel = new ProjectModel($this->container); @@ -162,6 +174,14 @@ class ProjectModelTest extends Base $this->assertEmpty($categories); } + public function testCreationWithBlankTaskLimit() + { + $projectModel = new ProjectModel($this->container); + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest1', 'task_limit' => ''))); + $project = $projectModel->getById(1); + $this->assertEquals(0, $project['task_limit']); + } + public function testUpdateLastModifiedDate() { $projectModel = new ProjectModel($this->container); @@ -215,6 +235,20 @@ class ProjectModelTest extends Base $this->assertEquals(1, $project['per_swimlane_task_limits']); } + public function testUpdateTaskLimit() + { + $projectModel = new ProjectModel($this->container); + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + + $project = $projectModel->getById(1); + $this->assertEquals(0, $project['task_limit']); + + $this->assertTrue($projectModel->update(array('id'=> 1, 'task_limit' => 1))); + + $project = $projectModel->getById(1); + $this->assertEquals(1, $project['task_limit']); + } + public function testGetAllIds() { $projectModel = new ProjectModel($this->container); @@ -453,24 +487,29 @@ class ProjectModelTest extends Base { $projectModel = new ProjectModel($this->container); $userModel = new UserModel($this->container); + $taskCreationModel = new TaskCreationModel($this->container); $this->assertEquals(2, $userModel->create(array('username' => 'user1', 'name' => 'Me'))); $this->assertEquals(1, $projectModel->create(array('name' => 'My project 1'), 2)); $this->assertEquals(2, $projectModel->create(array('name' => 'My project 2'))); + $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1))); - $project = $projectModel->getByIdWithOwner(1); + $project = $projectModel->getByIdWithOwnerAndTaskCount(1); $this->assertNotEmpty($project); $this->assertSame('My project 1', $project['name']); $this->assertSame('Me', $project['owner_name']); $this->assertSame('user1', $project['owner_username']); $this->assertEquals(2, $project['owner_id']); + $this->assertEquals(2, $project['nb_active_tasks']); - $project = $projectModel->getByIdWithOwner(2); + $project = $projectModel->getByIdWithOwnerAndTaskCount(2); $this->assertNotEmpty($project); $this->assertSame('My project 2', $project['name']); $this->assertEquals('', $project['owner_name']); $this->assertEquals('', $project['owner_username']); $this->assertEquals(0, $project['owner_id']); + $this->assertEquals(0, $project['nb_active_tasks']); } public function testGetList() diff --git a/tests/units/Model/SwimlaneModelTest.php b/tests/units/Model/SwimlaneModelTest.php index 6bcc3bbf7..e1a85bf14 100644 --- a/tests/units/Model/SwimlaneModelTest.php +++ b/tests/units/Model/SwimlaneModelTest.php @@ -14,13 +14,15 @@ class SwimlaneModelTest extends Base $swimlaneModel = new SwimlaneModel($this->container); $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); - $this->assertEquals(2, $swimlaneModel->create(1, 'Swimlane #1')); + $this->assertEquals(2, $swimlaneModel->create(1, 'Swimlane #1', '', 1)); $swimlanes = $swimlaneModel->getAll(1); $this->assertNotEmpty($swimlanes); $this->assertEquals(2, count($swimlanes)); $this->assertEquals('Default swimlane', $swimlanes[0]['name']); $this->assertEquals('Swimlane #1', $swimlanes[1]['name']); + $this->assertEquals(0, $swimlanes[0]['task_limit']); + $this->assertEquals(1, $swimlanes[1]['task_limit']); $this->assertEquals(2, $swimlaneModel->getIdByName(1, 'Swimlane #1')); $this->assertEquals(0, $swimlaneModel->getIdByName(2, 'Swimlane #2')); @@ -85,10 +87,11 @@ class SwimlaneModelTest extends Base $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); $this->assertEquals(2, $swimlaneModel->create(1, 'Swimlane #1')); - $this->assertTrue($swimlaneModel->update(2, array('name' => 'foobar'))); + $this->assertTrue($swimlaneModel->update(2, array('name' => 'foobar', 'task_limit' => 1))); $swimlane = $swimlaneModel->getById(2); $this->assertEquals('foobar', $swimlane['name']); + $this->assertEquals(1, $swimlane['task_limit']); } public function testDisableEnable()