Merged branch 'master' of https://github.com/fguillot/kanboard
only imports conflicted
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -97,6 +96,25 @@ class DateParser extends Base
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
@@ -112,6 +130,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)
|
||||
*
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
72
app/Model/ProjectDailyStats.php
Normal file
72
app/Model/ProjectDailyStats.php
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
71
app/Model/TaskAnalytic.php
Normal file
71
app/Model/TaskAnalytic.php
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Model;
|
||||
|
||||
use SimpleValidator\Validator;
|
||||
use SimpleValidator\Validators;
|
||||
use PicoDb\Table;
|
||||
|
||||
/**
|
||||
* TaskLink model
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
$result = false;
|
||||
|
||||
if ($result) {
|
||||
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 ($original_task['swimlane_id'] != $swimlane_id) {
|
||||
$this->calculateAndSave($project_id, 0, $column_id, 1, $original_task['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');
|
||||
}
|
||||
|
||||
// The column must exists
|
||||
if (! isset($columns[$column_id])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We put our task to the new position
|
||||
if ($task_id) {
|
||||
array_splice($columns[$column_id], $position - 1, 0, $task_id);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
return $r1 && $r2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save task positions
|
||||
* Move a task to another 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 $swimlane_id
|
||||
* @param integer $original_column_id
|
||||
* @param integer $new_column_id
|
||||
* @return boolean
|
||||
*/
|
||||
private function savePositions(array $columns, $swimlane_id)
|
||||
private function saveColumnChange($project_id, $task_id, $position, $swimlane_id, $original_column_id, $new_column_id)
|
||||
{
|
||||
return $this->db->transaction(function ($db) use ($columns, $swimlane_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();
|
||||
|
||||
foreach ($columns as $column_id => $column) {
|
||||
return $r1 && $r2;
|
||||
}
|
||||
|
||||
$position = 1;
|
||||
/**
|
||||
* 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();
|
||||
|
||||
foreach ($column as $task_id) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result = $db->table(Task::TABLE)->eq('id', $task_id)->update(array(
|
||||
'position' => $position,
|
||||
'column_id' => $column_id,
|
||||
'swimlane_id' => $swimlane_id,
|
||||
));
|
||||
/**
|
||||
* Save all task positions for one 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 saveTaskPositions($project_id, $task_id, $position, $column_id, $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');
|
||||
|
||||
if (! $result) {
|
||||
return false;
|
||||
}
|
||||
$offset = 1;
|
||||
|
||||
$position++;
|
||||
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++;
|
||||
}
|
||||
});
|
||||
|
||||
// Rewrite other tasks position
|
||||
if (! $this->saveTaskPosition($current_task_id, $offset, $column_id, $swimlane_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$offset++;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')),
|
||||
);
|
||||
|
||||
@@ -38,6 +38,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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user