Added automatic actions based on a daily event

This commit is contained in:
Frederic Guillot 2016-01-27 21:45:37 -05:00
parent 320c7971f6
commit 32e4a932c8
25 changed files with 421 additions and 30 deletions

View File

@ -6,6 +6,13 @@ New features:
* Add project owner (Directly Responsible Individual)
* Add configurable task priority
* Add Greek translation
* Add automatic actions to close tasks with no activity
* Add automatic actions to send an email when there is no activity on a task
* Regroup all daily background tasks in one command: "cronjob"
Improvements:
* Show progress for task links in board tooltips
Version 1.0.24
--------------

View File

@ -125,7 +125,7 @@ abstract class Base extends \Kanboard\Core\Base
$params[] = $key.'='.var_export($value, true);
}
return $this->getName().'('.implode('|', $params).'])';
return $this->getName().'('.implode('|', $params).')';
}
/**

View File

@ -0,0 +1,95 @@
<?php
namespace Kanboard\Action;
use Kanboard\Model\Task;
/**
* Close automatically a task after when inactive
*
* @package action
* @author Frederic Guillot
*/
class TaskCloseNoActivity extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Close a task when there is no activity');
}
/**
* Get the list of compatible events
*
* @access public
* @return array
*/
public function getCompatibleEvents()
{
return array(Task::EVENT_DAILY_CRONJOB);
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'duration' => t('Duration in days')
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array('tasks');
}
/**
* Execute the action (close the task)
*
* @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();
$max = $this->getParam('duration') * 86400;
foreach ($data['tasks'] as $task) {
$duration = time() - $task['date_modification'];
if ($duration > $max) {
$results[] = $this->taskStatus->close($task['id']);
}
}
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;
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace Kanboard\Action;
use Kanboard\Model\Task;
/**
* Email a task with no activity
*
* @package action
* @author Frederic Guillot
*/
class TaskEmailNoActivity extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Send email when there is no activity on a task');
}
/**
* Get the list of compatible events
*
* @access public
* @return array
*/
public function getCompatibleEvents()
{
return array(
Task::EVENT_DAILY_CRONJOB,
);
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'user_id' => t('User that will receive the email'),
'subject' => t('Email subject'),
'duration' => t('Duration in days'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array('tasks');
}
/**
* 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;
}
/**
* Execute the action (move the task to another column)
*
* @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();
$max = $this->getParam('duration') * 86400;
$user = $this->user->getById($this->getParam('user_id'));
if (! empty($user['email'])) {
foreach ($data['tasks'] as $task) {
$duration = time() - $task['date_modification'];
if ($duration > $max) {
$results[] = $this->sendEmail($task['id'], $user);
}
}
}
return in_array(true, $results, true);
}
/**
* Send email
*
* @access private
* @param integer $task_id
* @param array $user
* @return boolean
*/
private function sendEmail($task_id, array $user)
{
$task = $this->taskFinder->getDetails($task_id);
$this->emailClient->send(
$user['email'],
$user['name'] ?: $user['username'],
$this->getParam('subject'),
$this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url')))
);
return true;
}
}

