summaryrefslogtreecommitdiff
path: root/app/Model
diff options
context:
space:
mode:
Diffstat (limited to 'app/Model')
-rw-r--r--app/Model/Authentication.php5
-rw-r--r--app/Model/DateParser.php51
-rw-r--r--app/Model/ProjectAnalytic.php94
-rw-r--r--app/Model/ProjectDailyColumnStats.php (renamed from app/Model/ProjectDailySummary.php)42
-rw-r--r--app/Model/ProjectDailyStats.php72
-rw-r--r--app/Model/TaskAnalytic.php71
-rw-r--r--app/Model/TaskCreation.php9
-rw-r--r--app/Model/TaskFilter.php24
-rw-r--r--app/Model/TaskLink.php1
-rw-r--r--app/Model/TaskModification.php3
-rw-r--r--app/Model/TaskPosition.php227
-rw-r--r--app/Model/TaskValidator.php2
-rw-r--r--app/Model/Transition.php16
-rw-r--r--app/Model/UserSession.php24
14 files changed, 497 insertions, 144 deletions
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
index 86c1c43f..31969b57 100644
--- a/app/Model/Authentication.php
+++ b/app/Model/Authentication.php
@@ -49,11 +49,6 @@ class Authentication extends Base
return false;
}
- // We update each time the RememberMe cookie tokens
- if ($this->backend('rememberMe')->hasCookie()) {
- $this->backend('rememberMe')->refresh();
- }
-
return true;
}
diff --git a/app/Model/DateParser.php b/app/Model/DateParser.php
index be79a92e..79a8385c 100644
--- a/app/Model/DateParser.php
+++ b/app/Model/DateParser.php
@@ -85,8 +85,7 @@ class DateParser extends Base
*/
public function getTimestamp($value)
{
- foreach ($this->getDateFormats() as $format) {
-
+ foreach ($this->getAllFormats() as $format) {
$timestamp = $this->getValidDate($value, $format);
if ($timestamp !== 0) {
@@ -98,6 +97,25 @@ class DateParser extends Base
}
/**
+ * Get all combinations of date/time formats
+ *
+ * @access public
+ * @return []string
+ */
+ public function getAllFormats()
+ {
+ $formats = array();
+
+ foreach ($this->getDateFormats() as $date) {
+ foreach ($this->getTimeFormats() as $time) {
+ $formats[] = $date.' '.$time;
+ }
+ }
+
+ return array_merge($formats, $this->getDateFormats());
+ }
+
+ /**
* Return the list of supported date formats (for the parser)
*
* @access public
@@ -113,6 +131,21 @@ class DateParser extends Base
}
/**
+ * Return the list of supported time formats (for the parser)
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getTimeFormats()
+ {
+ return array(
+ 'H:i',
+ 'g:i A',
+ 'g:iA',
+ );
+ }
+
+ /**
* Return the list of available date formats (for the config page)
*
* @access public
@@ -143,7 +176,7 @@ class DateParser extends Base
* Get a timetstamp from an ISO date format
*
* @access public
- * @param string $date Date format
+ * @param string $date
* @return integer
*/
public function getTimestampFromIsoFormat($date)
@@ -166,7 +199,6 @@ class DateParser extends Base
}
foreach ($fields as $field) {
-
if (! empty($values[$field])) {
$values[$field] = date($format, $values[$field]);
}
@@ -180,15 +212,16 @@ class DateParser extends Base
* Convert date (form input data)
*
* @access public
- * @param array $values Database values
- * @param string[] $fields Date fields
+ * @param array $values Database values
+ * @param string[] $fields Date fields
+ * @param boolean $keep_time Keep time or not
*/
- public function convert(array &$values, array $fields)
+ public function convert(array &$values, array $fields, $keep_time = false)
{
foreach ($fields as $field) {
-
if (! empty($values[$field]) && ! is_numeric($values[$field])) {
- $values[$field] = $this->removeTimeFromTimestamp($this->getTimestamp($values[$field]));
+ $timestamp = $this->getTimestamp($values[$field]);
+ $values[$field] = $keep_time ? $timestamp : $this->removeTimeFromTimestamp($timestamp);
}
}
}
diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php
index a663f921..1ee8a405 100644
--- a/app/Model/ProjectAnalytic.php
+++ b/app/Model/ProjectAnalytic.php
@@ -49,7 +49,7 @@ class ProjectAnalytic extends Base
* Get users repartition
*
* @access public
- * @param integer $project_id Project id
+ * @param integer $project_id
* @return array
*/
public function getUserRepartition($project_id)
@@ -87,4 +87,96 @@ class ProjectAnalytic extends Base
return array_values($metrics);
}
+
+ /**
+ * Get the average lead and cycle time
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAverageLeadAndCycleTime($project_id)
+ {
+ $stats = array(
+ 'count' => 0,
+ 'total_lead_time' => 0,
+ 'total_cycle_time' => 0,
+ 'avg_lead_time' => 0,
+ 'avg_cycle_time' => 0,
+ );
+
+ $tasks = $this->db
+ ->table(Task::TABLE)
+ ->columns('date_completed', 'date_creation', 'date_started')
+ ->eq('project_id', $project_id)
+ ->desc('id')
+ ->limit(1000)
+ ->findAll();
+
+ foreach ($tasks as &$task) {
+ $stats['count']++;
+ $stats['total_lead_time'] += ($task['date_completed'] ?: time()) - $task['date_creation'];
+ $stats['total_cycle_time'] += empty($task['date_started']) ? 0 : ($task['date_completed'] ?: time()) - $task['date_started'];
+ }
+
+ $stats['avg_lead_time'] = (int) ($stats['total_lead_time'] / $stats['count']);
+ $stats['avg_cycle_time'] = (int) ($stats['total_cycle_time'] / $stats['count']);
+
+ return $stats;
+ }
+
+ /**
+ * Get the average time spent into each column
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAverageTimeSpentByColumn($project_id)
+ {
+ $stats = array();
+ $columns = $this->board->getColumnsList($project_id);
+
+ // Get the time spent of the last move for each tasks
+ $tasks = $this->db
+ ->table(Task::TABLE)
+ ->columns('id', 'date_completed', 'date_moved', 'column_id')
+ ->eq('project_id', $project_id)
+ ->desc('id')
+ ->limit(1000)
+ ->findAll();
+
+ // Init values
+ foreach ($columns as $column_id => $column_title) {
+ $stats[$column_id] = array(
+ 'count' => 0,
+ 'time_spent' => 0,
+ 'average' => 0,
+ 'title' => $column_title,
+ );
+ }
+
+ // Get time spent foreach task/column and take into account the last move
+ foreach ($tasks as &$task) {
+ $sums = $this->transition->getTimeSpentByTask($task['id']);
+
+ if (! isset($sums[$task['column_id']])) {
+ $sums[$task['column_id']] = 0;
+ }
+
+ $sums[$task['column_id']] += ($task['date_completed'] ?: time()) - $task['date_moved'];
+
+ foreach ($sums as $column_id => $time_spent) {
+ $stats[$column_id]['count']++;
+ $stats[$column_id]['time_spent'] += $time_spent;
+ }
+ }
+
+ // Calculate average for each column
+ foreach ($columns as $column_id => $column_title) {
+ $stats[$column_id]['average'] = (int) ($stats[$column_id]['time_spent'] / $stats[$column_id]['count']);
+ }
+
+ return $stats;
+ }
}
diff --git a/app/Model/ProjectDailySummary.php b/app/Model/ProjectDailyColumnStats.php
index 04dc5629..26e9d8b7 100644
--- a/app/Model/ProjectDailySummary.php
+++ b/app/Model/ProjectDailyColumnStats.php
@@ -3,22 +3,22 @@
namespace Model;
/**
- * Project daily summary
+ * Project Daily Column Stats
*
* @package model
* @author Frederic Guillot
*/
-class ProjectDailySummary extends Base
+class ProjectDailyColumnStats extends Base
{
/**
* SQL table name
*
* @var string
*/
- const TABLE = 'project_daily_summaries';
+ const TABLE = 'project_daily_column_stats';
/**
- * Update daily totals for the project
+ * Update daily totals for the project and foreach column
*
* "total" is the number open of tasks in the column
* "score" is the sum of tasks score in the column
@@ -38,7 +38,7 @@ class ProjectDailySummary extends Base
// This call will fail if the record already exists
// (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
- $db->table(ProjectDailySummary::TABLE)->insert(array(
+ $db->table(ProjectDailyColumnStats::TABLE)->insert(array(
'day' => $date,
'project_id' => $project_id,
'column_id' => $column_id,
@@ -46,7 +46,7 @@ class ProjectDailySummary extends Base
'score' => 0,
));
- $db->table(ProjectDailySummary::TABLE)
+ $db->table(ProjectDailyColumnStats::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->eq('day', $date)
@@ -95,19 +95,19 @@ class ProjectDailySummary extends Base
*/
public function getRawMetrics($project_id, $from, $to)
{
- return $this->db->table(ProjectDailySummary::TABLE)
+ return $this->db->table(ProjectDailyColumnStats::TABLE)
->columns(
- ProjectDailySummary::TABLE.'.column_id',
- ProjectDailySummary::TABLE.'.day',
- ProjectDailySummary::TABLE.'.total',
- ProjectDailySummary::TABLE.'.score',
+ ProjectDailyColumnStats::TABLE.'.column_id',
+ ProjectDailyColumnStats::TABLE.'.day',
+ ProjectDailyColumnStats::TABLE.'.total',
+ ProjectDailyColumnStats::TABLE.'.score',
Board::TABLE.'.title AS column_title'
)
->join(Board::TABLE, 'id', 'column_id')
- ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
+ ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
- ->asc(ProjectDailySummary::TABLE.'.day')
+ ->asc(ProjectDailyColumnStats::TABLE.'.day')
->findAll();
}
@@ -122,17 +122,17 @@ class ProjectDailySummary extends Base
*/
public function getRawMetricsByDay($project_id, $from, $to)
{
- return $this->db->table(ProjectDailySummary::TABLE)
+ return $this->db->table(ProjectDailyColumnStats::TABLE)
->columns(
- ProjectDailySummary::TABLE.'.day',
- 'SUM('.ProjectDailySummary::TABLE.'.total) AS total',
- 'SUM('.ProjectDailySummary::TABLE.'.score) AS score'
+ ProjectDailyColumnStats::TABLE.'.day',
+ 'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total',
+ 'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score'
)
- ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
+ ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
- ->asc(ProjectDailySummary::TABLE.'.day')
- ->groupBy(ProjectDailySummary::TABLE.'.day')
+ ->asc(ProjectDailyColumnStats::TABLE.'.day')
+ ->groupBy(ProjectDailyColumnStats::TABLE.'.day')
->findAll();
}
@@ -160,7 +160,7 @@ class ProjectDailySummary extends Base
$aggregates = array();
// Fetch metrics for the project
- $records = $this->db->table(ProjectDailySummary::TABLE)
+ $records = $this->db->table(ProjectDailyColumnStats::TABLE)
->eq('project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
diff --git a/app/Model/ProjectDailyStats.php b/app/Model/ProjectDailyStats.php
new file mode 100644
index 00000000..56a51730
--- /dev/null
+++ b/app/Model/ProjectDailyStats.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Model;
+
+/**
+ * Project Daily Stats
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectDailyStats extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_daily_stats';
+
+ /**
+ * Update daily totals for the project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $date Record date (YYYY-MM-DD)
+ * @return boolean
+ */
+ public function updateTotals($project_id, $date)
+ {
+ $lead_cycle_time = $this->projectAnalytic->getAverageLeadAndCycleTime($project_id);
+
+ return $this->db->transaction(function($db) use ($project_id, $date, $lead_cycle_time) {
+
+ // This call will fail if the record already exists
+ // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
+ $db->table(ProjectDailyStats::TABLE)->insert(array(
+ 'day' => $date,
+ 'project_id' => $project_id,
+ 'avg_lead_time' => 0,
+ 'avg_cycle_time' => 0,
+ ));
+
+ $db->table(ProjectDailyStats::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('day', $date)
+ ->update(array(
+ 'avg_lead_time' => $lead_cycle_time['avg_lead_time'],
+ 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'],
+ ));
+ });
+ }
+
+ /**
+ * Get raw metrics for the project within a data range
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $from Start date (ISO format YYYY-MM-DD)
+ * @param string $to End date
+ * @return array
+ */
+ public function getRawMetrics($project_id, $from, $to)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns('day', 'avg_lead_time', 'avg_cycle_time')
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->asc(self::TABLE.'.day')
+ ->findAll();
+ }
+}
diff --git a/app/Model/TaskAnalytic.php b/app/Model/TaskAnalytic.php
new file mode 100644
index 00000000..33a645c1
--- /dev/null
+++ b/app/Model/TaskAnalytic.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Model;
+
+/**
+ * Task Analytic
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskAnalytic extends Base
+{
+ /**
+ * Get the time between date_creation and date_completed or now if empty
+ *
+ * @access public
+ * @param array $task
+ * @return integer
+ */
+ public function getLeadTime(array $task)
+ {
+ return ($task['date_completed'] ?: time()) - $task['date_creation'];
+ }
+
+ /**
+ * Get the time between date_started and date_completed or now if empty
+ *
+ * @access public
+ * @param array $task
+ * @return integer
+ */
+ public function getCycleTime(array $task)
+ {
+ if (empty($task['date_started'])) {
+ return 0;
+ }
+
+ return ($task['date_completed'] ?: time()) - $task['date_started'];
+ }
+
+ /**
+ * Get the average time spent in each column
+ *
+ * @access public
+ * @param array $task
+ * @return array
+ */
+ public function getTimeSpentByColumn(array $task)
+ {
+ $result = array();
+ $columns = $this->board->getColumnsList($task['project_id']);
+ $sums = $this->transition->getTimeSpentByTask($task['id']);
+
+ foreach ($columns as $column_id => $column_title) {
+
+ $time_spent = isset($sums[$column_id]) ? $sums[$column_id] : 0;
+
+ if ($task['column_id'] == $column_id) {
+ $time_spent += ($task['date_completed'] ?: time()) - $task['date_moved'];
+ }
+
+ $result[] = array(
+ 'id' => $column_id,
+ 'title' => $column_title,
+ 'time_spent' => $time_spent,
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php
index 893cbc43..e530da13 100644
--- a/app/Model/TaskCreation.php
+++ b/app/Model/TaskCreation.php
@@ -43,9 +43,10 @@ class TaskCreation extends Base
*/
public function prepare(array &$values)
{
- $this->dateParser->convert($values, array('date_due', 'date_started'));
+ $this->dateParser->convert($values, array('date_due'));
+ $this->dateParser->convert($values, array('date_started'), true);
$this->removeFields($values, array('another_task'));
- $this->resetFields($values, array('owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated'));
+ $this->resetFields($values, array('creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated'));
if (empty($values['column_id'])) {
$values['column_id'] = $this->board->getFirstColumn($values['project_id']);
@@ -59,6 +60,10 @@ class TaskCreation extends Base
$values['title'] = t('Untitled');
}
+ if ($this->userSession->isLogged()) {
+ $values['creator_id'] = $this->userSession->getId();
+ }
+
$values['swimlane_id'] = empty($values['swimlane_id']) ? 0 : $values['swimlane_id'];
$values['date_creation'] = time();
$values['date_modification'] = $values['date_creation'];
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index 6cf10a17..dfd97d9d 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -118,7 +118,7 @@ class TaskFilter extends Base
* Exclude a list of task_id
*
* @access public
- * @param array $task_ids
+ * @param integer[] $task_ids
* @return TaskFilter
*/
public function excludeTasks(array $task_ids)
@@ -641,10 +641,10 @@ class TaskFilter extends Base
* Transform results to ical events
*
* @access public
- * @param string $start_column Column name for the start date
- * @param string $end_column Column name for the end date
- * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object
- * @return Eluceo\iCal\Component\Calendar
+ * @param string $start_column Column name for the start date
+ * @param string $end_column Column name for the end date
+ * @param Calendar $vCalendar Calendar object
+ * @return Calendar
*/
public function addDateTimeIcalEvents($start_column, $end_column, Calendar $vCalendar = null)
{
@@ -674,9 +674,9 @@ class TaskFilter extends Base
* Transform results to all day ical events
*
* @access public
- * @param string $column Column name for the date
- * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object
- * @return Eluceo\iCal\Component\Calendar
+ * @param string $column Column name for the date
+ * @param Calendar $vCalendar Calendar object
+ * @return Calendar
*/
public function addAllDayIcalEvents($column = 'date_due', Calendar $vCalendar = null)
{
@@ -706,7 +706,7 @@ class TaskFilter extends Base
* @access protected
* @param array $task
* @param string $uid
- * @return Eluceo\iCal\Component\Event
+ * @return Event
*/
protected function getTaskIcalEvent(array &$task, $uid)
{
@@ -723,11 +723,11 @@ class TaskFilter extends Base
$vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']);
$vEvent->setUrl($this->helper->url->base().$this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
- if (! empty($task['creator_id'])) {
- $vEvent->setOrganizer('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local'));
+ if (! empty($task['owner_id'])) {
+ $vEvent->setOrganizer('MAILTO:'.($task['assignee_email'] ?: $task['assignee_username'].'@kanboard.local'));
}
- if (! empty($task['owner_id'])) {
+ if (! empty($task['creator_id'])) {
$attendees = new Attendees;
$attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local'));
$vEvent->setAttendees($attendees);
diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php
index 7d3a8918..3fdbd04b 100644
--- a/app/Model/TaskLink.php
+++ b/app/Model/TaskLink.php
@@ -4,7 +4,6 @@ namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
-use PicoDb\Table;
/**
* TaskLink model
diff --git a/app/Model/TaskModification.php b/app/Model/TaskModification.php
index 4691ce81..b67106e1 100644
--- a/app/Model/TaskModification.php
+++ b/app/Model/TaskModification.php
@@ -83,7 +83,8 @@ class TaskModification extends Base
*/
public function prepare(array &$values)
{
- $this->dateParser->convert($values, array('date_due', 'date_started'));
+ $this->dateParser->convert($values, array('date_due'));
+ $this->dateParser->convert($values, array('date_started'), true);
$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', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate'));
diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php
index a33a4029..874633b1 100644
--- a/app/Model/TaskPosition.php
+++ b/app/Model/TaskPosition.php
@@ -26,109 +26,176 @@ class TaskPosition extends Base
*/
public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true)
{
- $original_task = $this->taskFinder->getById($task_id);
+ if ($position < 1) {
+ return false;
+ }
+
+ $task = $this->taskFinder->getById($task_id);
// Ignore closed tasks
- if ($original_task['is_active'] == Task::STATUS_CLOSED) {
+ if ($task['is_active'] == Task::STATUS_CLOSED) {
return true;
}
- $result = $this->calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id);
-
- if ($result) {
+ $result = false;
- if ($original_task['swimlane_id'] != $swimlane_id) {
- $this->calculateAndSave($project_id, 0, $column_id, 1, $original_task['swimlane_id']);
- }
+ if ($task['swimlane_id'] != $swimlane_id) {
+ $result = $this->saveSwimlaneChange($project_id, $task_id, $position, $task['column_id'], $column_id, $task['swimlane_id'], $swimlane_id);
+ }
+ else if ($task['column_id'] != $column_id) {
+ $result = $this->saveColumnChange($project_id, $task_id, $position, $swimlane_id, $task['column_id'], $column_id);
+ }
+ else if ($task['position'] != $position) {
+ $result = $this->savePositionChange($project_id, $task_id, $position, $column_id, $swimlane_id);
+ }
- if ($fire_events) {
- $this->fireEvents($original_task, $column_id, $position, $swimlane_id);
- }
+ if ($result && $fire_events) {
+ $this->fireEvents($task, $column_id, $position, $swimlane_id);
}
return $result;
}
/**
- * Calculate the new position of all tasks
+ * Move a task to another swimlane
*
- * @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param integer $column_id Column id
- * @param integer $position Position (must be >= 1)
- * @param integer $swimlane_id Swimlane id
- * @return array|boolean
+ * @access private
+ * @param integer $project_id
+ * @param integer $task_id
+ * @param integer $position
+ * @param integer $original_column_id
+ * @param integer $new_column_id
+ * @param integer $original_swimlane_id
+ * @param integer $new_swimlane_id
+ * @return boolean
*/
- public function calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id = 0)
+ private function saveSwimlaneChange($project_id, $task_id, $position, $original_column_id, $new_column_id, $original_swimlane_id, $new_swimlane_id)
{
- // The position can't be lower than 1
- if ($position < 1) {
- return false;
- }
+ $this->db->startTransaction();
+ $r1 = $this->saveTaskPositions($project_id, $task_id, 0, $original_column_id, $original_swimlane_id);
+ $r2 = $this->saveTaskPositions($project_id, $task_id, $position, $new_column_id, $new_swimlane_id);
+ $this->db->closeTransaction();
- $board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id');
- $columns = array();
-
- // For each column fetch all tasks ordered by position
- foreach ($board as $board_column_id) {
-
- $columns[$board_column_id] = $this->db->table(Task::TABLE)
- ->eq('is_active', 1)
- ->eq('swimlane_id', $swimlane_id)
- ->eq('project_id', $project_id)
- ->eq('column_id', $board_column_id)
- ->neq('id', $task_id)
- ->asc('position')
- ->asc('id') // Fix Postgresql unit test
- ->findAllByColumn('id');
- }
+ return $r1 && $r2;
+ }
- // The column must exists
- if (! isset($columns[$column_id])) {
- return false;
- }
+ /**
+ * Move a task to another column
+ *
+ * @access private
+ * @param integer $project_id
+ * @param integer $task_id
+ * @param integer $position
+ * @param integer $swimlane_id
+ * @param integer $original_column_id
+ * @param integer $new_column_id
+ * @return boolean
+ */
+ private function saveColumnChange($project_id, $task_id, $position, $swimlane_id, $original_column_id, $new_column_id)
+ {
+ $this->db->startTransaction();
+ $r1 = $this->saveTaskPositions($project_id, $task_id, 0, $original_column_id, $swimlane_id);
+ $r2 = $this->saveTaskPositions($project_id, $task_id, $position, $new_column_id, $swimlane_id);
+ $this->db->closeTransaction();
- // We put our task to the new position
- if ($task_id) {
- array_splice($columns[$column_id], $position - 1, 0, $task_id);
- }
+ return $r1 && $r2;
+ }
+
+ /**
+ * Move a task to another position in the same column
+ *
+ * @access private
+ * @param integer $project_id
+ * @param integer $task_id
+ * @param integer $position
+ * @param integer $column_id
+ * @param integer $swimlane_id
+ * @return boolean
+ */
+ private function savePositionChange($project_id, $task_id, $position, $column_id, $swimlane_id)
+ {
+ $this->db->startTransaction();
+ $result = $this->saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id);
+ $this->db->closeTransaction();
- return $columns;
+ return $result;
}
/**
- * Save task positions
+ * Save all task positions for one column
*
* @access private
- * @param array $columns Sorted tasks
- * @param integer $swimlane_id Swimlane id
+ * @param integer $project_id
+ * @param integer $task_id
+ * @param integer $position
+ * @param integer $column_id
+ * @param integer $swimlane_id
* @return boolean
*/
- private function savePositions(array $columns, $swimlane_id)
+ private function saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id)
{
- return $this->db->transaction(function ($db) use ($columns, $swimlane_id) {
+ $tasks_ids = $this->db->table(Task::TABLE)
+ ->eq('is_active', 1)
+ ->eq('swimlane_id', $swimlane_id)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->neq('id', $task_id)
+ ->asc('position')
+ ->asc('id')
+ ->findAllByColumn('id');
+
+ $offset = 1;
+
+ foreach ($tasks_ids as $current_task_id) {
+
+ // Insert the new task
+ if ($position == $offset) {
+ if (! $this->saveTaskPosition($task_id, $offset, $column_id, $swimlane_id)) {
+ return false;
+ }
+ $offset++;
+ }
- foreach ($columns as $column_id => $column) {
+ // Rewrite other tasks position
+ if (! $this->saveTaskPosition($current_task_id, $offset, $column_id, $swimlane_id)) {
+ return false;
+ }
- $position = 1;
+ $offset++;
+ }
- foreach ($column as $task_id) {
+ // Insert the new task at the bottom and normalize bad position
+ if ($position >= $offset && ! $this->saveTaskPosition($task_id, $offset, $column_id, $swimlane_id)) {
+ return false;
+ }
- $result = $db->table(Task::TABLE)->eq('id', $task_id)->update(array(
- 'position' => $position,
- 'column_id' => $column_id,
- 'swimlane_id' => $swimlane_id,
- ));
+ return true;
+ }
- if (! $result) {
- return false;
- }
+ /**
+ * Save new task position
+ *
+ * @access private
+ * @param integer $task_id
+ * @param integer $position
+ * @param integer $column_id
+ * @param integer $swimlane_id
+ * @return boolean
+ */
+ private function saveTaskPosition($task_id, $position, $column_id, $swimlane_id)
+ {
+ $result = $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array(
+ 'position' => $position,
+ 'column_id' => $column_id,
+ 'swimlane_id' => $swimlane_id,
+ ));
+
+ if (! $result) {
+ $this->db->cancelTransaction();
+ return false;
+ }
- $position++;
- }
- }
- });
+ return true;
}
/**
@@ -165,26 +232,4 @@ class TaskPosition extends Base
$this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data));
}
}
-
- /**
- * Calculate the new position of all tasks
- *
- * @access private
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param integer $column_id Column id
- * @param integer $position Position (must be >= 1)
- * @param integer $swimlane_id Swimlane id
- * @return boolean
- */
- private function calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id)
- {
- $positions = $this->calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id);
-
- if ($positions === false || ! $this->savePositions($positions, $swimlane_id)) {
- return false;
- }
-
- return true;
- }
}
diff --git a/app/Model/TaskValidator.php b/app/Model/TaskValidator.php
index ec1383ad..95b8a26c 100644
--- a/app/Model/TaskValidator.php
+++ b/app/Model/TaskValidator.php
@@ -39,7 +39,7 @@ class TaskValidator extends Base
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()),
+ new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getAllFormats()),
new Validators\Numeric('time_spent', t('This value must be numeric')),
new Validators\Numeric('time_estimated', t('This value must be numeric')),
);
diff --git a/app/Model/Transition.php b/app/Model/Transition.php
index cb759e4a..ac3fba54 100644
--- a/app/Model/Transition.php
+++ b/app/Model/Transition.php
@@ -39,6 +39,22 @@ class Transition extends Base
}
/**
+ * Get time spent by task for each column
+ *
+ * @access public
+ * @param integer $task_id
+ * @return array
+ */
+ public function getTimeSpentByTask($task_id)
+ {
+ return $this->db
+ ->hashtable(self::TABLE)
+ ->groupBy('src_column_id')
+ ->eq('task_id', $task_id)
+ ->getAll('src_column_id', 'SUM(time_spent) AS time_spent');
+ }
+
+ /**
* Get all transitions by task
*
* @access public
diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php
index 6de4a182..44a9c2a2 100644
--- a/app/Model/UserSession.php
+++ b/app/Model/UserSession.php
@@ -118,4 +118,28 @@ class UserSession extends Base
{
$_SESSION['filters'][$project_id] = $filters;
}
+
+ /**
+ * Is board collapsed or expanded
+ *
+ * @access public
+ * @param integer $project_id
+ * @return boolean
+ */
+ public function isBoardCollapsed($project_id)
+ {
+ return ! empty($_SESSION['board_collapsed'][$project_id]) ? $_SESSION['board_collapsed'][$project_id] : false;
+ }
+
+ /**
+ * Set board display mode
+ *
+ * @access public
+ * @param integer $project_id
+ * @param boolean $collapsed
+ */
+ public function setBoardDisplayMode($project_id, $collapsed)
+ {
+ $_SESSION['board_collapsed'][$project_id] = $collapsed;
+ }
}