Merge pull-request #847 (recurring tasks)
This commit is contained in:
@@ -314,6 +314,18 @@ class Board extends Base
|
||||
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last column id for a given project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @return integer
|
||||
*/
|
||||
public function getLastColumn($project_id)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of columns sorted by position [ column_id => title ]
|
||||
*
|
||||
|
||||
@@ -41,6 +41,41 @@ class Task extends Base
|
||||
const EVENT_CREATE_UPDATE = 'task.create_update';
|
||||
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
|
||||
|
||||
/**
|
||||
* Recurrence: status
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const RECURE_STATUS_NONE = 0;
|
||||
const RECURE_STATUS_PENDING = 1;
|
||||
const RECURE_STATUS_PROCESSED = 2;
|
||||
|
||||
/**
|
||||
* Recurrence: trigger
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const RECURE_TRIGGER_FIRST = 0;
|
||||
const RECURE_TRIGGER_LAST = 1;
|
||||
const RECURE_TRIGGER_CLOSE = 2;
|
||||
|
||||
/**
|
||||
* Recurrence: timeframe
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const RECURE_DAYS = 0;
|
||||
const RECURE_MONTHS = 1;
|
||||
const RECURE_YEARS = 2;
|
||||
|
||||
/**
|
||||
* Recurrence: base date used to calculate new due date
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const RECURE_BASEDATE_DUEDATE = 0;
|
||||
const RECURE_BASEDATE_TRIGGERDATE = 1;
|
||||
|
||||
/**
|
||||
* Remove a task
|
||||
*
|
||||
@@ -76,4 +111,62 @@ class Task extends Base
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list user selectable recurrence status
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getRecurrenceStatusList()
|
||||
{
|
||||
return array (
|
||||
Task::RECURE_STATUS_NONE => t('No'),
|
||||
Task::RECURE_STATUS_PENDING => t('Yes'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list recurrence triggers
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getRecurrenceTriggerList()
|
||||
{
|
||||
return array (
|
||||
Task::RECURE_TRIGGER_FIRST => t('When task is moved from first column'),
|
||||
Task::RECURE_TRIGGER_LAST => t('When task is moved to last column'),
|
||||
Task::RECURE_TRIGGER_CLOSE => t('When task is closed'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list options to calculate recurrence due date
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getRecurrenceBasedateList()
|
||||
{
|
||||
return array (
|
||||
Task::RECURE_BASEDATE_DUEDATE => t('Existing due date'),
|
||||
Task::RECURE_BASEDATE_TRIGGERDATE => t('Action date'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list recurrence timeframes
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getRecurrenceTimeframeList()
|
||||
{
|
||||
return array (
|
||||
Task::RECURE_DAYS => t('Day(s)'),
|
||||
Task::RECURE_MONTHS => t('Month(s)'),
|
||||
Task::RECURE_YEARS => t('Year(s)'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
87
app/Model/TaskDuplication.php
Normal file → Executable file
87
app/Model/TaskDuplication.php
Normal file → Executable file
@@ -30,6 +30,11 @@ class TaskDuplication extends Base
|
||||
'category_id',
|
||||
'time_estimated',
|
||||
'swimlane_id',
|
||||
'recurrence_status',
|
||||
'recurrence_trigger',
|
||||
'recurrence_factor',
|
||||
'recurrence_timeframe',
|
||||
'recurrence_basedate',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -44,6 +49,43 @@ class TaskDuplication extends Base
|
||||
return $this->save($task_id, $this->copyFields($task_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create task recurrence to the same project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $task_id Task id
|
||||
* @return boolean|integer Recurrence task id
|
||||
*/
|
||||
public function createRecurrence($task_id)
|
||||
{
|
||||
$values = $this->copyFields($task_id);
|
||||
|
||||
if ($values['recurrence_status'] == Task::RECURE_STATUS_PENDING)
|
||||
{
|
||||
$values['recurrence_parent'] = $task_id;
|
||||
$values['column_id'] = $this->board->getFirstColumn($values['project_id']);
|
||||
$this->recurrenceDateDue($values);
|
||||
$recuretask = $this->save($task_id, $values);
|
||||
|
||||
if ($recuretask)
|
||||
{
|
||||
$recurrenceStatusUpdate = $this->db
|
||||
->table(Task::TABLE)
|
||||
->eq('id',$task_id)
|
||||
->update(array(
|
||||
'recurrence_status' => Task::RECURE_STATUS_PROCESSED,
|
||||
'recurrence_child' => $recuretask,
|
||||
));
|
||||
|
||||
if($recurrenceStatusUpdate)
|
||||
{
|
||||
return $recuretask;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate a task to another project
|
||||
*
|
||||
@@ -126,6 +168,51 @@ class TaskDuplication extends Base
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate new due date for new recurrence task
|
||||
*
|
||||
* @access private
|
||||
* @param array $values
|
||||
*/
|
||||
private function recurrenceDateDue(&$values)
|
||||
{
|
||||
if ($values['date_due'] && $values['recurrence_factor'])
|
||||
{
|
||||
if ($values['recurrence_basedate'])
|
||||
{
|
||||
$values['date_due'] = time();
|
||||
}
|
||||
|
||||
$factor = abs($values['recurrence_factor']);
|
||||
|
||||
if ($values['recurrence_factor'] < 0)
|
||||
{
|
||||
$subtract=TRUE;
|
||||
}
|
||||
|
||||
switch ($values['recurrence_timeframe'])
|
||||
{
|
||||
case Task::RECURE_MONTHS:
|
||||
$interval = 'P' . $factor . 'M';
|
||||
break;
|
||||
case Task::RECURE_YEARS:
|
||||
$interval = 'P' . $factor . 'Y';
|
||||
break;
|
||||
default:
|
||||
$interval = 'P' . $factor . 'D';
|
||||
break;
|
||||
}
|
||||
|
||||
$date_due = new \DateTime();
|
||||
|
||||
$date_due->setTimestamp($values['date_due']);
|
||||
|
||||
$subtract ? $date_due->sub(new \DateInterval($interval)) : $date_due->add(new \DateInterval($interval));
|
||||
|
||||
$values['date_due'] = $date_due->getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate fields for the new task
|
||||
*
|
||||
|
||||
@@ -104,6 +104,13 @@ class TaskFinder extends Base
|
||||
'tasks.score',
|
||||
'tasks.category_id',
|
||||
'tasks.date_moved',
|
||||
'tasks.recurrence_status',
|
||||
'tasks.recurrence_trigger',
|
||||
'tasks.recurrence_factor',
|
||||
'tasks.recurrence_timeframe',
|
||||
'tasks.recurrence_basedate',
|
||||
'tasks.recurrence_parent',
|
||||
'tasks.recurrence_child',
|
||||
'users.username AS assignee_username',
|
||||
'users.name AS assignee_name'
|
||||
)
|
||||
@@ -246,6 +253,13 @@ class TaskFinder extends Base
|
||||
tasks.category_id,
|
||||
tasks.swimlane_id,
|
||||
tasks.date_moved,
|
||||
tasks.recurrence_status,
|
||||
tasks.recurrence_trigger,
|
||||
tasks.recurrence_factor,
|
||||
tasks.recurrence_timeframe,
|
||||
tasks.recurrence_basedate,
|
||||
tasks.recurrence_parent,
|
||||
tasks.recurrence_child,
|
||||
project_has_categories.name AS category_name,
|
||||
projects.name AS project_name,
|
||||
columns.title AS column_title,
|
||||
|
||||
@@ -67,7 +67,7 @@ class TaskModification extends Base
|
||||
$this->dateParser->convert($values, array('date_due', 'date_started'));
|
||||
$this->removeFields($values, array('another_task', 'id'));
|
||||
$this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent'));
|
||||
$this->convertIntegerFields($values, array('is_active'));
|
||||
$this->convertIntegerFields($values, array('is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate'));
|
||||
|
||||
$values['date_modification'] = time();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,19 @@ class TaskPosition extends Base
|
||||
if ($fire_events) {
|
||||
$this->fireEvents($original_task, $column_id, $position, $swimlane_id);
|
||||
}
|
||||
|
||||
if ($original_task['recurrence_status'] == Task::RECURE_STATUS_PENDING
|
||||
&& $original_task['column_id'] != $column_id
|
||||
&& (
|
||||
($original_task['column_id'] == $this->board->getFirstColumn($project_id)
|
||||
&& $original_task['recurrence_trigger'] == Task::RECURE_TRIGGER_FIRST)
|
||||
|| ($column_id == $this->board->getLastColumn($project_id)
|
||||
&& $original_task['recurrence_trigger'] == Task::RECURE_TRIGGER_LAST)
|
||||
)
|
||||
)
|
||||
{
|
||||
$this->taskDuplication->createRecurrence($task_id);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
@@ -89,7 +89,9 @@ class TaskStatus extends Base
|
||||
*/
|
||||
private function changeStatus($task_id, $status, $date_completed, $event)
|
||||
{
|
||||
if (! $this->taskFinder->exists($task_id)) {
|
||||
$task = $this->taskFinder->getById($task_id);
|
||||
|
||||
if (!$task['id']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -107,6 +109,13 @@ class TaskStatus extends Base
|
||||
$event,
|
||||
new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id))
|
||||
);
|
||||
|
||||
if ($status == Task::STATUS_CLOSED
|
||||
&& $task['recurrence_status'] == Task::RECURE_STATUS_PENDING
|
||||
&& $task['recurrence_trigger'] == Task::RECURE_TRIGGER_CLOSE)
|
||||
{
|
||||
$this->taskDuplication->createRecurrence($task_id);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
@@ -30,6 +30,13 @@ class TaskValidator extends Base
|
||||
new Validators\Integer('score', t('This value must be an integer')),
|
||||
new Validators\Integer('category_id', t('This value must be an integer')),
|
||||
new Validators\Integer('swimlane_id', t('This value must be an integer')),
|
||||
new Validators\Integer('recurrence_child', t('This value must be an integer')),
|
||||
new Validators\Integer('recurrence_parent', t('This value must be an integer')),
|
||||
new Validators\Integer('recurrence_factor', t('This value must be an integer')),
|
||||
new Validators\Integer('recurrence_timeframe', t('This value must be an integer')),
|
||||
new Validators\Integer('recurrence_basedate', t('This value must be an integer')),
|
||||
new Validators\Integer('recurrence_trigger', t('This value must be an integer')),
|
||||
new Validators\Integer('recurrence_status', t('This value must be an integer')),
|
||||
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
|
||||
new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()),
|
||||
new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateFormats()),
|
||||
@@ -81,6 +88,28 @@ class TaskValidator extends Base
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate edit recurrence
|
||||
*
|
||||
* @access public
|
||||
* @param array $values Form values
|
||||
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
|
||||
*/
|
||||
public function validateEditRecurrence(array $values)
|
||||
{
|
||||
$rules = array(
|
||||
new Validators\Required('id', t('The id is required')),
|
||||
);
|
||||
|
||||
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
|
||||
|
||||
return array(
|
||||
$v->execute(),
|
||||
$v->getErrors()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate task modification (form)
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user