diff --git a/app/Controller/ProjectCreationController.php b/app/Controller/ProjectCreationController.php index d07e73567..cfc7ffe8a 100644 --- a/app/Controller/ProjectCreationController.php +++ b/app/Controller/ProjectCreationController.php @@ -97,6 +97,7 @@ class ProjectCreationController extends BaseController 'name' => $values['name'], 'is_private' => $values['is_private'], 'identifier' => $values['identifier'], + 'per_swimlane_task_limits' => $values['per_swimlane_task_limits'], ); return $this->projectModel->create($project, $this->userSession->getId(), true); diff --git a/app/Formatter/BoardFormatter.php b/app/Formatter/BoardFormatter.php index 30e335f86..96f45ae11 100644 --- a/app/Formatter/BoardFormatter.php +++ b/app/Formatter/BoardFormatter.php @@ -3,6 +3,7 @@ namespace Kanboard\Formatter; use Kanboard\Core\Filter\FormatterInterface; +use Kanboard\Model\ProjectModel; use Kanboard\Model\SwimlaneModel; use Kanboard\Model\TaskModel; @@ -43,8 +44,16 @@ class BoardFormatter extends BaseFormatter implements FormatterInterface */ public function format() { + $project = $this->projectModel->getById($this->projectId); $swimlanes = $this->swimlaneModel->getAllByStatus($this->projectId, SwimlaneModel::ACTIVE); - $columns = $this->columnModel->getAllWithTaskCount($this->projectId); + if ($project['per_swimlane_task_limits']) { + $columns = array(); + foreach ($swimlanes as $swimlane) { + $columns = array_merge($columns, $this->columnModel->getAllWithPerSwimlaneTaskCount($this->projectId, $swimlane['id'])); + } + } else { + $columns = $this->columnModel->getAllWithTaskCount($this->projectId); + } if (empty($swimlanes) || empty($columns)) { return array(); diff --git a/app/Formatter/BoardSwimlaneFormatter.php b/app/Formatter/BoardSwimlaneFormatter.php index dbc2190fb..1ceef3be4 100644 --- a/app/Formatter/BoardSwimlaneFormatter.php +++ b/app/Formatter/BoardSwimlaneFormatter.php @@ -78,13 +78,16 @@ class BoardSwimlaneFormatter extends BaseFormatter implements FormatterInterface public function format() { $nb_swimlanes = count($this->swimlanes); - $nb_columns = count($this->columns); foreach ($this->swimlanes as &$swimlane) { + $columns = array_values(array_filter($this->columns, function($column) use ($swimlane) { + return !array_key_exists('swimlane_id', $column) || $column['swimlane_id'] == $swimlane['id']; + })); + $nb_columns = count($columns); $swimlane['id'] = (int) $swimlane['id']; $swimlane['columns'] = $this->boardColumnFormatter ->withSwimlaneId($swimlane['id']) - ->withColumns($this->columns) + ->withColumns($columns) ->withTasks($this->tasks) ->withTags($this->tags) ->format(); @@ -95,14 +98,12 @@ class BoardSwimlaneFormatter extends BaseFormatter implements FormatterInterface $swimlane['score'] = array_column_sum($swimlane['columns'], 'score'); $this->calculateStatsByColumnAcrossSwimlanes($swimlane['columns']); - } - foreach ($this->swimlanes as &$swimlane) { foreach ($swimlane['columns'] as $columnIndex => &$column) { $column['column_nb_tasks'] = $this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks']; $column['column_nb_score'] = $this->swimlanes[0]['columns'][$columnIndex]['column_score']; // add number of open tasks to each column, ignoring the current filter - $column['column_nb_open_tasks'] = $this->columns[array_search($column['id'], array_column($this->columns, 'id'))]['nb_open_tasks']; + $column['column_nb_open_tasks'] = $columns[array_search($column['id'], array_column($columns, 'id'))]['nb_open_tasks']; } } diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 87af56f83..95753b7d2 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1407,5 +1407,8 @@ return array( 'Automatically update the start date when the task is moved away from a specific column' => 'Atualizar automaticamente a data de início quando a tarefa sair de determinada coluna', 'HTTP Client:' => 'Cliente HTTP:', 'XBT - bitcoin' => 'XBT - bitcoin', + 'Task limits apply to each swimlane individually' => 'Limites de tarefas aplicam-se a cada raia individualmente', + 'Task limits are applied to each swimlane individually' => 'Limites de tarefas são aplicados a cada raia individualmente', + 'Task limits are applied across swimlanes' => 'Limites de tarefas são aplicados ao conjunto de todas as raias', // 'Assigned' => '', ); diff --git a/app/Model/ColumnModel.php b/app/Model/ColumnModel.php index 05f76fb9d..cc4ba0017 100644 --- a/app/Model/ColumnModel.php +++ b/app/Model/ColumnModel.php @@ -138,6 +138,24 @@ class ColumnModel extends Base ->findAll(); } + /** + * Get all columns with task count + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAllWithPerSwimlaneTaskCount($project_id, $swimlane_id) + { + return $this->db->table(self::TABLE) + ->columns('id', 'title', 'position', 'task_limit', 'description', 'hide_in_dashboard', 'project_id', $swimlane_id.' AS swimlane_id') + ->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE column_id=".self::TABLE.".id AND swimlane_id=".$swimlane_id." AND is_active='1'", 'nb_open_tasks') + ->subquery("SELECT COUNT(*) FROM ".TaskModel::TABLE." WHERE column_id=".self::TABLE.".id AND swimlane_id=".$swimlane_id." AND is_active='0'", 'nb_closed_tasks') + ->eq('project_id', $project_id) + ->asc('position') + ->findAll(); + } + /** * Get the list of columns sorted by position [ column_id => title ] * diff --git a/app/Model/ProjectDuplicationModel.php b/app/Model/ProjectDuplicationModel.php index 90a9f03d9..4f79e72ee 100644 --- a/app/Model/ProjectDuplicationModel.php +++ b/app/Model/ProjectDuplicationModel.php @@ -159,6 +159,7 @@ class ProjectDuplicationModel extends Base 'priority_default' => $project['priority_default'], 'priority_start' => $project['priority_start'], 'priority_end' => $project['priority_end'], + 'per_swimlane_task_limits' => empty($project['per_swimlane_task_limits']) ? 0 : 1, 'identifier' => $identifier, ); diff --git a/app/Model/ProjectModel.php b/app/Model/ProjectModel.php index 40e92de61..b6c113f73 100644 --- a/app/Model/ProjectModel.php +++ b/app/Model/ProjectModel.php @@ -457,6 +457,8 @@ class ProjectModel extends Base return false; } + $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')); return $this->exists($values['id']) && diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index f3ff6ed67..b759ece8d 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -8,7 +8,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 133; +const VERSION = 134; + +function version_134(PDO $pdo) +{ + $pdo->exec('ALTER TABLE `projects` ADD COLUMN `per_swimlane_task_limits` INT DEFAULT 0 NOT NULL'); +} function version_133(PDO $pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index fb78197d5..e2d710346 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -8,7 +8,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 111; +const VERSION = 112; + +function version_112(PDO $pdo) +{ + $pdo->exec('ALTER TABLE "projects" ADD COLUMN per_swimlane_task_limits BOOLEAN DEFAULT FALSE'); +} function version_111(PDO $pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index bc9ae01ea..a24b87227 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -8,7 +8,12 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 120; +const VERSION = 121; + +function version_121(PDO $pdo) +{ + $pdo->exec('ALTER TABLE projects ADD COLUMN per_swimlane_task_limits INTEGER DEFAULT 0 NOT NULL'); +} function version_120(PDO $pdo) { diff --git a/app/Template/project_creation/create.php b/app/Template/project_creation/create.php index 12eb32151..5d58daa14 100644 --- a/app/Template/project_creation/create.php +++ b/app/Template/project_creation/create.php @@ -14,6 +14,8 @@ form->text('identifier', $values, $errors, array('autofocus')) ?>

+ form->checkbox('per_swimlane_task_limits', t('Task limits apply to each swimlane individually'), 1, false) ?> + 1): ?> form->label(t('Create from another project'), 'src_project_id') ?> form->select('src_project_id', $projects_list, $values, array(), array(), 'js-project-creation-select-options') ?> diff --git a/app/Template/project_edit/show.php b/app/Template/project_edit/show.php index 4e50239b4..69bf860a5 100644 --- a/app/Template/project_edit/show.php +++ b/app/Template/project_edit/show.php @@ -26,6 +26,8 @@ form->label(t('Description'), 'description') ?> 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)) ?>
@@ -40,29 +42,29 @@
form->label(t('Project owner'), 'owner_id') ?> - form->select('owner_id', $owners, $values, $errors, array('tabindex="5"')) ?> + form->select('owner_id', $owners, $values, $errors, array('tabindex="6"')) ?>
- form->date(t('Start date'), 'start_date', $values, $errors, array('tabindex="6"')) ?> - form->date(t('End date'), 'end_date', $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->label(t('Default priority'), 'priority_default') ?> - form->number('priority_default', $values, $errors, array('tabindex="8"')) ?> + form->number('priority_default', $values, $errors, array('tabindex="9"')) ?> form->label(t('Lowest priority'), 'priority_start') ?> - form->number('priority_start', $values, $errors, array('tabindex="9"')) ?> + form->number('priority_start', $values, $errors, array('tabindex="10"')) ?> form->label(t('Highest priority'), 'priority_end') ?> - form->number('priority_end', $values, $errors, array('tabindex="10"')) ?> + form->number('priority_end', $values, $errors, array('tabindex="11"')) ?>
- modal->submitButtons(array('tabindex' => 11)) ?> + modal->submitButtons(array('tabindex' => 12)) ?> diff --git a/app/Template/project_view/show.php b/app/Template/project_view/show.php index 755b25c8e..d1a7c2258 100644 --- a/app/Template/project_view/show.php +++ b/app/Template/project_view/show.php @@ -31,6 +31,12 @@
  • dt->date($project['end_date']) ?>
  • + + +
  • + +
  • + diff --git a/tests/units/Model/ProjectDuplicationModelTest.php b/tests/units/Model/ProjectDuplicationModelTest.php index e704f981b..f12140706 100644 --- a/tests/units/Model/ProjectDuplicationModelTest.php +++ b/tests/units/Model/ProjectDuplicationModelTest.php @@ -643,4 +643,23 @@ class ProjectDuplicationModelTest extends Base $this->assertEquals(1, $filter['is_shared']); $this->assertEquals(0, $filter['append']); } + + public function testCloneProjectWithPerSwimlaneTaskLimits() + { + $projectModel = new ProjectModel($this->container); + $projectDuplicationModel = new ProjectDuplicationModel($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'With Per-Swimlane Task Limits'))); + $this->assertTrue($projectModel->update(array('id' => 1, 'per_swimlane_task_limits' => 1))); + + $project = $projectModel->getById(1); + $this->assertEquals(1, $project['per_swimlane_task_limits']); + + $this->assertEquals(2, $projectDuplicationModel->duplicate(1)); + + $project = $projectModel->getById(2); + $this->assertNotEmpty($project); + $this->assertEquals('With Per-Swimlane Task Limits (Clone)', $project['name']); + $this->assertEquals(1, $project['per_swimlane_task_limits']); + } } diff --git a/tests/units/Model/ProjectModelTest.php b/tests/units/Model/ProjectModelTest.php index 7958ef0b8..42cb9039b 100644 --- a/tests/units/Model/ProjectModelTest.php +++ b/tests/units/Model/ProjectModelTest.php @@ -43,6 +43,7 @@ class ProjectModelTest extends Base $this->assertEquals(1, $project['is_active']); $this->assertEquals(0, $project['is_public']); $this->assertEquals(0, $project['is_private']); + $this->assertEquals(0, $project['per_swimlane_task_limits']); $this->assertEquals(time(), $project['last_modified'], '', 1); $this->assertEmpty($project['token']); $this->assertEmpty($project['start_date']); @@ -200,6 +201,20 @@ class ProjectModelTest extends Base $this->assertEquals(0, $project['owner_id']); } + public function testUpdatePerSwimlaneTaskLimits() + { + $projectModel = new ProjectModel($this->container); + $this->assertEquals(1, $projectModel->create(array('name' => 'UnitTest'))); + + $project = $projectModel->getById(1); + $this->assertEquals(0, $project['per_swimlane_task_limits']); + + $this->assertTrue($projectModel->update(array('id'=> 1, 'per_swimlane_task_limits' => 1))); + + $project = $projectModel->getById(1); + $this->assertEquals(1, $project['per_swimlane_task_limits']); + } + public function testGetAllIds() { $projectModel = new ProjectModel($this->container);