summaryrefslogtreecommitdiff
path: root/app/Model/Task.php
diff options
context:
space:
mode:
Diffstat (limited to 'app/Model/Task.php')
-rw-r--r--app/Model/Task.php627
1 files changed, 627 insertions, 0 deletions
diff --git a/app/Model/Task.php b/app/Model/Task.php
new file mode 100644
index 00000000..bd67d272
--- /dev/null
+++ b/app/Model/Task.php
@@ -0,0 +1,627 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use DateTime;
+
+/**
+ * Task model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Task extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'tasks';
+
+ /**
+ * Task status
+ *
+ * @var integer
+ */
+ const STATUS_OPEN = 1;
+ const STATUS_CLOSED = 0;
+
+ /**
+ * Events
+ *
+ * @var string
+ */
+ const EVENT_MOVE_COLUMN = 'task.move.column';
+ const EVENT_MOVE_POSITION = 'task.move.position';
+ const EVENT_UPDATE = 'task.update';
+ const EVENT_CREATE = 'task.create';
+ const EVENT_CLOSE = 'task.close';
+ const EVENT_OPEN = 'task.open';
+ const EVENT_CREATE_UPDATE = 'task.create_update';
+
+ /**
+ * Get available colors
+ *
+ * @access public
+ * @return array
+ */
+ public function getColors()
+ {
+ return array(
+ 'yellow' => t('Yellow'),
+ 'blue' => t('Blue'),
+ 'green' => t('Green'),
+ 'purple' => t('Purple'),
+ 'red' => t('Red'),
+ 'orange' => t('Orange'),
+ 'grey' => t('Grey'),
+ );
+ }
+
+ /**
+ * Fetch one task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param boolean $more If true, fetch all related information
+ * @return array
+ */
+ public function getById($task_id, $more = false)
+ {
+ if ($more) {
+
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.title',
+ self::TABLE.'.description',
+ self::TABLE.'.date_creation',
+ self::TABLE.'.date_completed',
+ self::TABLE.'.date_due',
+ self::TABLE.'.color_id',
+ self::TABLE.'.project_id',
+ self::TABLE.'.column_id',
+ self::TABLE.'.owner_id',
+ self::TABLE.'.position',
+ self::TABLE.'.is_active',
+ self::TABLE.'.score',
+ self::TABLE.'.category_id',
+ Category::TABLE.'.name AS category_name',
+ Project::TABLE.'.name AS project_name',
+ Board::TABLE.'.title AS column_title',
+ User::TABLE.'.username'
+ )
+ ->join(Category::TABLE, 'id', 'category_id')
+ ->join(Project::TABLE, 'id', 'project_id')
+ ->join(Board::TABLE, 'id', 'column_id')
+ ->join(User::TABLE, 'id', 'owner_id')
+ ->eq(self::TABLE.'.id', $task_id)
+ ->findOne();
+ }
+ else {
+
+ return $this->db->table(self::TABLE)->eq('id', $task_id)->findOne();
+ }
+ }
+
+ /**
+ * Count all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param array $status List of status id
+ * @return integer
+ */
+ public function countByProjectId($project_id, array $status = array(self::STATUS_OPEN, self::STATUS_CLOSED))
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Get tasks that match defined filters
+ *
+ * @access public
+ * @param array $filters Filters: [ ['column' => '...', 'operator' => '...', 'value' => '...'], ... ]
+ * @param array $sorting Sorting: [ 'column' => 'date_creation', 'direction' => 'asc']
+ * @return array
+ */
+ public function find(array $filters, array $sorting = array())
+ {
+ $table = $this->db
+ ->table(self::TABLE)
+ ->columns(
+ '(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments',
+ 'tasks.id',
+ 'tasks.title',
+ 'tasks.description',
+ 'tasks.date_creation',
+ 'tasks.date_completed',
+ 'tasks.date_due',
+ 'tasks.color_id',
+ 'tasks.project_id',
+ 'tasks.column_id',
+ 'tasks.owner_id',
+ 'tasks.position',
+ 'tasks.is_active',
+ 'tasks.score',
+ 'tasks.category_id',
+ 'users.username'
+ )
+ ->join('users', 'id', 'owner_id');
+
+ foreach ($filters as $key => $filter) {
+
+ if ($key === 'or') {
+
+ $table->beginOr();
+
+ foreach ($filter as $subfilter) {
+ $table->$subfilter['operator']($subfilter['column'], $subfilter['value']);
+ }
+
+ $table->closeOr();
+ }
+ else if (isset($filter['operator']) && isset($filter['column']) && isset($filter['value'])) {
+ $table->$filter['operator']($filter['column'], $filter['value']);
+ }
+ }
+
+ if (empty($sorting)) {
+ $table->orderBy('tasks.position', 'ASC');
+ }
+ else {
+ $table->orderBy($sorting['column'], $sorting['direction']);
+ }
+
+ return $table->findAll();
+ }
+
+ /**
+ * Count the number of tasks for a given column and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param array $status List of status id
+ * @return integer
+ */
+ public function countByColumnId($project_id, $column_id, array $status = array(self::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Duplicate a task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function duplicate($task_id)
+ {
+ $this->db->startTransaction();
+
+ $boardModel = new Board($this->db, $this->event);
+
+ // Get the original task
+ $task = $this->getById($task_id);
+
+ // Cleanup data
+ unset($task['id']);
+ unset($task['date_completed']);
+
+ // Assign new values
+ $task['date_creation'] = time();
+ $task['is_active'] = 1;
+ $task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']);
+
+ // Save task
+ if (! $this->db->table(self::TABLE)->save($task)) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
+ $task_id = $this->db->getConnection()->getLastId();
+
+ $this->db->closeTransaction();
+
+ // Trigger events
+ $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task);
+ $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task);
+
+ return $task_id;
+ }
+
+ /**
+ * Duplicate a task to another project (always copy to the first column)
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param integer $project_id Destination project id
+ * @return boolean
+ */
+ public function duplicateToAnotherProject($task_id, $project_id)
+ {
+ $this->db->startTransaction();
+
+ $boardModel = new Board($this->db, $this->event);
+
+ // Get the original task
+ $task = $this->getById($task_id);
+
+ // Cleanup data
+ unset($task['id']);
+ unset($task['date_completed']);
+
+ // Assign new values
+ $task['date_creation'] = time();
+ $task['owner_id'] = 0;
+ $task['category_id'] = 0;
+ $task['is_active'] = 1;
+ $task['column_id'] = $boardModel->getFirstColumn($project_id);
+ $task['project_id'] = $project_id;
+ $task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']);
+
+ // Save task
+ if (! $this->db->table(self::TABLE)->save($task)) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
+ $task_id = $this->db->getConnection()->getLastId();
+
+ $this->db->closeTransaction();
+
+ // Trigger events
+ $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task);
+ $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task);
+
+ return $task_id;
+ }
+
+ /**
+ * Create a task
+ *
+ * @access public
+ * @param array $values Form values
+ * @return boolean
+ */
+ public function create(array $values)
+ {
+ $this->db->startTransaction();
+
+ // Prepare data
+ if (isset($values['another_task'])) {
+ unset($values['another_task']);
+ }
+
+ if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) {
+ $values['date_due'] = $this->parseDate($values['date_due']);
+ }
+
+ $values['date_creation'] = time();
+ $values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']);
+
+ // Save task
+ if (! $this->db->table(self::TABLE)->save($values)) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
+ $task_id = $this->db->getConnection()->getLastId();
+
+ $this->db->closeTransaction();
+
+ // Trigger events
+ $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
+ $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values);
+
+ return $task_id;
+ }
+
+ /**
+ * Update a task
+ *
+ * @access public
+ * @param array $values Form values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ // Prepare data
+ if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) {
+ $values['date_due'] = $this->parseDate($values['date_due']);
+ }
+
+ $original_task = $this->getById($values['id']);
+
+ if ($original_task === false) {
+ return false;
+ }
+
+ $updated_task = $values;
+ unset($updated_task['id']);
+
+ $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task);
+
+ // Trigger events
+ if ($result) {
+
+ $events = array();
+
+ if (! in_array($this->event->getLastTriggeredEvent(), array(self::EVENT_CREATE_UPDATE))) {
+ $events[] = self::EVENT_CREATE_UPDATE;
+ $events[] = self::EVENT_UPDATE;
+ }
+
+ if (isset($values['column_id']) && $original_task['column_id'] != $values['column_id']) {
+ $events[] = self::EVENT_MOVE_COLUMN;
+ }
+ else if (isset($values['position']) && $original_task['position'] != $values['position']) {
+ $events[] = self::EVENT_MOVE_POSITION;
+ }
+
+ $event_data = array_merge($original_task, $values);
+ $event_data['task_id'] = $original_task['id'];
+
+ foreach ($events as $event) {
+ $this->event->trigger($event, $event_data);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Mark a task closed
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function close($task_id)
+ {
+ $result = $this->db
+ ->table(self::TABLE)
+ ->eq('id', $task_id)
+ ->update(array(
+ 'is_active' => 0,
+ 'date_completed' => time()
+ ));
+
+ if ($result) {
+ $this->event->trigger(self::EVENT_CLOSE, array('task_id' => $task_id) + $this->getById($task_id));
+ }
+
+ return $result;
+ }
+
+ /**
+ * Mark a task open
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function open($task_id)
+ {
+ $result = $this->db
+ ->table(self::TABLE)
+ ->eq('id', $task_id)
+ ->update(array(
+ 'is_active' => 1,
+ 'date_completed' => ''
+ ));
+
+ if ($result) {
+ $this->event->trigger(self::EVENT_OPEN, array('task_id' => $task_id) + $this->getById($task_id));
+ }
+
+ return $result;
+ }
+
+ /**
+ * Remove a task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function remove($task_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $task_id)->remove();
+ }
+
+ /**
+ * Move a task to another column or to another position
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param integer $column_id Column id
+ * @param integer $position Position (must be greater than 1)
+ * @return boolean
+ */
+ public function move($task_id, $column_id, $position)
+ {
+ return $this->update(array(
+ 'id' => $task_id,
+ 'column_id' => $column_id,
+ 'position' => $position,
+ ));
+ }
+
+ /**
+ * Validate task creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('color_id', t('The color is required')),
+ new Validators\Required('project_id', t('The project is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\Required('column_id', t('The column is required')),
+ new Validators\Integer('column_id', t('This value must be an integer')),
+ new Validators\Integer('owner_id', t('This value must be an integer')),
+ new Validators\Integer('score', t('This value must be an integer')),
+ new Validators\Required('title', t('The title is required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
+ new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate description creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateDescriptionCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('The id is required')),
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Required('description', t('The description is required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate task modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('The id is required')),
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Required('color_id', t('The color is required')),
+ new Validators\Required('project_id', t('The project is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\Required('column_id', t('The column is required')),
+ new Validators\Integer('column_id', t('This value must be an integer')),
+ new Validators\Integer('owner_id', t('This value must be an integer')),
+ new Validators\Integer('score', t('This value must be an integer')),
+ new Validators\Required('title', t('The title is required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
+ new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate assignee change
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateAssigneeModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('The id is required')),
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Required('project_id', t('The project is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\Required('owner_id', t('This value is required')),
+ new Validators\Integer('owner_id', t('This value must be an integer')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Return a timestamp if the given date format is correct otherwise return 0
+ *
+ * @access public
+ * @param string $value Date to parse
+ * @param string $format Date format
+ * @return integer
+ */
+ public function getValidDate($value, $format)
+ {
+ $date = DateTime::createFromFormat($format, $value);
+
+ if ($date !== false) {
+ $errors = DateTime::getLastErrors();
+ if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) {
+ $timestamp = $date->getTimestamp();
+ return $timestamp > 0 ? $timestamp : 0;
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Parse a date ad return a unix timestamp, try different date formats
+ *
+ * @access public
+ * @param string $value Date to parse
+ * @return integer
+ */
+ public function parseDate($value)
+ {
+ foreach ($this->getDateFormats() as $format) {
+
+ $timestamp = $this->getValidDate($value, $format);
+
+ if ($timestamp !== 0) {
+ return $timestamp;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the list of supported date formats
+ *
+ * @access public
+ * @return array
+ */
+ public function getDateFormats()
+ {
+ return array(
+ t('m/d/Y'),
+ 'Y-m-d',
+ 'Y_m_d',
+ );
+ }
+}