summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog1
-rw-r--r--app/Action/TaskAssignColorOnDueDate.php100
-rw-r--r--app/Locale/pt_PT/translations.php60
-rw-r--r--app/ServiceProvider/ActionProvider.php2
-rw-r--r--app/Template/board/table_column.php29
-rw-r--r--assets/js/components/calendar.js21
-rw-r--r--assets/js/components/select-dropdown-autocomplete.js2
-rw-r--r--tests/units/Action/TaskAssignColorOnDueDateTest.php37
8 files changed, 209 insertions, 43 deletions
diff --git a/ChangeLog b/ChangeLog
index 30b6d0e2..10c41b2c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,7 @@ New features:
* Send tasks by email
* Add Reply-To header to emails sent from Kanboard
* Upload Sqlite database from user interface
+* Automatic action to change task color when due date is expired
Improvements:
diff --git a/app/Action/TaskAssignColorOnDueDate.php b/app/Action/TaskAssignColorOnDueDate.php
new file mode 100644
index 00000000..06b70a18
--- /dev/null
+++ b/app/Action/TaskAssignColorOnDueDate.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Kanboard\Action;
+
+use Kanboard\Model\TaskModel;
+
+
+/**
+ * Assign a color to a priority
+ *
+ * @package Kanboard\Action
+ * @author Julien Buratto
+ */
+class TaskAssignColorOnDueDate extends Base
+{
+ /**
+ * Get action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign automatically a color when due date is expired');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_DAILY_CRONJOB,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'color_id' => t('Color'),
+ );
+ }
+
+ /**
+ * Get all tasks
+ *
+ * @access public
+ * @return array
+ */
+
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+ }
+
+ /**
+ * Execute the action (change the task color)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $results = array();
+
+ foreach ($data['tasks'] as $task) {
+ if ($task['date_due'] <= time() && $task['date_due'] > 0) {
+ $values = array(
+ 'id' => $task['id'],
+ 'color_id' => $this->getParam('color_id'),
+ );
+ $results[] = $this->taskModificationModel->update($values, false);
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+}
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index 7aa39b2e..cb6873cd 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -1302,34 +1302,34 @@ return array(
'Do you really want to reopen this project: "%s"?' => 'Deseja mesmo reabrir este projecto?: "%s"?',
'This project is open' => 'Este projeto está aberto',
'This project is closed' => 'Este projecto está fechado',
- // 'Unable to upload files, check the permissions of your data folder.' => '',
- // 'Another category with the same name exists in this project' => '',
- // 'Comment sent by email successfully.' => '',
- // 'Sent by email to [%s](mailto:%s) (%s)' => '',
- // 'Unable to read uploaded file.' => '',
- // 'Database uploaded successfully.' => '',
- // 'Task sent by email successfully.' => '',
- // 'There is no category in this project.' => '',
- // 'Send by email' => '',
- // 'Create and send a comment by email' => '',
- // 'Subject' => '',
- // 'Upload the database' => '',
- // 'You could upload the previously downloaded Sqlite database (Gzip format).' => '',
- // 'Database file' => '',
- // 'Upload' => '',
- // 'Remove this user from group' => '',
- // 'Your project must have at least one active swimlane.' => '',
- // 'Project: %s' => '',
- // 'Automatic action not found: "%s"' => '',
- // '%d projects' => '',
- // '%d project' => '',
- // 'There is no project.' => '',
- // 'Sort' => '',
- // 'Project ID' => '',
- // 'Project name' => '',
- // 'Public' => '',
- // 'Private' => '',
- // '%d tasks' => '',
- // '%d task' => '',
- // 'Task ID' => '',
+ 'Unable to upload files, check the permissions of your data folder.' => 'Impossivel enviar ficheiros, verifique as permissões da pasta data',
+ 'Another category with the same name exists in this project' => 'Outra categoria com o mesmo nome já existe neste projecto',
+ 'Comment sent by email successfully.' => 'Comentário enviado por email com sucesso.',
+ 'Sent by email to [%s](mailto:%s) (%s)' => 'Enviado por email para [%s](mailto:%s) (%s)',
+ 'Unable to read uploaded file.' => 'Não foi possivel ler ficheiro enviado.',
+ 'Database uploaded successfully.' => 'Base de dados enviada com sucesso.',
+ 'Task sent by email successfully.' => 'Tarefa enviada por email com sucesso.',
+ 'There is no category in this project.' => 'Não existe categorias neste projecto.',
+ 'Send by email' => 'Enviar por email',
+ 'Create and send a comment by email' => 'Criar e enviar um comentário por email',
+ 'Subject' => 'Assunto',
+ 'Upload the database' => 'Enviar a base de dados',
+ 'You could upload the previously downloaded Sqlite database (Gzip format).' => 'Poderia enviar a base de dados Sqlite transferida anteriormente (formato Gzip).',
+ 'Database file' => 'Ficheiro base de dados',
+ 'Upload' => 'Enviar',
+ 'Remove this user from group' => 'Remover este utilizador do grupo',
+ 'Your project must have at least one active swimlane.' => 'O seu projecto deve ter pelo menos uma swimlane activa.',
+ 'Project: %s' => 'Projecto: %s',
+ 'Automatic action not found: "%s"' => 'Acção automática não encontrada: "%s"',
+ '%d projects' => '%d projetos',
+ '%d project' => '%d projecto',
+ 'There is no project.' => 'Não existe projecto.',
+ 'Sort' => 'Ordenar',
+ 'Project ID' => 'ID do Projecto',
+ 'Project name' => 'Nome do Projecto',
+ 'Public' => 'Público',
+ 'Private' => 'Privado',
+ '%d tasks' => '%d tarefas',
+ '%d task' => '%d tarefa',
+ 'Task ID' => 'ID da Tarefa',
);
diff --git a/app/ServiceProvider/ActionProvider.php b/app/ServiceProvider/ActionProvider.php
index 81f2b39e..a7e8040e 100644
--- a/app/ServiceProvider/ActionProvider.php
+++ b/app/ServiceProvider/ActionProvider.php
@@ -4,6 +4,7 @@ namespace Kanboard\ServiceProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
+use Kanboard\Action\TaskAssignColorOnDueDate;
use Kanboard\Action\TaskAssignColorPriority;
use Kanboard\Action\TaskAssignDueDateOnCreation;
use Kanboard\Action\TaskMoveColumnClosed;
@@ -92,6 +93,7 @@ class ActionProvider implements ServiceProviderInterface
$container['actionManager']->register(new TaskAssignDueDateOnCreation($container));
$container['actionManager']->register(new TaskAssignColorSwimlane($container));
$container['actionManager']->register(new TaskAssignPrioritySwimlane($container));
+ $container['actionManager']->register(new TaskAssignColorOnDueDate($container));
return $container;
}
diff --git a/app/Template/board/table_column.php b/app/Template/board/table_column.php
index df16715f..c2d6b9fc 100644
--- a/app/Template/board/table_column.php
+++ b/app/Template/board/table_column.php
@@ -53,17 +53,26 @@
<?php endif ?>
</span>
- <?php if (! $not_editable && ! empty($column['description'])): ?>
- <span class="tooltip pull-right" title="<?= $this->text->markdownAttribute($column['description']) ?>">
- &nbsp;<i class="fa fa-info-circle"></i>
- </span>
- <?php endif ?>
+ <span class="pull-right">
+ <?php if ($swimlane['nb_swimlanes'] > 1 && ! empty($column['column_score'])): ?>
+ <span title="<?= t('Total score in this column across all swimlanes') ?>">
+ (<span><?= $column['column_score'] ?></span>)
+ </span>
+ <?php endif ?>
- <?php if (! empty($column['score'])): ?>
- <span class="pull-right" title="<?= t('Score') ?>">
- <?= $column['score'] ?>
- </span>
- <?php endif ?>
+ <?php if (! empty($column['score'])): ?>
+ <span title="<?= t('Score') ?>">
+ <?= $column['score'] ?>
+ </span>
+ <?php endif ?>
+
+ <?php if (! $not_editable && ! empty($column['description'])): ?>
+ <span class="tooltip" title="<?= $this->text->markdownAttribute($column['description']) ?>">
+ &nbsp;<i class="fa fa-info-circle"></i>
+ </span>
+ <?php endif ?>
+
+ </span>
<?php if ($column['task_limit']): ?>
<span title="<?= t('Task limit') ?>">
diff --git a/assets/js/components/calendar.js b/assets/js/components/calendar.js
index d07c911d..ed6916b2 100644
--- a/assets/js/components/calendar.js
+++ b/assets/js/components/calendar.js
@@ -1,13 +1,23 @@
KB.component('calendar', function (containerElement, options) {
+ var modeMapping = { // Let's have bookable pretty mode names
+ month: 'month',
+ week: 'agendaWeek',
+ day: 'agendaDay'
+ };
this.render = function () {
var calendar = $(containerElement);
+ var mode = 'month';
+ if (window.location.hash) { // Check if hash contains mode
+ var hashMode = window.location.hash.substr(1);
+ mode = modeMapping[hashMode] || mode;
+ }
calendar.fullCalendar({
locale: $("body").data("js-lang"),
editable: true,
eventLimit: true,
- defaultView: "month",
+ defaultView: mode,
header: {
left: 'prev,next today',
center: 'title',
@@ -26,7 +36,14 @@ KB.component('calendar', function (containerElement, options) {
})
});
},
- viewRender: function() {
+ viewRender: function(view) {
+ // Map view.name back and update location.hash
+ for (var id in modeMapping) {
+ if (modeMapping[id] === view.name) { // Found
+ window.location.hash = id;
+ break;
+ }
+ }
var url = options.checkUrl;
var params = {
"start": calendar.fullCalendar('getView').start.format(),
diff --git a/assets/js/components/select-dropdown-autocomplete.js b/assets/js/components/select-dropdown-autocomplete.js
index c2c36f5b..188dd5e9 100644
--- a/assets/js/components/select-dropdown-autocomplete.js
+++ b/assets/js/components/select-dropdown-autocomplete.js
@@ -161,7 +161,7 @@ KB.component('select-dropdown-autocomplete', function(containerElement, options)
var hasActiveItem = false;
for (var i = 0; i < items.length; i++) {
- if (text.length === 0 || items[i]['data-label'].toLowerCase().indexOf(text.toLowerCase()) === 0) {
+ if (text.length === 0 || items[i]['data-label'].toLowerCase().indexOf(text.toLowerCase()) > -1) {
var item = items[i];
if (typeof options.defaultValue !== 'undefined' && String(options.defaultValue) === item['data-value']) {
diff --git a/tests/units/Action/TaskAssignColorOnDueDateTest.php b/tests/units/Action/TaskAssignColorOnDueDateTest.php
new file mode 100644
index 00000000..4bb87c06
--- /dev/null
+++ b/tests/units/Action/TaskAssignColorOnDueDateTest.php
@@ -0,0 +1,37 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Action\TaskAssignColorOnDueDate;
+use Kanboard\Event\TaskListEvent;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\TaskModel;
+
+class TaskAssignColorOnDueDateTest extends Base
+{
+ public function testChangeColor()
+ {
+ $projectModel = new ProjectModel($this->container);
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new TaskFinderModel($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('project_id' => 1, 'title' => 'test')));
+
+ $tasks = $taskFinderModel->getAll(1);
+ $event = new TaskListEvent(array('tasks' => $tasks, 'project_id' => 1));
+
+ $action = new TaskAssignColorOnDueDate($this->container);
+ $action->setProjectId(1);
+ $action->setParam('color_id', 'red');
+
+ $this->assertTrue($action->execute($event, TaskModel::EVENT_DAILY_CRONJOB));
+
+ $tasks = $taskFinderModel->getAll(1);
+ $this->assertEquals('red', $tasks[0]['color_id']);
+ $this->assertEquals('yellow', $tasks[1]['color_id']);
+ }
+}