From a314bbb489eff2d419481ad001805ce13edb5352 Mon Sep 17 00:00:00 2001 From: David-Norris Date: Sun, 3 May 2015 00:12:28 -0400 Subject: Initial Recurring Tasks Commit Initial Recurring Tasks Commit No Locales Updated. --- app/Controller/Board.php | 17 +++++++ app/Controller/Task.php | 60 +++++++++++++++++++++++ app/Model/Task.php | 91 +++++++++++++++++++++++++++++++++++ app/Model/TaskDuplication.php | 87 +++++++++++++++++++++++++++++++++ app/Model/TaskFinder.php | 14 ++++++ app/Model/TaskModification.php | 2 +- app/Model/TaskPosition.php | 8 +++ app/Model/TaskStatus.php | 11 ++++- app/Model/TaskValidator.php | 29 +++++++++++ app/Schema/Mysql.php | 13 ++++- app/Schema/Postgres.php | 13 ++++- app/Schema/Sqlite.php | 13 ++++- app/Template/board/recurrence.php | 18 +++++++ app/Template/board/task_footer.php | 8 +++ app/Template/board/task_menu.php | 3 +- app/Template/task/details.php | 39 +++++++++++++++ app/Template/task/edit_recurrence.php | 85 ++++++++++++++++++++++++++++++++ app/Template/task/show.php | 2 +- app/Template/task/sidebar.php | 5 +- 19 files changed, 510 insertions(+), 8 deletions(-) mode change 100644 => 100755 app/Model/TaskDuplication.php create mode 100644 app/Template/board/recurrence.php create mode 100644 app/Template/task/edit_recurrence.php diff --git a/app/Controller/Board.php b/app/Controller/Board.php index d9243633..94a57e84 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -350,4 +350,21 @@ class Board extends Base 'redirect' => 'board', ))); } + + /** + * Get recurrence information on mouseover + * + * @access public + */ + public function recurrence() + { + $task = $this->getTask(); + + $this->response->html($this->template->render('board/recurrence', array( + 'task' => $task, + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ))); + } } diff --git a/app/Controller/Task.php b/app/Controller/Task.php index 5bca4510..2f590695 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -81,6 +81,9 @@ class Task extends Base 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), 'title' => $task['project_name'].' > '.$task['title'], + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), ))); } @@ -443,6 +446,63 @@ class Task extends Base } } + /** + * Edit recurrence form + * + * @access public + */ + public function recurrence() + { + $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values); + + if ($valid) { + + if ($this->taskModification->update($values)) { + $this->session->flash(t('Task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + + if ($ajax) { + $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); + } + else { + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); + } + } + } + else { + $values = $task; + $errors = array(); + } + + $params = array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'ajax' => $ajax, + 'recurrence_status_list' => $this->task->getRecurrenceStatusList(), + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ); + + if ($ajax) { + $this->response->html($this->template->render('task/edit_recurrence', $params)); + } + else { + $this->response->html($this->taskLayout('task/edit_recurrence', $params)); + } + } + /** * Move a task to another project * diff --git a/app/Model/Task.php b/app/Model/Task.php index bc2913ec..1a58e981 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -41,6 +41,40 @@ 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_MOVE = 0; + const RECURE_TRIGGER_CLOSE = 1; + + /** + * 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 +110,61 @@ 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_MOVE => 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)'), + ); + } } diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php old mode 100644 new mode 100755 index bd593dc1..f3ce4f7b --- a/app/Model/TaskDuplication.php +++ b/app/Model/TaskDuplication.php @@ -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 * diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index 7216e92a..554279a5 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -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' ) @@ -245,6 +252,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, diff --git a/app/Model/TaskModification.php b/app/Model/TaskModification.php index dac52334..677fcd60 100644 --- a/app/Model/TaskModification.php +++ b/app/Model/TaskModification.php @@ -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(); } diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php index ab5fe43b..8589cac5 100644 --- a/app/Model/TaskPosition.php +++ b/app/Model/TaskPosition.php @@ -39,6 +39,14 @@ class TaskPosition extends Base if ($fire_events) { $this->fireEvents($original_task, $column_id, $position, $swimlane_id); } + + if ($original_task['column_id'] != $column_id + && $column_id == $this->board->getLastColumnPosition($project_id) + && $original_task['recurrence_status'] == Task::RECURE_STATUS_PENDING + && $original_task['recurrence_trigger'] == Task::RECURE_TRIGGER_MOVE) + { + $this->taskDuplication->createRecurrence($task_id); + } } return $result; diff --git a/app/Model/TaskStatus.php b/app/Model/TaskStatus.php index 30a65e1e..1ae8bfeb 100644 --- a/app/Model/TaskStatus.php +++ b/app/Model/TaskStatus.php @@ -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; diff --git a/app/Model/TaskValidator.php b/app/Model/TaskValidator.php index ae21ca28..ec1383ad 100644 --- a/app/Model/TaskValidator.php +++ b/app/Model/TaskValidator.php @@ -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) * diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 22f8c1b0..9ed23ee0 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,18 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 66; +const VERSION = 67; + +function version_67($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER'); +} function version_66($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index db30af67..f1262816 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,18 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 47; +const VERSION = 48; + +function version_48($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER'); +} function version_47($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 79c50458..714d0e2f 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,18 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 65; +const VERSION = 66; + +function version_66($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER'); +} function version_65($pdo) { diff --git a/app/Template/board/recurrence.php b/app/Template/board/recurrence.php new file mode 100644 index 00000000..1b71bc34 --- /dev/null +++ b/app/Template/board/recurrence.php @@ -0,0 +1,18 @@ +
+ +
+ + +
+ +
+
+
+
+ +
+ + +
+ +
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php index 36ed2684..b8868f52 100644 --- a/app/Template/board/task_footer.php +++ b/app/Template/board/task_footer.php @@ -25,6 +25,14 @@ + + + + + + + +   diff --git a/app/Template/board/task_menu.php b/app/Template/board/task_menu.php index f3ec3019..fba2d71d 100644 --- a/app/Template/board/task_menu.php +++ b/app/Template/board/task_menu.php @@ -5,6 +5,7 @@
  • a(t('Change assignee'), 'board', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?>
  • a(t('Change category'), 'board', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?>
  • a(t('Change description'), 'task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?>
  • +
  • a(t('Edit recurrence'), 'task', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?>
  • a(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?>
  • a(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?>
  • a(t('Edit this task'), 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?>
  • @@ -12,4 +13,4 @@
  • a(t('Close this task'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'task-board-popover') ?>
  • - \ No newline at end of file + diff --git a/app/Template/task/details.php b/app/Template/task/details.php index 76241acf..331862b9 100644 --- a/app/Template/task/details.php +++ b/app/Template/task/details.php @@ -80,5 +80,44 @@ a(t('Public link'), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?> + +
  • + +
  • + + +
  • + + + + + + + +
  • + + +
  • + +
  • + + +
  • + +
  • + diff --git a/app/Template/task/edit_recurrence.php b/app/Template/task/edit_recurrence.php new file mode 100644 index 00000000..bb86e429 --- /dev/null +++ b/app/Template/task/edit_recurrence.php @@ -0,0 +1,85 @@ + +
    + + + +
    + + formCsrf() ?> + + +
    + +
      +
    • +
        +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      +
    • +
    + + +
      + +
    • + +
    • + + +
    • + +
    • + +
    + + + + + formHidden('id', $values) ?> + formHidden('project_id', $values) ?> + + formLabel(t('Generate recurrent task'), 'recurrence_status') ?> + formSelect('recurrence_status', $recurrence_status_list, $values, $errors) ?>
    + + formLabel(t('Trigger to generate recurrent task'), 'recurrence_trigger') ?> + formSelect('recurrence_trigger', $recurrence_trigger_list, $values, $errors) ?>
    + + formLabel(t('Factor to calculate new due date'), 'recurrence_factor') ?> + formNumber('recurrence_factor', $values, $errors) ?>
    + + formLabel(t('Timeframe to calculate new due date'), 'recurrence_timeframe') ?> + formSelect('recurrence_timeframe', $recurrence_timeframe_list, $values, $errors) ?>
    + + formLabel(t('Base date to calculate new due date'), 'recurrence_basedate') ?> + formSelect('recurrence_basedate', $recurrence_basedate_list, $values, $errors) ?>
    + + + +
    + +
    + + + + + + a(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?> + + a(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + +
    +
    +
    diff --git a/app/Template/task/show.php b/app/Template/task/show.php index 50316c9f..9d16ab74 100644 --- a/app/Template/task/show.php +++ b/app/Template/task/show.php @@ -1,4 +1,4 @@ -render('task/details', array('task' => $task, 'project' => $project)) ?> +render('task/details', array('task' => $task, 'project' => $project, 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList())) ?> render('task/time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?> render('task/show_description', array('task' => $task)) ?> render('tasklink/show', array('task' => $task, 'links' => $links, 'link_label_list' => $link_label_list)) ?> diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php index a740e939..7c82700a 100644 --- a/app/Template/task/sidebar.php +++ b/app/Template/task/sidebar.php @@ -24,6 +24,9 @@
  • a(t('Edit the description'), 'task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
  • +
  • + a(t('Edit recurrence'), 'task', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> +
  • a(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
  • @@ -61,4 +64,4 @@ - \ No newline at end of file + -- cgit v1.2.3