Allow task limits to be applied per-swimlane
This commit is contained in:
committed by
Frédéric Guillot
parent
0a6f614571
commit
e59ab08af3
@@ -97,6 +97,7 @@ class ProjectCreationController extends BaseController
|
|||||||
'name' => $values['name'],
|
'name' => $values['name'],
|
||||||
'is_private' => $values['is_private'],
|
'is_private' => $values['is_private'],
|
||||||
'identifier' => $values['identifier'],
|
'identifier' => $values['identifier'],
|
||||||
|
'per_swimlane_task_limits' => $values['per_swimlane_task_limits'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->projectModel->create($project, $this->userSession->getId(), true);
|
return $this->projectModel->create($project, $this->userSession->getId(), true);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Kanboard\Formatter;
|
namespace Kanboard\Formatter;
|
||||||
|
|
||||||
use Kanboard\Core\Filter\FormatterInterface;
|
use Kanboard\Core\Filter\FormatterInterface;
|
||||||
|
use Kanboard\Model\ProjectModel;
|
||||||
use Kanboard\Model\SwimlaneModel;
|
use Kanboard\Model\SwimlaneModel;
|
||||||
use Kanboard\Model\TaskModel;
|
use Kanboard\Model\TaskModel;
|
||||||
|
|
||||||
@@ -43,8 +44,16 @@ class BoardFormatter extends BaseFormatter implements FormatterInterface
|
|||||||
*/
|
*/
|
||||||
public function format()
|
public function format()
|
||||||
{
|
{
|
||||||
|
$project = $this->projectModel->getById($this->projectId);
|
||||||
$swimlanes = $this->swimlaneModel->getAllByStatus($this->projectId, SwimlaneModel::ACTIVE);
|
$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)) {
|
if (empty($swimlanes) || empty($columns)) {
|
||||||
return array();
|
return array();
|
||||||
|
|||||||
@@ -78,13 +78,16 @@ class BoardSwimlaneFormatter extends BaseFormatter implements FormatterInterface
|
|||||||
public function format()
|
public function format()
|
||||||
{
|
{
|
||||||
$nb_swimlanes = count($this->swimlanes);
|
$nb_swimlanes = count($this->swimlanes);
|
||||||
$nb_columns = count($this->columns);
|
|
||||||
|
|
||||||
foreach ($this->swimlanes as &$swimlane) {
|
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['id'] = (int) $swimlane['id'];
|
||||||
$swimlane['columns'] = $this->boardColumnFormatter
|
$swimlane['columns'] = $this->boardColumnFormatter
|
||||||
->withSwimlaneId($swimlane['id'])
|
->withSwimlaneId($swimlane['id'])
|
||||||
->withColumns($this->columns)
|
->withColumns($columns)
|
||||||
->withTasks($this->tasks)
|
->withTasks($this->tasks)
|
||||||
->withTags($this->tags)
|
->withTags($this->tags)
|
||||||
->format();
|
->format();
|
||||||
@@ -95,14 +98,12 @@ class BoardSwimlaneFormatter extends BaseFormatter implements FormatterInterface
|
|||||||
$swimlane['score'] = array_column_sum($swimlane['columns'], 'score');
|
$swimlane['score'] = array_column_sum($swimlane['columns'], 'score');
|
||||||
|
|
||||||
$this->calculateStatsByColumnAcrossSwimlanes($swimlane['columns']);
|
$this->calculateStatsByColumnAcrossSwimlanes($swimlane['columns']);
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($this->swimlanes as &$swimlane) {
|
|
||||||
foreach ($swimlane['columns'] as $columnIndex => &$column) {
|
foreach ($swimlane['columns'] as $columnIndex => &$column) {
|
||||||
$column['column_nb_tasks'] = $this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks'];
|
$column['column_nb_tasks'] = $this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks'];
|
||||||
$column['column_nb_score'] = $this->swimlanes[0]['columns'][$columnIndex]['column_score'];
|
$column['column_nb_score'] = $this->swimlanes[0]['columns'][$columnIndex]['column_score'];
|
||||||
// add number of open tasks to each column, ignoring the current filter
|
// 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'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
'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:',
|
'HTTP Client:' => 'Cliente HTTP:',
|
||||||
'XBT - bitcoin' => 'XBT - bitcoin',
|
'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' => '',
|
// 'Assigned' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,6 +138,24 @@ class ColumnModel extends Base
|
|||||||
->findAll();
|
->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 ]
|
* Get the list of columns sorted by position [ column_id => title ]
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ class ProjectDuplicationModel extends Base
|
|||||||
'priority_default' => $project['priority_default'],
|
'priority_default' => $project['priority_default'],
|
||||||
'priority_start' => $project['priority_start'],
|
'priority_start' => $project['priority_start'],
|
||||||
'priority_end' => $project['priority_end'],
|
'priority_end' => $project['priority_end'],
|
||||||
|
'per_swimlane_task_limits' => empty($project['per_swimlane_task_limits']) ? 0 : 1,
|
||||||
'identifier' => $identifier,
|
'identifier' => $identifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -457,6 +457,8 @@ class ProjectModel extends Base
|
|||||||
return false;
|
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'));
|
$this->helper->model->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end'));
|
||||||
|
|
||||||
return $this->exists($values['id']) &&
|
return $this->exists($values['id']) &&
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ use PDO;
|
|||||||
use Kanboard\Core\Security\Token;
|
use Kanboard\Core\Security\Token;
|
||||||
use Kanboard\Core\Security\Role;
|
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)
|
function version_133(PDO $pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ use PDO;
|
|||||||
use Kanboard\Core\Security\Token;
|
use Kanboard\Core\Security\Token;
|
||||||
use Kanboard\Core\Security\Role;
|
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)
|
function version_111(PDO $pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ use Kanboard\Core\Security\Token;
|
|||||||
use Kanboard\Core\Security\Role;
|
use Kanboard\Core\Security\Role;
|
||||||
use PDO;
|
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)
|
function version_120(PDO $pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
<?= $this->form->text('identifier', $values, $errors, array('autofocus')) ?>
|
<?= $this->form->text('identifier', $values, $errors, array('autofocus')) ?>
|
||||||
<p class="form-help"><?= t('The project identifier is optional and must be alphanumeric, example: MYPROJECT.') ?></p>
|
<p class="form-help"><?= t('The project identifier is optional and must be alphanumeric, example: MYPROJECT.') ?></p>
|
||||||
|
|
||||||
|
<?= $this->form->checkbox('per_swimlane_task_limits', t('Task limits apply to each swimlane individually'), 1, false) ?>
|
||||||
|
|
||||||
<?php if (count($projects_list) > 1): ?>
|
<?php if (count($projects_list) > 1): ?>
|
||||||
<?= $this->form->label(t('Create from another project'), 'src_project_id') ?>
|
<?= $this->form->label(t('Create from another project'), 'src_project_id') ?>
|
||||||
<?= $this->form->select('src_project_id', $projects_list, $values, array(), array(), 'js-project-creation-select-options') ?>
|
<?= $this->form->select('src_project_id', $projects_list, $values, array(), array(), 'js-project-creation-select-options') ?>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
|
|
||||||
<?= $this->form->label(t('Description'), 'description') ?>
|
<?= $this->form->label(t('Description'), 'description') ?>
|
||||||
<?= $this->form->textEditor('description', $values, $errors, array('tabindex' => 4)) ?>
|
<?= $this->form->textEditor('description', $values, $errors, array('tabindex' => 4)) ?>
|
||||||
|
|
||||||
|
<?= $this->form->checkbox('per_swimlane_task_limits', t('Task limits apply to each swimlane individually'), 1, $project['per_swimlane_task_limits'] == 1, '', array('tabindex' => 5)) ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -40,29 +42,29 @@
|
|||||||
|
|
||||||
<div class="form-inline">
|
<div class="form-inline">
|
||||||
<?= $this->form->label(t('Project owner'), 'owner_id') ?>
|
<?= $this->form->label(t('Project owner'), 'owner_id') ?>
|
||||||
<?= $this->form->select('owner_id', $owners, $values, $errors, array('tabindex="5"')) ?>
|
<?= $this->form->select('owner_id', $owners, $values, $errors, array('tabindex="6"')) ?>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><?= t('Dates') ?></legend>
|
<legend><?= t('Dates') ?></legend>
|
||||||
|
|
||||||
<?= $this->form->date(t('Start date'), 'start_date', $values, $errors, array('tabindex="6"')) ?>
|
<?= $this->form->date(t('Start date'), 'start_date', $values, $errors, array('tabindex="7"')) ?>
|
||||||
<?= $this->form->date(t('End date'), 'end_date', $values, $errors, array('tabindex="7"')) ?>
|
<?= $this->form->date(t('End date'), 'end_date', $values, $errors, array('tabindex="8"')) ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><?= t('Priorities') ?></legend>
|
<legend><?= t('Priorities') ?></legend>
|
||||||
|
|
||||||
<?= $this->form->label(t('Default priority'), 'priority_default') ?>
|
<?= $this->form->label(t('Default priority'), 'priority_default') ?>
|
||||||
<?= $this->form->number('priority_default', $values, $errors, array('tabindex="8"')) ?>
|
<?= $this->form->number('priority_default', $values, $errors, array('tabindex="9"')) ?>
|
||||||
|
|
||||||
<?= $this->form->label(t('Lowest priority'), 'priority_start') ?>
|
<?= $this->form->label(t('Lowest priority'), 'priority_start') ?>
|
||||||
<?= $this->form->number('priority_start', $values, $errors, array('tabindex="9"')) ?>
|
<?= $this->form->number('priority_start', $values, $errors, array('tabindex="10"')) ?>
|
||||||
|
|
||||||
<?= $this->form->label(t('Highest priority'), 'priority_end') ?>
|
<?= $this->form->label(t('Highest priority'), 'priority_end') ?>
|
||||||
<?= $this->form->number('priority_end', $values, $errors, array('tabindex="10"')) ?>
|
<?= $this->form->number('priority_end', $values, $errors, array('tabindex="11"')) ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<?= $this->modal->submitButtons(array('tabindex' => 11)) ?>
|
<?= $this->modal->submitButtons(array('tabindex' => 12)) ?>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
<?php if ($project['end_date']): ?>
|
<?php if ($project['end_date']): ?>
|
||||||
<li><?= t('End date: ').$this->dt->date($project['end_date']) ?></li>
|
<li><?= t('End date: ').$this->dt->date($project['end_date']) ?></li>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
|
<?php if ($project['per_swimlane_task_limits']): ?>
|
||||||
|
<li><?= t('Task limits are applied to each swimlane individually') ?></li>
|
||||||
|
<?php else: ?>
|
||||||
|
<li><?= t('Task limits are applied across swimlanes') ?></li>
|
||||||
|
<?php endif ?>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<?php if (! empty($project['description'])): ?>
|
<?php if (! empty($project['description'])): ?>
|
||||||
|
|||||||
@@ -643,4 +643,23 @@ class ProjectDuplicationModelTest extends Base
|
|||||||
$this->assertEquals(1, $filter['is_shared']);
|
$this->assertEquals(1, $filter['is_shared']);
|
||||||
$this->assertEquals(0, $filter['append']);
|
$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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class ProjectModelTest extends Base
|
|||||||
$this->assertEquals(1, $project['is_active']);
|
$this->assertEquals(1, $project['is_active']);
|
||||||
$this->assertEquals(0, $project['is_public']);
|
$this->assertEquals(0, $project['is_public']);
|
||||||
$this->assertEquals(0, $project['is_private']);
|
$this->assertEquals(0, $project['is_private']);
|
||||||
|
$this->assertEquals(0, $project['per_swimlane_task_limits']);
|
||||||
$this->assertEquals(time(), $project['last_modified'], '', 1);
|
$this->assertEquals(time(), $project['last_modified'], '', 1);
|
||||||
$this->assertEmpty($project['token']);
|
$this->assertEmpty($project['token']);
|
||||||
$this->assertEmpty($project['start_date']);
|
$this->assertEmpty($project['start_date']);
|
||||||
@@ -200,6 +201,20 @@ class ProjectModelTest extends Base
|
|||||||
$this->assertEquals(0, $project['owner_id']);
|
$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()
|
public function testGetAllIds()
|
||||||
{
|
{
|
||||||
$projectModel = new ProjectModel($this->container);
|
$projectModel = new ProjectModel($this->container);
|
||||||
|
|||||||
Reference in New Issue
Block a user