summaryrefslogtreecommitdiff
path: root/app/Model
diff options
context:
space:
mode:
Diffstat (limited to 'app/Model')
-rw-r--r--app/Model/Acl.php11
-rw-r--r--app/Model/Action.php1
-rw-r--r--app/Model/Authentication.php5
-rw-r--r--app/Model/Board.php11
-rw-r--r--app/Model/Color.php2
-rw-r--r--app/Model/DateParser.php51
-rw-r--r--app/Model/Notification.php4
-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
-rwxr-xr-xapp/Model/TaskDuplication.php41
-rw-r--r--app/Model/TaskFilter.php219
-rw-r--r--app/Model/TaskFinder.php22
-rw-r--r--app/Model/TaskLink.php1
-rw-r--r--app/Model/TaskModification.php3
-rw-r--r--app/Model/TaskPosition.php228
-rw-r--r--app/Model/TaskValidator.php2
-rw-r--r--app/Model/Transition.php16
-rw-r--r--app/Model/User.php14
-rw-r--r--app/Model/UserSession.php48
22 files changed, 776 insertions, 191 deletions
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
index 91ed035b..95056de6 100644
--- a/app/Model/Acl.php
+++ b/app/Model/Acl.php
@@ -18,12 +18,12 @@ class Acl extends Base
*/
private $public_acl = array(
'auth' => array('login', 'check'),
- 'user' => array('google', 'github'),
'task' => array('readonly'),
'board' => array('readonly'),
'webhook' => '*',
'ical' => '*',
'feed' => '*',
+ 'oauth' => array('google', 'github'),
);
/**
@@ -37,9 +37,14 @@ class Acl extends Base
'comment' => '*',
'file' => '*',
'project' => array('show'),
- 'projectinfo' => array('tasks', 'search', 'activity'),
+ 'listing' => '*',
+ 'activity' => '*',
'subtask' => '*',
'task' => '*',
+ 'taskduplication' => '*',
+ 'taskcreation' => '*',
+ 'taskmodification' => '*',
+ 'taskstatus' => '*',
'tasklink' => '*',
'timer' => '*',
'calendar' => array('show', 'project'),
@@ -69,7 +74,7 @@ class Acl extends Base
* @var array
*/
private $admin_acl = array(
- 'user' => array('index', 'create', 'save', 'remove'),
+ 'user' => array('index', 'create', 'save', 'remove', 'authentication'),
'config' => '*',
'link' => '*',
'project' => array('remove'),
diff --git a/app/Model/Action.php b/app/Model/Action.php
index d0607794..5e994c99 100644
--- a/app/Model/Action.php
+++ b/app/Model/Action.php
@@ -92,6 +92,7 @@ class Action extends Base
GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'),
GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'),
GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'),
+ GitlabWebhook::EVENT_ISSUE_COMMENT => t('Gitlab issue comment created'),
BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'),
BitbucketWebhook::EVENT_ISSUE_OPENED => t('Bitbucket issue opened'),
BitbucketWebhook::EVENT_ISSUE_CLOSED => t('Bitbucket issue closed'),
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/Board.php b/app/Model/Board.php
index f6f968f4..bcf77b3e 100644
--- a/app/Model/Board.php
+++ b/app/Model/Board.php
@@ -237,10 +237,11 @@ class Board extends Base
* Get all tasks sorted by columns and swimlanes
*
* @access public
- * @param integer $project_id Project id
+ * @param integer $project_id
+ * @param callable $callback
* @return array
*/
- public function getBoard($project_id)
+ public function getBoard($project_id, $callback = null)
{
$swimlanes = $this->swimlane->getSwimlanes($project_id);
$columns = $this->getColumns($project_id);
@@ -253,7 +254,11 @@ class Board extends Base
$swimlanes[$i]['nb_tasks'] = 0;
for ($j = 0; $j < $nb_columns; $j++) {
- $swimlanes[$i]['columns'][$j]['tasks'] = $this->taskFinder->getTasksByColumnAndSwimlane($project_id, $columns[$j]['id'], $swimlanes[$i]['id']);
+
+ $column_id = $columns[$j]['id'];
+ $swimlane_id = $swimlanes[$i]['id'];
+
+ $swimlanes[$i]['columns'][$j]['tasks'] = $callback === null ? $this->taskFinder->getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id) : $callback($project_id, $column_id, $swimlane_id);
$swimlanes[$i]['columns'][$j]['nb_tasks'] = count($swimlanes[$i]['columns'][$j]['tasks']);
$swimlanes[$i]['columns'][$j]['score'] = $this->getColumnSum($swimlanes[$i]['columns'][$j]['tasks'], 'score');
$swimlanes[$i]['nb_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks'];
diff --git a/app/Model/Color.php b/app/Model/Color.php
index 1fd81b85..73e5d629 100644
--- a/app/Model/Color.php
+++ b/app/Model/Color.php
@@ -147,7 +147,7 @@ class Color extends Base
*/
public function getDefaultColor()
{
- return 'yellow'; // TODO: make this parameter configurable
+ return $this->config->get('default_color', 'yellow');
}
/**
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/Notification.php b/app/Model/Notification.php
index 6a50f7ba..9628e344 100644
--- a/app/Model/Notification.php
+++ b/app/Model/Notification.php
@@ -37,7 +37,6 @@ class Notification extends Base
public function sendOverdueTaskNotifications()
{
$tasks = $this->taskFinder->getOverdueTasks();
- $projects = array();
foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) {
@@ -157,10 +156,9 @@ class Notification extends Base
*
* @access public
* @param array $user
- * @param array $event_data
* @return boolean
*/
- public function filterNone(array $user, array $event_data)
+ public function filterNone(array $user)
{
return $user['notifications_filter'] == self::FILTER_NONE;
}
diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php
index a663f921..8ac22626 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'] = $stats['count'] > 0 ? (int) ($stats['total_lead_time'] / $stats['count']) : 0;
+ $stats['avg_cycle_time'] = $stats['count'] > 0 ? (int) ($stats['total_cycle_time'] / $stats['count']) : 0;
+
+ 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'] = $stats[$column_id]['count'] > 0 ? (int) ($stats[$column_id]['time_spent'] / $stats[$column_id]['count']) : 0;
+ }
+
+ 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/TaskDuplication.php b/app/Model/TaskDuplication.php
index afcac4c7..8048f036 100755
--- a/app/Model/TaskDuplication.php
+++ b/app/Model/TaskDuplication.php
@@ -93,15 +93,22 @@ class TaskDuplication extends Base
* Duplicate a task to another project
*
* @access public
- * @param integer $task_id Task id
- * @param integer $project_id Project id
- * @return boolean|integer Duplicated task id
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return boolean|integer
*/
- public function duplicateToProject($task_id, $project_id)
+ public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
{
$values = $this->copyFields($task_id);
$values['project_id'] = $project_id;
- $values['column_id'] = $this->board->getFirstColumn($project_id);
+ $values['column_id'] = $column_id !== null ? $column_id : $this->board->getFirstColumn($project_id);
+ $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id'];
+ $values['category_id'] = $category_id !== null ? $category_id : $values['category_id'];
+ $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id'];
$this->checkDestinationProjectValues($values);
@@ -112,22 +119,26 @@ class TaskDuplication extends Base
* Move a task to another project
*
* @access public
- * @param integer $task_id Task id
- * @param integer $project_id Project id
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
* @return boolean
*/
- public function moveToProject($task_id, $project_id)
+ public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
{
$task = $this->taskFinder->getById($task_id);
$values = array();
$values['is_active'] = 1;
$values['project_id'] = $project_id;
- $values['column_id'] = $this->board->getFirstColumn($project_id);
+ $values['column_id'] = $column_id !== null ? $column_id : $this->board->getFirstColumn($project_id);
$values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1;
- $values['owner_id'] = $task['owner_id'];
- $values['category_id'] = $task['category_id'];
- $values['swimlane_id'] = $task['swimlane_id'];
+ $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id'];
+ $values['category_id'] = $category_id !== null ? $category_id : $task['category_id'];
+ $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id'];
$this->checkDestinationProjectValues($values);
@@ -144,10 +155,10 @@ class TaskDuplication extends Base
/**
* Check if the assignee and the category are available in the destination project
*
- * @access private
+ * @access public
* @param array $values
*/
- private function checkDestinationProjectValues(&$values)
+ public function checkDestinationProjectValues(array &$values)
{
// Check if the assigned user is allowed for the destination project
if ($values['owner_id'] > 0 && ! $this->projectPermission->isUserAllowed($values['project_id'], $values['owner_id'])) {
@@ -169,6 +180,8 @@ class TaskDuplication extends Base
$this->swimlane->getNameById($values['swimlane_id'])
);
}
+
+ return $values;
}
/**
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index 31080cb5..77ab1f3c 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -50,6 +50,12 @@ class TaskFilter extends Base
case 'T_DUE':
$this->filterByDueDate($value);
break;
+ case 'T_UPDATED':
+ $this->filterByModificationDate($value);
+ break;
+ case 'T_CREATED':
+ $this->filterByCreationDate($value);
+ break;
case 'T_TITLE':
$this->filterByTitle($value);
break;
@@ -68,6 +74,12 @@ class TaskFilter extends Base
case 'T_COLUMN':
$this->filterByColumnName($value);
break;
+ case 'T_REFERENCE':
+ $this->filterByReference($value);
+ break;
+ case 'T_SWIMLANE':
+ $this->filterBySwimlaneName($value);
+ break;
}
}
@@ -98,6 +110,25 @@ class TaskFilter extends Base
}
/**
+ * Create a new subtask query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function createSubtaskQuery()
+ {
+ return $this->db->table(Subtask::TABLE)
+ ->columns(
+ Subtask::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ User::TABLE.'.name',
+ User::TABLE.'.username'
+ )
+ ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE)
+ ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE);
+ }
+
+ /**
* Clone the filter
*
* @access public
@@ -115,7 +146,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)
@@ -141,6 +172,22 @@ class TaskFilter extends Base
}
/**
+ * Filter by reference
+ *
+ * @access public
+ * @param string $reference
+ * @return TaskFilter
+ */
+ public function filterByReference($reference)
+ {
+ if (! empty($reference)) {
+ $this->query->eq(Task::TABLE.'.reference', $reference);
+ }
+
+ return $this;
+ }
+
+ /**
* Filter by title
*
* @access public
@@ -154,7 +201,7 @@ class TaskFilter extends Base
}
/**
- * Filter by title
+ * Filter by title or id if the string is like #123 or an integer
*
* @access public
* @param string $title
@@ -162,7 +209,16 @@ class TaskFilter extends Base
*/
public function filterByTitle($title)
{
- $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%');
+ if (strlen($title) > 1 && $title{0} === '#' && ctype_digit(substr($title, 1))) {
+ $this->query->eq(Task::TABLE.'.id', substr($title, 1));
+ }
+ else if (ctype_digit($title)) {
+ $this->query->eq(Task::TABLE.'.id', $title);
+ }
+ else {
+ $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%');
+ }
+
return $this;
}
@@ -219,6 +275,30 @@ class TaskFilter extends Base
}
/**
+ * Filter by swimlane name
+ *
+ * @access public
+ * @param array $values List of swimlane name
+ * @return TaskFilter
+ */
+ public function filterBySwimlaneName(array $values)
+ {
+ $this->query->beginOr();
+
+ foreach ($values as $swimlane) {
+ if ($swimlane === 'default') {
+ $this->query->eq(Task::TABLE.'.swimlane_id', 0);
+ }
+ else {
+ $this->query->ilike(Swimlane::TABLE.'.name', $swimlane);
+ $this->query->addCondition(Task::TABLE.'.swimlane_id=0 AND '.Project::TABLE.'.default_swimlane '.$this->db->getDriver()->getOperator('ILIKE')." '$swimlane'");
+ }
+ }
+
+ $this->query->closeOr();
+ }
+
+ /**
* Filter by category id
*
* @access public
@@ -285,7 +365,6 @@ class TaskFilter extends Base
$this->query->beginOr();
foreach ($values as $assignee) {
-
switch ($assignee) {
case 'me':
$this->query->eq(Task::TABLE.'.owner_id', $this->userSession->getId());
@@ -299,7 +378,40 @@ class TaskFilter extends Base
}
}
+ $this->filterBySubtaskAssignee($values);
+
$this->query->closeOr();
+
+ return $this;
+ }
+
+ /**
+ * Filter by subtask assignee names
+ *
+ * @access public
+ * @param array $values List of assignees
+ * @return TaskFilter
+ */
+ public function filterBySubtaskAssignee(array $values)
+ {
+ $subtaskQuery = $this->createSubtaskQuery();
+ $subtaskQuery->beginOr();
+
+ foreach ($values as $assignee) {
+ if ($assignee === 'me') {
+ $subtaskQuery->eq(Subtask::TABLE.'.user_id', $this->userSession->getId());
+ }
+ else {
+ $subtaskQuery->ilike(User::TABLE.'.username', '%'.$assignee.'%');
+ $subtaskQuery->ilike(User::TABLE.'.name', '%'.$assignee.'%');
+ }
+ }
+
+ $subtaskQuery->closeOr();
+
+ $this->query->in(Task::TABLE.'.id', $subtaskQuery->findAllByColumn('task_id'));
+
+ return $this;
}
/**
@@ -474,6 +586,22 @@ class TaskFilter extends Base
* Filter by creation date
*
* @access public
+ * @param string $date ISO8601 date format
+ * @return TaskFilter
+ */
+ public function filterByCreationDate($date)
+ {
+ if ($date === 'recently') {
+ return $this->filterRecentlyDate(Task::TABLE.'.date_creation');
+ }
+
+ return $this->filterWithOperator(Task::TABLE.'.date_creation', $date, true);
+ }
+
+ /**
+ * Filter by creation date
+ *
+ * @access public
* @param string $start
* @param string $end
* @return TaskFilter
@@ -491,6 +619,22 @@ class TaskFilter extends Base
}
/**
+ * Filter by modification date
+ *
+ * @access public
+ * @param string $date ISO8601 date format
+ * @return TaskFilter
+ */
+ public function filterByModificationDate($date)
+ {
+ if ($date === 'recently') {
+ return $this->filterRecentlyDate(Task::TABLE.'.date_modification');
+ }
+
+ return $this->filterWithOperator(Task::TABLE.'.date_modification', $date, true);
+ }
+
+ /**
* Get all results of the filter
*
* @access public
@@ -513,6 +657,23 @@ class TaskFilter extends Base
}
/**
+ * Get swimlanes and tasks to display the board
+ *
+ * @access public
+ * @return array
+ */
+ public function getBoard($project_id)
+ {
+ $tasks = $this->filterByProject($project_id)->query->asc(Task::TABLE.'.position')->findAll();
+
+ return $this->board->getBoard($project_id, function ($project_id, $column_id, $swimlane_id) use ($tasks) {
+ return array_filter($tasks, function(array $task) use ($column_id, $swimlane_id) {
+ return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id;
+ });
+ });
+ }
+
+ /**
* Format the results to the ajax autocompletion
*
* @access public
@@ -589,10 +750,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)
{
@@ -622,9 +783,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)
{
@@ -654,7 +815,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)
{
@@ -671,11 +832,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);
@@ -703,7 +864,6 @@ class TaskFilter extends Base
);
foreach ($operators as $operator => $method) {
-
if (strpos($value, $operator) === 0) {
$value = substr($value, strlen($operator));
$this->query->$method($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
@@ -711,7 +871,32 @@ class TaskFilter extends Base
}
}
- $this->query->eq($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
+ if ($is_date) {
+ $timestamp = $this->dateParser->getTimestampFromIsoFormat($value);
+ $this->query->gte($field, $timestamp);
+ $this->query->lte($field, $timestamp + 86399);
+ }
+ else {
+ $this->query->eq($field, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Use the board_highlight_period for the "recently" keyword
+ *
+ * @access private
+ * @param string $field
+ * @return TaskFilter
+ */
+ private function filterRecentlyDate($field)
+ {
+ $duration = $this->config->get('board_highlight_period', 0);
+
+ if ($duration > 0) {
+ $this->query->gte($field, time() - $duration);
+ }
return $this;
}
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index f061cef0..a16cb69f 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -13,20 +13,6 @@ use PDO;
class TaskFinder extends Base
{
/**
- * Get query for closed tasks
- *
- * @access public
- * @param integer $project_id Project id
- * @return \PicoDb\Table
- */
- public function getClosedTaskQuery($project_id)
- {
- return $this->getExtendedQuery()
- ->eq('project_id', $project_id)
- ->eq('is_active', Task::STATUS_CLOSED);
- }
-
- /**
* Get query for assigned user tasks
*
* @access public
@@ -77,6 +63,7 @@ class TaskFinder extends Base
'tasks.date_creation',
'tasks.date_modification',
'tasks.date_completed',
+ 'tasks.date_started',
'tasks.date_due',
'tasks.color_id',
'tasks.project_id',
@@ -102,11 +89,14 @@ class TaskFinder extends Base
Category::TABLE.'.name AS category_name',
Category::TABLE.'.description AS category_description',
Board::TABLE.'.title AS column_name',
+ Swimlane::TABLE.'.name AS swimlane_name',
+ Project::TABLE.'.default_swimlane',
Project::TABLE.'.name AS project_name'
)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
->join(Category::TABLE, 'id', 'category_id', Task::TABLE)
->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
+ ->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE)
->join(Project::TABLE, 'id', 'project_id', Task::TABLE);
}
@@ -142,8 +132,8 @@ class TaskFinder extends Base
{
return $this->db
->table(Task::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', $status_id)
+ ->eq(Task::TABLE.'.project_id', $project_id)
+ ->eq(Task::TABLE.'.is_active', $status_id)
->findAll();
}
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 0c4beb2d..874633b1 100644
--- a/app/Model/TaskPosition.php
+++ b/app/Model/TaskPosition.php
@@ -26,104 +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;
+ }
- $result = $this->calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id);
+ $task = $this->taskFinder->getById($task_id);
- if ($result) {
+ // Ignore closed tasks
+ if ($task['is_active'] == Task::STATUS_CLOSED) {
+ return true;
+ }
- if ($original_task['swimlane_id'] != $swimlane_id) {
- $this->calculateAndSave($project_id, 0, $column_id, 1, $original_task['swimlane_id']);
- }
+ $result = false;
- if ($fire_events) {
- $this->fireEvents($original_task, $column_id, $position, $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 ($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;
}
/**
@@ -160,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/User.php b/app/Model/User.php
index 4c32942c..b6804abc 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -122,13 +122,13 @@ class User extends Base
}
/**
- * Get a specific user by the GitHub id
+ * Get a specific user by the Github id
*
* @access public
- * @param string $github_id GitHub user id
+ * @param string $github_id Github user id
* @return array|boolean
*/
- public function getByGitHubId($github_id)
+ public function getByGithubId($github_id)
{
if (empty($github_id)) {
return false;
@@ -377,6 +377,7 @@ class User extends Base
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Email('email', t('Email address invalid')),
new Validators\Integer('is_admin', t('This value must be an integer')),
+ new Validators\Integer('is_ldap_user', t('This value must be an integer')),
);
}
@@ -409,7 +410,12 @@ class User extends Base
new Validators\Required('username', t('The username is required')),
);
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules()));
+ if (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) {
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+ }
+ else {
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules()));
+ }
return array(
$v->execute(),
diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php
index f1f2ffee..44a9c2a2 100644
--- a/app/Model/UserSession.php
+++ b/app/Model/UserSession.php
@@ -94,4 +94,52 @@ class UserSession extends Base
{
return ! empty($this->session['user']);
}
+
+ /**
+ * Get project filters from the session
+ *
+ * @access public
+ * @param integer $project_id
+ * @return string
+ */
+ public function getFilters($project_id)
+ {
+ return ! empty($_SESSION['filters'][$project_id]) ? $_SESSION['filters'][$project_id] : 'status:open';
+ }
+
+ /**
+ * Save project filters in the session
+ *
+ * @access public
+ * @param integer $project_id
+ * @param string $filters
+ */
+ public function setFilters($project_id, $filters)
+ {
+ $_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;
+ }
}