33
app/Console/Cronjob.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\NullOutput;
class Cronjob extends Base
{
private $commands = array(
'projects:daily-stats',
'notification:overdue-tasks',
'trigger:tasks',
);
protected function configure()
{
$this
->setName('cronjob')
->setDescription('Execute daily cronjob');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
foreach ($this->commands as $command) {
$job = $this->getApplication()->find($command);
$job->run(new ArrayInput(array('command' => $command)), new NullOutput());
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Kanboard\Model\Task;
use Kanboard\Event\TaskListEvent;
class TaskTrigger extends Base
{
protected function configure()
{
$this
->setName('trigger:tasks')
->setDescription('Trigger scheduler event for all tasks');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
foreach ($this->getProjectIds() as $project_id) {
$tasks = $this->taskFinder->getAll($project_id);
$nb_tasks = count($tasks);
if ($nb_tasks > 0) {
$output->writeln('Trigger task event: project_id='.$project_id.', nb_tasks='.$nb_tasks);
$this->sendEvent($tasks, $project_id);
}
}
}
private function getProjectIds()
{
$listeners = $this->dispatcher->getListeners(Task::EVENT_DAILY_CRONJOB);
$project_ids = array();
foreach ($listeners as $listener) {
$project_ids[] = $listener[0]->getProjectId();
}
return array_unique($project_ids);
}
private function sendEvent(array &$tasks, $project_id)
{
$event = new TaskListEvent(array('project_id' => $project_id));
$event->setTasks($tasks);
$this->dispatcher->dispatch(Task::EVENT_DAILY_CRONJOB, $event);
}
}

View File

@ -52,6 +52,7 @@ class EventManager
Task::EVENT_CLOSE => t('Closing a task'),
Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
Task::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'),
);
$events = array_merge($events, $this->events);

View File

@ -7,7 +7,7 @@ use Symfony\Component\EventDispatcher\Event as BaseEvent;
class GenericEvent extends BaseEvent implements ArrayAccess
{
private $container = array();
protected $container = array();
public function __construct(array $values = array())
{

View File

@ -0,0 +1,11 @@
<?php
namespace Kanboard\Event;
class TaskListEvent extends GenericEvent
{
public function setTasks(array &$tasks)
{
$this->container['tasks'] =& $tasks;
}
}

View File

@ -42,6 +42,7 @@ class Task extends Base
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
const EVENT_OVERDUE = 'task.overdue';
const EVENT_USER_MENTION = 'task.user.mention';
const EVENT_DAILY_CRONJOB = 'task.cronjob.daily';
/**
* Recurrence: status

View File

@ -23,12 +23,14 @@ use Kanboard\Action\TaskCloseColumn;
use Kanboard\Action\TaskCreation;
use Kanboard\Action\TaskDuplicateAnotherProject;
use Kanboard\Action\TaskEmail;
use Kanboard\Action\TaskEmailNoActivity;
use Kanboard\Action\TaskMoveAnotherProject;
use Kanboard\Action\TaskMoveColumnAssigned;
use Kanboard\Action\TaskMoveColumnCategoryChange;
use Kanboard\Action\TaskMoveColumnUnAssigned;
use Kanboard\Action\TaskOpen;
use Kanboard\Action\TaskUpdateStartDate;
use Kanboard\Action\TaskCloseNoActivity;
/**
* Action Provider
@ -63,9 +65,11 @@ class ActionProvider implements ServiceProviderInterface
$container['actionManager']->register(new TaskAssignUser($container));
$container['actionManager']->register(new TaskClose($container));
$container['actionManager']->register(new TaskCloseColumn($container));
$container['actionManager']->register(new TaskCloseNoActivity($container));
$container['actionManager']->register(new TaskCreation($container));
$container['actionManager']->register(new TaskDuplicateAnotherProject($container));
$container['actionManager']->register(new TaskEmail($container));
$container['actionManager']->register(new TaskEmailNoActivity($container));
$container['actionManager']->register(new TaskMoveAnotherProject($container));
$container['actionManager']->register(new TaskMoveColumnAssigned($container));
$container['actionManager']->register(new TaskMoveColumnCategoryChange($container));

View File

@ -15,22 +15,25 @@
<?php if ($this->text->contains($param_name, 'column_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?><br/>
<?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'user_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->select('params['.$param_name.']', $users_list, $values) ?><br/>
<?= $this->form->select('params['.$param_name.']', $users_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'project_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?><br/>
<?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'color_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?><br/>
<?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'category_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?><br/>
<?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'link_id')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->select('params['.$param_name.']', $links_list, $values) ?><br/>
<?= $this->form->select('params['.$param_name.']', $links_list, $values) ?>
<?php elseif ($this->text->contains($param_name, 'duration')): ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->number('params['.$param_name.']', $values) ?>
<?php else: ?>
<?= $this->form->label($param_desc, $param_name) ?>
<?= $this->form->text('params['.$param_name.']', $values) ?>

View File

@ -62,9 +62,4 @@ This chart show the average lead and cycle time for the last 1000 tasks over tim
Those metrics are calculated and recorded every day for the whole project.
Don't forget to run the daily job for statistics calculation
-------------------------------------------------------
To generate accurate analytic data, you should run the daily cronjob **project daily statistics**.
[Read the documentation of Kanboard CLI](cli.markdown)
Note: Don't forget to run the [daily cronjob](cronjob.markdown) to have accurate statistics.

View File

@ -1,6 +1,8 @@
Centos Installation
===================
Note: Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
Centos 7
--------

View File

@ -4,7 +4,7 @@ Command Line Interface
Kanboard provides a simple command line interface that can be used from any Unix terminal.
This tool can be used only on the local machine.
This feature is useful to run commands outside the web server process by example running a huge report.
This feature is useful to run commands outside of the web server processes.
Usage
-----
@ -28,6 +28,7 @@ Options:
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Available commands:
cronjob Execute daily cronjob
help Displays help for a command
list Lists commands
export
@ -42,6 +43,8 @@ Available commands:
notification:overdue-tasks Send notifications for overdue tasks
projects
projects:daily-stats Calculate daily statistics for all projects
trigger
trigger:tasks Trigger scheduler event for all tasks
```
Available commands
@ -116,7 +119,7 @@ Emails will be sent to all users with notifications enabled.
You can also display the overdue tasks with the flag `--show`:
```bash
$ ./kanboard notification:overdue-tasks --show
./kanboard notification:overdue-tasks --show
+-----+---------+------------+------------+--------------+----------+
| Id | Title | Due date | Project Id | Project name | Assignee |
+-----+---------+------------+------------+--------------+----------+
@ -125,20 +128,22 @@ $ ./kanboard notification:overdue-tasks --show
+-----+---------+------------+------------+--------------+----------+
```
Cronjob example:
```bash
# Everyday at 8am we check for due tasks
0 8 * * * cd /path/to/kanboard && ./kanboard notification:overdue-tasks >/dev/null 2>&1
```
### Run daily project stats calculation
You can add a background task to calculate the project statistics every day:
This command calculate the statistics of each project:
```bash
$ ./kanboard projects:daily-stats
./kanboard projects:daily-stats
Run calculation for Project #0
Run calculation for Project #1
Run calculation for Project #10
```
### Trigger for tasks
This command send a "daily cronjob event" to all open tasks of each project.
```bash
./kanboard trigger:tasks
Trigger task event: project_id=2, nb_tasks=1
```

32
doc/cronjob.markdown Normal file
View File

@ -0,0 +1,32 @@
Background Job Scheduling
=========================
To work properly, Kanboard requires that a background job run on a daily basis.
Usually on Unix platforms, this process is done by `cron`.
This background job is necessary for these features:
- Reports and analytics (calculate daily stats of each projects)
- Send overdue task notifications
- Execute automatic actions connected to the event "Daily background job for tasks"
Configuration on Unix and Linux platforms
-----------------------------------------
There are multiple ways to define a cronjob on Unix/Linux operating systems, this example is for Ubuntu 14.04.
The procedure is similar to other systems.
Edit the crontab of your web server user:
```bash
sudo crontab -u www-data -e
```
Example to execute the daily cronjob at 8am:
```bash
0 8 * * * cd /path/to/kanboard && ./kanboard cronjob >/dev/null 2>&1
```
Note: the cronjob process must have write access to the database in case you are using Sqlite.
Usually, running the cronjob under the web server user is enough.

View File

@ -1,6 +1,8 @@
How to install Kanboard on Debian?
==================================
Note: Some features of Kanboard require that you run [a daily background job](cronjob.markdown).
Debian 8 (Jessie)
-----------------

View File

@ -55,7 +55,7 @@ Generally 3 elements have to be installed:
Fetch and extract ports...
```bash
$ portsnap fetch
$ portsnap fetch
$ portsnap extract
```
@ -122,6 +122,7 @@ there is no need to install it manually.
Please note
-----------
Port is being hosted on [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Feel free to comment,
- Port is being hosted on [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Feel free to comment,
fork and suggest updates!
- Some features of Kanboard require that you run [a daily background job](cronjob.markdown).

View File

@ -35,5 +35,5 @@ heroku open
Limitations
-----------
The storage of Heroku is ephemeral, that means uploaded files through Kanboard are not persistent after a reboot.
We may want to install a plugin to store your files in a cloud storage provider like [Amazon S3](https://github.com/kanboard/plugin-s3).
- The storage of Heroku is ephemeral, that means uploaded files through Kanboard are not persistent after a reboot. You may want to install a plugin to store your files in a cloud storage provider like [Amazon S3](https://github.com/kanboard/plugin-s3).
- Some features of Kanboard require that you run [a daily background job](cronjob.markdown).

View File

@ -103,6 +103,7 @@ Technical details
### Configuration
- [Config file](config.markdown)
- [Background tasks](cronjob.markdown)
- [Email configuration](email-configuration.markdown)
- [URL rewriting](nice-urls.markdown)

View File

@ -39,3 +39,8 @@ Security
- Don't forget to change the default user/password
- Don't allow everybody to access to the directory `data` from the URL. There is already a `.htaccess` for Apache but nothing for Nginx.
Notes
-----
- Some features of Kanboard require that you run [a daily background job](cronjob.markdown)

View File

@ -26,3 +26,5 @@ sudo unzip kanboard-latest.zip
sudo chown -R www-data:www-data kanboard/data
sudo rm kanboard-latest.zip
```
Some features of Kanboard require that you run [a daily background job](cronjob.markdown).

View File

@ -123,3 +123,8 @@ Tested configuration
--------------------
- Windows 2008 R2 / Apache 2.4.12 / PHP 5.6.8
Notes
-----
- Some features of Kanboard require that you run [a daily background job](cronjob.markdown).

View File

@ -65,3 +65,9 @@ Tested configurations
- Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16
- Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29
Notes
-----
- Some features of Kanboard require that you run [a daily background job](cronjob.markdown).

View File

@ -13,6 +13,8 @@ use Kanboard\Console\ProjectDailyColumnStatsExport;
use Kanboard\Console\TransitionExport;
use Kanboard\Console\LocaleSync;
use Kanboard\Console\LocaleComparator;
use Kanboard\Console\TaskTrigger;
use Kanboard\Console\Cronjob;
$container['dispatcher']->dispatch('app.bootstrap', new Event);
@ -25,4 +27,6 @@ $application->add(new ProjectDailyColumnStatsExport($container));
$application->add(new TransitionExport($container));
$application->add(new LocaleSync($container));
$application->add(new LocaleComparator($container));
$application->add(new TaskTrigger($container));
$application->add(new Cronjob($container));
$application->run();