Merged branch 'master' of https://github.com/fguillot/kanboard
only imports conflicted
This commit is contained in:
commit
a85a1c6132
|
|
@ -12,17 +12,17 @@ class Project extends Base
|
|||
{
|
||||
public function getProjectById($project_id)
|
||||
{
|
||||
return $this->project->getById($project_id);
|
||||
return $this->formatProject($this->project->getById($project_id));
|
||||
}
|
||||
|
||||
public function getProjectByName($name)
|
||||
{
|
||||
return $this->project->getByName($name);
|
||||
return $this->formatProject($this->project->getByName($name));
|
||||
}
|
||||
|
||||
public function getAllProjects()
|
||||
{
|
||||
return $this->project->getAll();
|
||||
return $this->formatProjects($this->project->getAll());
|
||||
}
|
||||
|
||||
public function removeProject($project_id)
|
||||
|
|
@ -82,4 +82,28 @@ class Project extends Base
|
|||
list($valid,) = $this->project->validateModification($values);
|
||||
return $valid && $this->project->update($values);
|
||||
}
|
||||
|
||||
private function formatProject($project)
|
||||
{
|
||||
if (! empty($project)) {
|
||||
$project['url'] = array(
|
||||
'board' => $this->helper->url->base().$this->helper->url->to('board', 'show', array('project_id' => $project['id'])),
|
||||
'calendar' => $this->helper->url->base().$this->helper->url->to('calendar', 'show', array('project_id' => $project['id'])),
|
||||
'list' => $this->helper->url->base().$this->helper->url->to('listing', 'show', array('project_id' => $project['id'])),
|
||||
);
|
||||
}
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
private function formatProjects($projects)
|
||||
{
|
||||
if (! empty($projects)) {
|
||||
foreach ($projects as &$project) {
|
||||
$project = $this->formatProject($project);
|
||||
}
|
||||
}
|
||||
|
||||
return $projects;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,17 +14,17 @@ class Task extends Base
|
|||
{
|
||||
public function getTask($task_id)
|
||||
{
|
||||
return $this->taskFinder->getById($task_id);
|
||||
return $this->formatTask($this->taskFinder->getById($task_id));
|
||||
}
|
||||
|
||||
public function getTaskByReference($project_id, $reference)
|
||||
{
|
||||
return $this->taskFinder->getByReference($project_id, $reference);
|
||||
return $this->formatTask($this->taskFinder->getByReference($project_id, $reference));
|
||||
}
|
||||
|
||||
public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN)
|
||||
{
|
||||
return $this->taskFinder->getAll($project_id, $status_id);
|
||||
return $this->formatTasks($this->taskFinder->getAll($project_id, $status_id));
|
||||
}
|
||||
|
||||
public function getOverdueTasks()
|
||||
|
|
@ -115,4 +115,24 @@ class Task extends Base
|
|||
list($valid) = $this->taskValidator->validateApiModification($values);
|
||||
return $valid && $this->taskModification->update($values);
|
||||
}
|
||||
|
||||
private function formatTask($task)
|
||||
{
|
||||
if (! empty($task)) {
|
||||
$task['url'] = $this->helper->url->base().$this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']));
|
||||
}
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
private function formatTasks($tasks)
|
||||
{
|
||||
if (! empty($tasks)) {
|
||||
foreach ($tasks as &$task) {
|
||||
$task = $this->formatTask($task);
|
||||
}
|
||||
}
|
||||
|
||||
return $tasks;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,31 +118,6 @@ class RememberMe extends Base
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the database and the cookie with a new sequence
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
$credentials = $this->readCookie();
|
||||
|
||||
if ($credentials !== false) {
|
||||
|
||||
$record = $this->find($credentials['token'], $credentials['sequence']);
|
||||
|
||||
if ($record) {
|
||||
|
||||
// Update the sequence
|
||||
$this->writeCookie(
|
||||
$record['token'],
|
||||
$this->update($record['token']),
|
||||
$record['expiration']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a session record
|
||||
*
|
||||
|
|
@ -197,9 +172,10 @@ class RememberMe extends Base
|
|||
|
||||
$this->cleanup($user_id);
|
||||
|
||||
$this->db
|
||||
->table(self::TABLE)
|
||||
->insert(array(
|
||||
$this
|
||||
->db
|
||||
->table(self::TABLE)
|
||||
->insert(array(
|
||||
'user_id' => $user_id,
|
||||
'ip' => $ip,
|
||||
'user_agent' => $user_agent,
|
||||
|
|
@ -207,7 +183,7 @@ class RememberMe extends Base
|
|||
'sequence' => $sequence,
|
||||
'expiration' => $expiration,
|
||||
'date_creation' => time(),
|
||||
));
|
||||
));
|
||||
|
||||
return array(
|
||||
'token' => $token,
|
||||
|
|
|
|||
|
|
@ -11,16 +11,17 @@ use Symfony\Component\Console\Command\Command;
|
|||
* @package console
|
||||
* @author Frederic Guillot
|
||||
*
|
||||
* @property \Model\Notification $notification
|
||||
* @property \Model\Project $project
|
||||
* @property \Model\ProjectPermission $projectPermission
|
||||
* @property \Model\ProjectAnalytic $projectAnalytic
|
||||
* @property \Model\ProjectDailySummary $projectDailySummary
|
||||
* @property \Model\SubtaskExport $subtaskExport
|
||||
* @property \Model\Task $task
|
||||
* @property \Model\TaskExport $taskExport
|
||||
* @property \Model\TaskFinder $taskFinder
|
||||
* @property \Model\Transition $transition
|
||||
* @property \Model\Notification $notification
|
||||
* @property \Model\Project $project
|
||||
* @property \Model\ProjectPermission $projectPermission
|
||||
* @property \Model\ProjectAnalytic $projectAnalytic
|
||||
* @property \Model\ProjectDailyColumnStats $projectDailyColumnStats
|
||||
* @property \Model\ProjectDailyStats $projectDailyColumnStats
|
||||
* @property \Model\SubtaskExport $subtaskExport
|
||||
* @property \Model\Task $task
|
||||
* @property \Model\TaskExport $taskExport
|
||||
* @property \Model\TaskFinder $taskFinder
|
||||
* @property \Model\Transition $transition
|
||||
*/
|
||||
abstract class Base extends Command
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ use Symfony\Component\Console\Input\InputArgument;
|
|||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ProjectDailySummaryExport extends Base
|
||||
class ProjectDailyColumnStatsExport extends Base
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('export:daily-project-summary')
|
||||
->setDescription('Daily project summary CSV export (number of tasks per column and per day)')
|
||||
->setName('export:daily-project-column-stats')
|
||||
->setDescription('Daily project column stats CSV export (number of tasks per column and per day)')
|
||||
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
|
||||
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
|
||||
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
|
||||
|
|
@ -21,7 +21,7 @@ class ProjectDailySummaryExport extends Base
|
|||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$data = $this->projectDailySummary->getAggregatedMetrics(
|
||||
$data = $this->projectDailyColumnStats->getAggregatedMetrics(
|
||||
$input->getArgument('project_id'),
|
||||
$input->getArgument('start_date'),
|
||||
$input->getArgument('end_date')
|
||||
|
|
@ -6,13 +6,13 @@ use Model\Project;
|
|||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ProjectDailySummaryCalculation extends Base
|
||||
class ProjectDailyStatsCalculation extends Base
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('projects:daily-summary')
|
||||
->setDescription('Calculate daily summary data for all projects');
|
||||
->setName('projects:daily-stats')
|
||||
->setDescription('Calculate daily statistics for all projects');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
|
|
@ -21,7 +21,8 @@ class ProjectDailySummaryCalculation extends Base
|
|||
|
||||
foreach ($projects as $project) {
|
||||
$output->writeln('Run calculation for '.$project['name']);
|
||||
$this->projectDailySummary->updateTotals($project['id'], date('Y-m-d'));
|
||||
$this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d'));
|
||||
$this->projectDailyStats->updateTotals($project['id'], date('Y-m-d'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,4 +26,20 @@ class Activity extends Base
|
|||
'title' => t('%s\'s activity', $project['name'])
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display task activities
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function task()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->taskLayout('activity/task', array(
|
||||
'title' => $task['title'],
|
||||
'task' => $task,
|
||||
'events' => $this->projectActivity->getTask($task['id']),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
namespace Controller;
|
||||
|
||||
/**
|
||||
* Project Anaytic controller
|
||||
* Project Analytic controller
|
||||
*
|
||||
* @package controller
|
||||
* @author Frederic Guillot
|
||||
|
|
@ -26,6 +26,56 @@ class Analytic extends Base
|
|||
return $this->template->layout('analytic/layout', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show average Lead and Cycle time
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function leadAndCycleTime()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
$this->projectDailyStats->updateTotals($project['id'], date('Y-m-d'));
|
||||
|
||||
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
|
||||
$to = $this->request->getStringParam('to', date('Y-m-d'));
|
||||
|
||||
if (! empty($values)) {
|
||||
$from = $values['from'];
|
||||
$to = $values['to'];
|
||||
}
|
||||
|
||||
$this->response->html($this->layout('analytic/lead_cycle_time', array(
|
||||
'values' => array(
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
),
|
||||
'project' => $project,
|
||||
'average' => $this->projectAnalytic->getAverageLeadAndCycleTime($project['id']),
|
||||
'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to),
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
'title' => t('Lead and Cycle time for "%s"', $project['name']),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show average time spent by column
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function averageTimeByColumn()
|
||||
{
|
||||
$project = $this->getProject();
|
||||
|
||||
$this->response->html($this->layout('analytic/avg_time_columns', array(
|
||||
'project' => $project,
|
||||
'metrics' => $this->projectAnalytic->getAverageTimeSpentByColumn($project['id']),
|
||||
'title' => t('Average time spent into each column for "%s"', $project['name']),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show tasks distribution graph
|
||||
*
|
||||
|
|
@ -88,6 +138,8 @@ class Analytic extends Base
|
|||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
|
||||
$this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d'));
|
||||
|
||||
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
|
||||
$to = $this->request->getStringParam('to', date('Y-m-d'));
|
||||
|
||||
|
|
@ -96,7 +148,7 @@ class Analytic extends Base
|
|||
$to = $values['to'];
|
||||
}
|
||||
|
||||
$display_graph = $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2;
|
||||
$display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2;
|
||||
|
||||
$this->response->html($this->layout($template, array(
|
||||
'values' => array(
|
||||
|
|
@ -104,7 +156,7 @@ class Analytic extends Base
|
|||
'to' => $to,
|
||||
),
|
||||
'display_graph' => $display_graph,
|
||||
'metrics' => $display_graph ? $this->projectDailySummary->getAggregatedMetrics($project['id'], $from, $to, $column) : array(),
|
||||
'metrics' => $display_graph ? $this->projectDailyColumnStats->getAggregatedMetrics($project['id'], $from, $to, $column) : array(),
|
||||
'project' => $project,
|
||||
'date_format' => $this->config->get('application_date_format'),
|
||||
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||
|
|
|
|||
|
|
@ -310,4 +310,28 @@ class Board extends Base
|
|||
'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable collapsed mode
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function collapse()
|
||||
{
|
||||
$project_id = $this->request->getIntegerParam('project_id');
|
||||
$this->userSession->setBoardDisplayMode($project_id, true);
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project_id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable expanded mode
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function expand()
|
||||
{
|
||||
$project_id = $this->request->getIntegerParam('project_id');
|
||||
$this->userSession->setBoardDisplayMode($project_id, false);
|
||||
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project_id)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class Export extends Base
|
|||
*/
|
||||
public function summary()
|
||||
{
|
||||
$this->common('projectDailySummary', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export'));
|
||||
$this->common('ProjectDailyColumnStats', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ class Ical extends Base
|
|||
*/
|
||||
private function renderCalendar(TaskFilter $filter, iCalendar $calendar)
|
||||
{
|
||||
$start = $this->request->getStringParam('start', strtotime('-1 month'));
|
||||
$end = $this->request->getStringParam('end', strtotime('+2 months'));
|
||||
$start = $this->request->getStringParam('start', strtotime('-2 month'));
|
||||
$end = $this->request->getStringParam('end', strtotime('+6 months'));
|
||||
|
||||
// Tasks
|
||||
if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class Task extends Base
|
|||
'time_spent' => $task['time_spent'] ?: '',
|
||||
);
|
||||
|
||||
$this->dateParser->format($values, array('date_started'));
|
||||
$this->dateParser->format($values, array('date_started'), 'Y-m-d H:i');
|
||||
|
||||
$this->response->html($this->taskLayout('task/show', array(
|
||||
'project' => $this->project->getById($task['project_id']),
|
||||
|
|
@ -88,19 +88,20 @@ class Task extends Base
|
|||
}
|
||||
|
||||
/**
|
||||
* Display task activities
|
||||
* Display task analytics
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function activites()
|
||||
public function analytics()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
|
||||
$this->response->html($this->taskLayout('task/activity', array(
|
||||
$this->response->html($this->taskLayout('task/analytics', array(
|
||||
'title' => $task['title'],
|
||||
'task' => $task,
|
||||
'ajax' => $this->request->isAjax(),
|
||||
'events' => $this->projectActivity->getTask($task['id']),
|
||||
'lead_time' => $this->taskAnalytic->getLeadTime($task),
|
||||
'cycle_time' => $this->taskAnalytic->getCycleTime($task),
|
||||
'time_spent_columns' => $this->taskAnalytic->getTimeSpentByColumn($task),
|
||||
)));
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +152,6 @@ class Task extends Base
|
|||
{
|
||||
$project = $this->getProject();
|
||||
$values = $this->request->getValues();
|
||||
$values['creator_id'] = $this->userSession->getId();
|
||||
|
||||
list($valid, $errors) = $this->taskValidator->validateCreation($values);
|
||||
|
||||
|
|
@ -618,4 +618,16 @@ class Task extends Base
|
|||
'transitions' => $this->transition->getAllByTask($task['id']),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set automatically the start date
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
$task = $this->getTask();
|
||||
$this->taskModification->update(array('id' => $task['id'], 'date_started' => time()));
|
||||
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ use Pimple\Container;
|
|||
* @property \Model\ProjectActivity $projectActivity
|
||||
* @property \Model\ProjectAnalytic $projectAnalytic
|
||||
* @property \Model\ProjectDuplication $projectDuplication
|
||||
* @property \Model\ProjectDailySummary $projectDailySummary
|
||||
* @property \Model\ProjectDailyColumnStats $projectDailyColumnStats
|
||||
* @property \Model\ProjectDailyStats $projectDailyStats
|
||||
* @property \Model\ProjectIntegration $projectIntegration
|
||||
* @property \Model\ProjectPermission $projectPermission
|
||||
* @property \Model\Subtask $subtask
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Core;
|
||||
|
||||
use Pimple\Container;
|
||||
|
||||
/**
|
||||
* Helper base class
|
||||
*
|
||||
|
|
@ -10,7 +12,7 @@ namespace Core;
|
|||
*
|
||||
* @property \Helper\App $app
|
||||
* @property \Helper\Asset $asset
|
||||
* @property \Helper\Datetime $datetime
|
||||
* @property \Helper\Dt $dt
|
||||
* @property \Helper\File $file
|
||||
* @property \Helper\Form $form
|
||||
* @property \Helper\Subtask $subtask
|
||||
|
|
@ -19,16 +21,34 @@ namespace Core;
|
|||
* @property \Helper\Url $url
|
||||
* @property \Helper\User $user
|
||||
*/
|
||||
class Helper extends Base
|
||||
class Helper
|
||||
{
|
||||
/**
|
||||
* Helper instances
|
||||
*
|
||||
* @static
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private static $helpers = array();
|
||||
private $helpers = array();
|
||||
|
||||
/**
|
||||
* Container instance
|
||||
*
|
||||
* @access protected
|
||||
* @var \Pimple\Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load automatically helpers
|
||||
|
|
@ -39,12 +59,12 @@ class Helper extends Base
|
|||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
if (! isset(self::$helpers[$name])) {
|
||||
if (! isset($this->helpers[$name])) {
|
||||
$class = '\Helper\\'.ucfirst($name);
|
||||
self::$helpers[$name] = new $class($this->container);
|
||||
$this->helpers[$name] = new $class($this->container);
|
||||
}
|
||||
|
||||
return self::$helpers[$name];
|
||||
return $this->helpers[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Helper;
|
||||
|
||||
/**
|
||||
* Board Helper
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Board extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Return true if tasks are collapsed
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return boolean
|
||||
*/
|
||||
public function isCollapsed($project_id)
|
||||
{
|
||||
return $this->userSession->isBoardCollapsed($project_id);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,14 +2,34 @@
|
|||
|
||||
namespace Helper;
|
||||
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* DateTime helpers
|
||||
*
|
||||
* @package helper
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Datetime extends \Core\Base
|
||||
class Dt extends \Core\Base
|
||||
{
|
||||
/**
|
||||
* Get duration in seconds into human format
|
||||
*
|
||||
* @access public
|
||||
* @param integer $seconds
|
||||
* @return string
|
||||
*/
|
||||
public function duration($seconds)
|
||||
{
|
||||
if ($seconds == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$dtF = new DateTime("@0");
|
||||
$dtT = new DateTime("@$seconds");
|
||||
return $dtF->diff($dtT)->format('%a days, %h hours, %i minutes and %s seconds');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the age of an item in quasi human readable format.
|
||||
* It's in this format: <1h , NNh, NNd
|
||||
|
|
@ -99,6 +99,10 @@ class Url extends \Core\Base
|
|||
*/
|
||||
public function server()
|
||||
{
|
||||
if (empty($_SERVER['SERVER_NAME'])) {
|
||||
return 'http://localhost/';
|
||||
}
|
||||
|
||||
$self = str_replace('\\', '/', dirname($_SERVER['PHP_SELF']));
|
||||
|
||||
$url = Request::isHTTPS() ? 'https://' : 'http://';
|
||||
|
|
|
|||
|
|
@ -39,6 +39,25 @@ class SlackWebhook extends \Core\Base
|
|||
return $options['slack_webhook_url'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optional Slack channel
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel($project_id)
|
||||
{
|
||||
$channel = $this->config->get('integration_slack_webhook_channel');
|
||||
|
||||
if (! empty($channel)) {
|
||||
return $channel;
|
||||
}
|
||||
|
||||
$options = $this->projectIntegration->getParameters($project_id);
|
||||
return $options['slack_webhook_channel'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to the incoming Slack webhook
|
||||
*
|
||||
|
|
@ -69,6 +88,11 @@ class SlackWebhook extends \Core\Base
|
|||
$payload['text'] .= '|'.t('view the task on Kanboard').'>';
|
||||
}
|
||||
|
||||
$channel = $this->getChannel($project_id);
|
||||
if (! empty($channel)) {
|
||||
$payload['channel'] = $channel;
|
||||
}
|
||||
|
||||
$this->httpClient->postJson($this->getWebhookUrl($project_id), $payload);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,15 +20,15 @@ return array(
|
|||
'Red' => 'Röd',
|
||||
'Orange' => 'Orange',
|
||||
'Grey' => 'Grå',
|
||||
// 'Brown' => '',
|
||||
// 'Deep Orange' => '',
|
||||
// 'Dark Grey' => '',
|
||||
// 'Pink' => '',
|
||||
// 'Teal' => '',
|
||||
// 'Cyan' => '',
|
||||
// 'Lime' => '',
|
||||
// 'Light Green' => '',
|
||||
// 'Amber' => '',
|
||||
'Brown' => 'Brun',
|
||||
'Deep Orange' => 'Mörkorange',
|
||||
'Dark Grey' => 'Mörkgrå',
|
||||
'Pink' => 'Rosa',
|
||||
'Teal' => 'Grönblå',
|
||||
'Cyan' => 'Cyan',
|
||||
'Lime' => 'Lime',
|
||||
'Light Green' => 'Ljusgrön',
|
||||
'Amber' => 'Bärnsten',
|
||||
'Save' => 'Spara',
|
||||
'Login' => 'Login',
|
||||
'Official website:' => 'Officiell webbsida:',
|
||||
|
|
@ -743,7 +743,7 @@ return array(
|
|||
'Move the task to another column when assigned to a user' => 'Flytta uppgiften till en annan kolumn när den tilldelats en användare',
|
||||
'Move the task to another column when assignee is cleared' => 'Flytta uppgiften till en annan kolumn när tilldelningen tas bort.',
|
||||
'Source column' => 'Källkolumn',
|
||||
// 'Show subtask estimates (forecast of future work)' => '',
|
||||
'Show subtask estimates (forecast of future work)' => 'Visa uppskattningar för deluppgifter (prognos för framtida arbete)',
|
||||
'Transitions' => 'Övergångar',
|
||||
'Executer' => 'Verkställare',
|
||||
'Time spent in the column' => 'Tid i kolumnen.',
|
||||
|
|
@ -788,187 +788,187 @@ return array(
|
|||
'Burndown chart for "%s"' => 'Burndown diagram för "%s"',
|
||||
'Burndown chart' => 'Burndown diagram',
|
||||
'This chart show the task complexity over the time (Work Remaining).' => 'Diagrammet visar uppgiftens svårighet över tid (återstående arbete).',
|
||||
// 'Screenshot taken %s' => '',
|
||||
// 'Add a screenshot' => '',
|
||||
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
|
||||
// 'Screenshot uploaded successfully.' => '',
|
||||
'Screenshot taken %s' => 'Skärmdump tagen %s',
|
||||
'Add a screenshot' => 'Lägg till en skärmdump',
|
||||
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Ta en skärmdump och tryck CTRL+V för att klistra in här.',
|
||||
'Screenshot uploaded successfully.' => 'Skärmdumpen laddades upp.',
|
||||
'SEK - Swedish Krona' => 'SEK - Svensk Krona',
|
||||
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
|
||||
// 'Identifier' => '',
|
||||
// 'Postmark (incoming emails)' => '',
|
||||
// 'Help on Postmark integration' => '',
|
||||
// 'Mailgun (incoming emails)' => '',
|
||||
// 'Help on Mailgun integration' => '',
|
||||
'The project identifier is an optional alphanumeric code used to identify your project.' => 'Projektidentifieraren är en valbar alfanumerisk kod som används för att identifiera ditt projekt.',
|
||||
'Identifier' => 'Identifierare',
|
||||
'Postmark (incoming emails)' => 'Postmark (inkommande e-post)',
|
||||
'Help on Postmark integration' => 'Hjälp för Postmark integration',
|
||||
'Mailgun (incoming emails)' => 'Mailgrun (inkommande e-post)',
|
||||
'Help on Mailgun integration' => 'Hjälp för Mailgrun integration',
|
||||
// 'Sendgrid (incoming emails)' => '',
|
||||
// 'Help on Sendgrid integration' => '',
|
||||
// 'Disable two factor authentication' => '',
|
||||
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
|
||||
// 'Edit link' => '',
|
||||
// 'Start to type task title...' => '',
|
||||
// 'A task cannot be linked to itself' => '',
|
||||
// 'The exact same link already exists' => '',
|
||||
// 'Recurrent task is scheduled to be generated' => '',
|
||||
// 'Recurring information' => '',
|
||||
// 'Score' => '',
|
||||
// 'The identifier must be unique' => '',
|
||||
// 'This linked task id doesn\'t exists' => '',
|
||||
// 'This value must be alphanumeric' => '',
|
||||
// 'Edit recurrence' => '',
|
||||
// 'Generate recurrent task' => '',
|
||||
// 'Trigger to generate recurrent task' => '',
|
||||
// 'Factor to calculate new due date' => '',
|
||||
// 'Timeframe to calculate new due date' => '',
|
||||
// 'Base date to calculate new due date' => '',
|
||||
// 'Action date' => '',
|
||||
// 'Base date to calculate new due date: ' => '',
|
||||
// 'This task has created this child task: ' => '',
|
||||
// 'Day(s)' => '',
|
||||
// 'Existing due date' => '',
|
||||
// 'Factor to calculate new due date: ' => '',
|
||||
// 'Month(s)' => '',
|
||||
// 'Recurrence' => '',
|
||||
// 'This task has been created by: ' => '',
|
||||
// 'Recurrent task has been generated:' => '',
|
||||
// 'Timeframe to calculate new due date: ' => '',
|
||||
// 'Trigger to generate recurrent task: ' => '',
|
||||
// 'When task is closed' => '',
|
||||
// 'When task is moved from first column' => '',
|
||||
// 'When task is moved to last column' => '',
|
||||
// 'Year(s)' => '',
|
||||
// 'Jabber (XMPP)' => '',
|
||||
// 'Send notifications to Jabber' => '',
|
||||
// 'XMPP server address' => '',
|
||||
// 'Jabber domain' => '',
|
||||
// 'Jabber nickname' => '',
|
||||
// 'Multi-user chat room' => '',
|
||||
// 'Help on Jabber integration' => '',
|
||||
// 'The server address must use this format: "tcp://hostname:5222"' => '',
|
||||
// 'Calendar settings' => '',
|
||||
// 'Project calendar view' => '',
|
||||
// 'Project settings' => '',
|
||||
// 'Show subtasks based on the time tracking' => '',
|
||||
// 'Show tasks based on the creation date' => '',
|
||||
// 'Show tasks based on the start date' => '',
|
||||
// 'Subtasks time tracking' => '',
|
||||
// 'User calendar view' => '',
|
||||
// 'Automatically update the start date' => '',
|
||||
// 'iCal feed' => '',
|
||||
// 'Preferences' => '',
|
||||
// 'Security' => '',
|
||||
// 'Two factor authentication disabled' => '',
|
||||
// 'Two factor authentication enabled' => '',
|
||||
// 'Unable to update this user.' => '',
|
||||
// 'There is no user management for private projects.' => '',
|
||||
// 'User that will receive the email' => '',
|
||||
// 'Email subject' => '',
|
||||
// 'Date' => '',
|
||||
// 'By @%s on Bitbucket' => '',
|
||||
// 'Bitbucket Issue' => '',
|
||||
// 'Commit made by @%s on Bitbucket' => '',
|
||||
// 'Commit made by @%s on Github' => '',
|
||||
// 'By @%s on Github' => '',
|
||||
// 'Commit made by @%s on Gitlab' => '',
|
||||
// 'Add a comment log when moving the task between columns' => '',
|
||||
// 'Move the task to another column when the category is changed' => '',
|
||||
// 'Send a task by email to someone' => '',
|
||||
// 'Reopen a task' => '',
|
||||
// 'Bitbucket issue opened' => '',
|
||||
// 'Bitbucket issue closed' => '',
|
||||
// 'Bitbucket issue reopened' => '',
|
||||
// 'Bitbucket issue assignee change' => '',
|
||||
// 'Bitbucket issue comment created' => '',
|
||||
// 'Column change' => '',
|
||||
// 'Position change' => '',
|
||||
// 'Swimlane change' => '',
|
||||
// 'Assignee change' => '',
|
||||
// '[%s] Overdue tasks' => '',
|
||||
// 'Notification' => '',
|
||||
// '%s moved the task #%d to the first swimlane' => '',
|
||||
// '%s moved the task #%d to the swimlane "%s"' => '',
|
||||
// 'Swimlane' => '',
|
||||
// 'Budget overview' => '',
|
||||
// 'Type' => '',
|
||||
// 'There is not enough data to show something.' => '',
|
||||
// 'Gravatar' => '',
|
||||
// 'Hipchat' => '',
|
||||
// 'Slack' => '',
|
||||
// '%s moved the task %s to the first swimlane' => '',
|
||||
// '%s moved the task %s to the swimlane "%s"' => '',
|
||||
// 'This report contains all subtasks information for the given date range.' => '',
|
||||
// 'This report contains all tasks information for the given date range.' => '',
|
||||
// 'Project activities for %s' => '',
|
||||
// 'view the board on Kanboard' => '',
|
||||
// 'The task have been moved to the first swimlane' => '',
|
||||
// 'The task have been moved to another swimlane:' => '',
|
||||
// 'Overdue tasks for the project "%s"' => '',
|
||||
// 'New title: %s' => '',
|
||||
// 'The task is not assigned anymore' => '',
|
||||
// 'New assignee: %s' => '',
|
||||
// 'There is no category now' => '',
|
||||
// 'New category: %s' => '',
|
||||
// 'New color: %s' => '',
|
||||
// 'New complexity: %d' => '',
|
||||
// 'The due date have been removed' => '',
|
||||
// 'There is no description anymore' => '',
|
||||
// 'Recurrence settings have been modified' => '',
|
||||
// 'Time spent changed: %sh' => '',
|
||||
// 'Time estimated changed: %sh' => '',
|
||||
// 'The field "%s" have been updated' => '',
|
||||
// 'The description have been modified' => '',
|
||||
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
|
||||
// 'Swimlane: %s' => '',
|
||||
// 'I want to receive notifications for:' => '',
|
||||
// 'All tasks' => '',
|
||||
// 'Only for tasks assigned to me' => '',
|
||||
// 'Only for tasks created by me' => '',
|
||||
// 'Only for tasks created by me and assigned to me' => '',
|
||||
// '%A' => '',
|
||||
// '%b %e, %Y, %k:%M %p' => '',
|
||||
// 'New due date: %B %e, %Y' => '',
|
||||
// 'Start date changed: %B %e, %Y' => '',
|
||||
// '%k:%M %p' => '',
|
||||
// '%%Y-%%m-%%d' => '',
|
||||
// 'Total for all columns' => '',
|
||||
// 'You need at least 2 days of data to show the chart.' => '',
|
||||
// '<15m' => '',
|
||||
// '<30m' => '',
|
||||
// 'Stop timer' => '',
|
||||
// 'Start timer' => '',
|
||||
// 'Add project member' => '',
|
||||
// 'Enable notifications' => '',
|
||||
// 'My activity stream' => '',
|
||||
// 'My calendar' => '',
|
||||
// 'Search tasks' => '',
|
||||
// 'Back to the calendar' => '',
|
||||
// 'Filters' => '',
|
||||
// 'Reset filters' => '',
|
||||
// 'My tasks due tomorrow' => '',
|
||||
// 'Tasks due today' => '',
|
||||
// 'Tasks due tomorrow' => '',
|
||||
// 'Tasks due yesterday' => '',
|
||||
// 'Closed tasks' => '',
|
||||
// 'Open tasks' => '',
|
||||
// 'Not assigned' => '',
|
||||
// 'View advanced search syntax' => '',
|
||||
// 'Overview' => '',
|
||||
// '%b %e %Y' => '',
|
||||
// 'Board/Calendar/List view' => '',
|
||||
// 'Switch to the board view' => '',
|
||||
// 'Switch to the calendar view' => '',
|
||||
// 'Switch to the list view' => '',
|
||||
// 'Go to the search/filter box' => '',
|
||||
// 'There is no activity yet.' => '',
|
||||
// 'No tasks found.' => '',
|
||||
// 'Keyboard shortcut: "%s"' => '',
|
||||
// 'List' => '',
|
||||
// 'Filter' => '',
|
||||
// 'Advanced search' => '',
|
||||
// 'Example of query: ' => '',
|
||||
// 'Search by project: ' => '',
|
||||
// 'Search by column: ' => '',
|
||||
// 'Search by assignee: ' => '',
|
||||
// 'Search by color: ' => '',
|
||||
// 'Search by category: ' => '',
|
||||
// 'Search by description: ' => '',
|
||||
// 'Search by due date: ' => '',
|
||||
'Help on Sendgrid integration' => 'Hjälp för Sendgrid integration',
|
||||
'Disable two factor authentication' => 'Inaktivera två-faktors autentisering',
|
||||
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Vill du verkligen inaktivera två-faktors autentisering för denna användare: "%s"?',
|
||||
'Edit link' => 'Ändra länk',
|
||||
'Start to type task title...' => 'Börja skriv uppgiftstitel...',
|
||||
'A task cannot be linked to itself' => 'En uppgift kan inte länkas till sig själv',
|
||||
'The exact same link already exists' => 'Länken existerar redan',
|
||||
'Recurrent task is scheduled to be generated' => 'Återkommande uppgift är schemalagd att genereras',
|
||||
'Recurring information' => 'Återkommande information',
|
||||
'Score' => 'Poäng',
|
||||
'The identifier must be unique' => 'Identifieraren måste vara unik',
|
||||
'This linked task id doesn\'t exists' => 'Denna länkade uppgifts id existerar inte',
|
||||
'This value must be alphanumeric' => 'Värdet måste vara alfanumeriskt',
|
||||
'Edit recurrence' => 'Ändra återkommande',
|
||||
'Generate recurrent task' => 'Generera återkommande uppgift',
|
||||
'Trigger to generate recurrent task' => 'Aktivera att generera återkommande uppgift',
|
||||
'Factor to calculate new due date' => 'Faktor för att beräkna nytt datum',
|
||||
'Timeframe to calculate new due date' => 'Tidsram för att beräkna nytt datum',
|
||||
'Base date to calculate new due date' => 'Basdatum för att beräkna nytt datum',
|
||||
'Action date' => 'Händelsedatum',
|
||||
'Base date to calculate new due date: ' => 'Basdatum för att beräkna nytt förfallodatum: ',
|
||||
'This task has created this child task: ' => 'Uppgiften har skapat denna underliggande uppgift: ',
|
||||
'Day(s)' => 'Dag(ar)',
|
||||
'Existing due date' => 'Existerande förfallodatum',
|
||||
'Factor to calculate new due date: ' => 'Faktor för att beräkna nytt förfallodatum: ',
|
||||
'Month(s)' => 'Månad(er)',
|
||||
'Recurrence' => 'Återkommande',
|
||||
'This task has been created by: ' => 'Uppgiften har skapats av: ',
|
||||
'Recurrent task has been generated:' => 'Återkommande uppgift har genererats:',
|
||||
'Timeframe to calculate new due date: ' => 'Tidsram för att beräkna nytt förfallodatum: ',
|
||||
'Trigger to generate recurrent task: ' => 'Aktivera att generera återkommande uppgift: ',
|
||||
'When task is closed' => 'När uppgiften är stängd',
|
||||
'When task is moved from first column' => 'När uppgiften flyttas från första kolumnen',
|
||||
'When task is moved to last column' => 'När uppgiften flyttas till sista kolumnen',
|
||||
'Year(s)' => 'År',
|
||||
'Jabber (XMPP)' => 'Jabber (XMPP)',
|
||||
'Send notifications to Jabber' => 'Skicka notiser till Jabber',
|
||||
'XMPP server address' => 'XMPP serveradress',
|
||||
'Jabber domain' => 'Jabber domän',
|
||||
'Jabber nickname' => 'Jabber smeknamn',
|
||||
'Multi-user chat room' => 'Multi-user chatrum',
|
||||
'Help on Jabber integration' => 'Hjälp för Jabber integration',
|
||||
'The server address must use this format: "tcp://hostname:5222"' => 'Serveradressen måste använda detta format: "tcp://hostname:5222"',
|
||||
'Calendar settings' => 'Inställningar för kalendern',
|
||||
'Project calendar view' => 'Projektkalendervy',
|
||||
'Project settings' => 'Projektinställningar',
|
||||
'Show subtasks based on the time tracking' => 'Visa deluppgifter baserade på tidsspårning',
|
||||
'Show tasks based on the creation date' => 'Visa uppgifter baserade på skapat datum',
|
||||
'Show tasks based on the start date' => 'Visa uppgifter baserade på startdatum',
|
||||
'Subtasks time tracking' => 'Deluppgifter tidsspårning',
|
||||
'User calendar view' => 'Användarkalendervy',
|
||||
'Automatically update the start date' => 'Automatisk uppdatering av startdatum',
|
||||
'iCal feed' => 'iCal flöde',
|
||||
'Preferences' => 'Preferenser',
|
||||
'Security' => 'Säkerhet',
|
||||
'Two factor authentication disabled' => 'Tvåfaktorsverifiering inaktiverad',
|
||||
'Two factor authentication enabled' => 'Tvåfaktorsverifiering aktiverad',
|
||||
'Unable to update this user.' => 'Kunde inte uppdatera användaren.',
|
||||
'There is no user management for private projects.' => 'Det finns ingen användarhantering för privata projekt.',
|
||||
'User that will receive the email' => 'Användare som kommer att ta emot mailet',
|
||||
'Email subject' => 'E-post ämne',
|
||||
'Date' => 'Datum',
|
||||
'By @%s on Bitbucket' => 'Av @%s på Bitbucket',
|
||||
'Bitbucket Issue' => 'Bitbucket fråga',
|
||||
'Commit made by @%s on Bitbucket' => 'Bidrag gjort av @%s på Bitbucket',
|
||||
'Commit made by @%s on Github' => 'Bidrag gjort av @%s på GitHub',
|
||||
'By @%s on Github' => 'Av @%s på GitHub',
|
||||
'Commit made by @%s on Gitlab' => 'Bidrag gjort av @%s på Gitlab',
|
||||
'Add a comment log when moving the task between columns' => 'Lägg till en kommentarslogg när en uppgift flyttas mellan kolumner',
|
||||
'Move the task to another column when the category is changed' => 'Flyttas uppgiften till en annan kolumn när kategorin ändras',
|
||||
'Send a task by email to someone' => 'Skicka en uppgift med e-post till någon',
|
||||
'Reopen a task' => 'Återöppna en uppgift',
|
||||
'Bitbucket issue opened' => 'Bitbucketfråga öppnad',
|
||||
'Bitbucket issue closed' => 'Bitbucketfråga stängd',
|
||||
'Bitbucket issue reopened' => 'Bitbucketfråga återöppnad',
|
||||
'Bitbucket issue assignee change' => 'Bitbucketfråga tilldelningsändring',
|
||||
'Bitbucket issue comment created' => 'Bitbucketfråga kommentar skapad',
|
||||
'Column change' => 'Kolumnändring',
|
||||
'Position change' => 'Positionsändring',
|
||||
'Swimlane change' => 'Swimlaneändring',
|
||||
'Assignee change' => 'Tilldelningsändring',
|
||||
'[%s] Overdue tasks' => '[%s] Försenade uppgifter',
|
||||
'Notification' => 'Notis',
|
||||
'%s moved the task #%d to the first swimlane' => '%s flyttade uppgiften #%d till första swimlane',
|
||||
'%s moved the task #%d to the swimlane "%s"' => '%s flyttade uppgiften #%d till swimlane "%s"',
|
||||
'Swimlane' => 'Swimlane',
|
||||
'Budget overview' => 'Budgetöversikt',
|
||||
'Type' => 'Typ',
|
||||
'There is not enough data to show something.' => 'Det finns inte tillräckligt mycket data för att visa något.',
|
||||
'Gravatar' => 'Gravatar',
|
||||
'Hipchat' => 'Hipchat',
|
||||
'Slack' => 'Slack',
|
||||
'%s moved the task %s to the first swimlane' => '%s flyttade uppgiften %s till första swimlane',
|
||||
'%s moved the task %s to the swimlane "%s"' => '%s flyttade uppgiften %s till swimlane "%s"',
|
||||
'This report contains all subtasks information for the given date range.' => 'Denna rapport innehåller all deluppgiftsinformation för det givna datumintervallet.',
|
||||
'This report contains all tasks information for the given date range.' => 'Denna rapport innehåller all uppgiftsinformation för det givna datumintervallet.',
|
||||
'Project activities for %s' => 'Projektaktiviteter för %s',
|
||||
'view the board on Kanboard' => 'visa tavlan på Kanboard',
|
||||
'The task have been moved to the first swimlane' => 'Uppgiften har flyttats till första swimlane',
|
||||
'The task have been moved to another swimlane:' => 'Uppgiften har flyttats till en annan swimlane:',
|
||||
'Overdue tasks for the project "%s"' => 'Försenade uppgifter för projektet "%s"',
|
||||
'New title: %s' => 'Ny titel: %s',
|
||||
'The task is not assigned anymore' => 'Uppgiften är inte länge tilldelad',
|
||||
'New assignee: %s' => 'Ny tilldelning: %s',
|
||||
'There is no category now' => 'Det finns ingen kategori nu',
|
||||
'New category: %s' => 'Ny kategori: %s',
|
||||
'New color: %s' => 'Ny färg: %s',
|
||||
'New complexity: %d' => 'Ny komplexitet: %d',
|
||||
'The due date have been removed' => 'Förfallodatumet har tagits bort',
|
||||
'There is no description anymore' => 'Det finns ingen beskrivning längre',
|
||||
'Recurrence settings have been modified' => 'Återkommande inställning har ändrats',
|
||||
'Time spent changed: %sh' => 'Spenderad tid har ändrats: %sh',
|
||||
'Time estimated changed: %sh' => 'Tidsuppskattning ändrad: %sh',
|
||||
'The field "%s" have been updated' => 'Fältet "%s" har uppdaterats',
|
||||
'The description have been modified' => 'Beskrivningen har modifierats',
|
||||
'Do you really want to close the task "%s" as well as all subtasks?' => 'Vill du verkligen stänga uppgiften "%s" och alla deluppgifter?',
|
||||
'Swimlane: %s' => 'Swimlane: %s',
|
||||
'I want to receive notifications for:' => 'Jag vill få notiser för:',
|
||||
'All tasks' => 'Alla uppgifter',
|
||||
'Only for tasks assigned to me' => 'Bara för uppgifter tilldelade mig',
|
||||
'Only for tasks created by me' => 'Bara för uppgifter skapade av mig',
|
||||
'Only for tasks created by me and assigned to me' => 'Bara för uppgifter skapade av mig och tilldelade till mig',
|
||||
'%A' => '%A',
|
||||
'%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p',
|
||||
'New due date: %B %e, %Y' => 'Nytt förfallodatum: %B %e, %Y',
|
||||
'Start date changed: %B %e, %Y' => 'Startdatum ändrat: %B %e, %Y',
|
||||
'%k:%M %p' => '%k:%M %p',
|
||||
'%%Y-%%m-%%d' => '%%Y-%%m-%%d',
|
||||
'Total for all columns' => 'Totalt för alla kolumner',
|
||||
'You need at least 2 days of data to show the chart.' => 'Du behöver minst två dagars data för att visa diagrammet.',
|
||||
'<15m' => '<15m',
|
||||
'<30m' => '<30m',
|
||||
'Stop timer' => 'Stoppa timer',
|
||||
'Start timer' => 'Starta timer',
|
||||
'Add project member' => 'Lägg till projektmedlem',
|
||||
'Enable notifications' => 'Aktivera notiser',
|
||||
'My activity stream' => 'Min aktivitetsström',
|
||||
'My calendar' => 'Min kalender',
|
||||
'Search tasks' => 'Sök uppgifter',
|
||||
'Back to the calendar' => 'Tillbaka till kalendern',
|
||||
'Filters' => 'Filter',
|
||||
'Reset filters' => 'Återställ filter',
|
||||
'My tasks due tomorrow' => 'Mina uppgifter förfaller imorgon',
|
||||
'Tasks due today' => 'Uppgifter förfaller idag',
|
||||
'Tasks due tomorrow' => 'Uppgifter förfaller imorgon',
|
||||
'Tasks due yesterday' => 'Uppgifter förföll igår',
|
||||
'Closed tasks' => 'Stängda uppgifter',
|
||||
'Open tasks' => 'Öppna uppgifter',
|
||||
'Not assigned' => 'Inte tilldelad',
|
||||
'View advanced search syntax' => 'Visa avancerad söksyntax',
|
||||
'Overview' => 'Översikts',
|
||||
'%b %e %Y' => '%b %e %Y',
|
||||
'Board/Calendar/List view' => 'Tavla/Kalender/Listvy',
|
||||
'Switch to the board view' => 'Växla till tavelvy',
|
||||
'Switch to the calendar view' => 'Växla till kalendervy',
|
||||
'Switch to the list view' => 'Växla till listvy',
|
||||
'Go to the search/filter box' => 'Gå till sök/filter box',
|
||||
'There is no activity yet.' => 'Det finns ingen aktivitet ännu.',
|
||||
'No tasks found.' => 'Inga uppgifter hittades.',
|
||||
'Keyboard shortcut: "%s"' => 'Tangentbordsgenväg: "%s"',
|
||||
'List' => 'Lista',
|
||||
'Filter' => 'Filter',
|
||||
'Advanced search' => 'Avancerad sök',
|
||||
'Example of query: ' => 'Exempel på fråga',
|
||||
'Search by project: ' => 'Sök efter projekt:',
|
||||
'Search by column: ' => 'Sök efter kolumn:',
|
||||
'Search by assignee: ' => 'Sök efter tilldelad:',
|
||||
'Search by color: ' => 'Sök efter färg:',
|
||||
'Search by category: ' => 'Sök efter kategori:',
|
||||
'Search by description: ' => 'Sök efter beskrivning',
|
||||
'Search by due date: ' => 'Sök efter förfallodatum',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,32 @@ use PDO;
|
|||
use Core\Security;
|
||||
use Model\Link;
|
||||
|
||||
const VERSION = 77;
|
||||
const VERSION = 79;
|
||||
|
||||
function version_79($pdo)
|
||||
{
|
||||
$pdo->exec("
|
||||
CREATE TABLE project_daily_stats (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
day CHAR(10) NOT NULL,
|
||||
project_id INT NOT NULL,
|
||||
avg_lead_time INT NOT NULL DEFAULT 0,
|
||||
avg_cycle_time INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB CHARSET=utf8
|
||||
");
|
||||
|
||||
$pdo->exec('CREATE UNIQUE INDEX project_daily_stats_idx ON project_daily_stats(day, project_id)');
|
||||
|
||||
$pdo->exec('RENAME TABLE project_daily_summaries TO project_daily_column_stats');
|
||||
}
|
||||
|
||||
function version_78($pdo)
|
||||
{
|
||||
$pdo->exec("ALTER TABLE project_integrations ADD COLUMN slack_webhook_channel VARCHAR(255) DEFAULT ''");
|
||||
$pdo->exec("INSERT INTO settings VALUES ('integration_slack_webhook_channel', '')");
|
||||
}
|
||||
|
||||
function version_77($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,31 @@ use PDO;
|
|||
use Core\Security;
|
||||
use Model\Link;
|
||||
|
||||
const VERSION = 57;
|
||||
const VERSION = 59;
|
||||
|
||||
function version_59($pdo)
|
||||
{
|
||||
$pdo->exec("
|
||||
CREATE TABLE project_daily_stats (
|
||||
id SERIAL PRIMARY KEY,
|
||||
day CHAR(10) NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
avg_lead_time INTEGER NOT NULL DEFAULT 0,
|
||||
avg_cycle_time INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
)
|
||||
");
|
||||
|
||||
$pdo->exec('CREATE UNIQUE INDEX project_daily_stats_idx ON project_daily_stats(day, project_id)');
|
||||
|
||||
$pdo->exec('ALTER TABLE project_daily_summaries RENAME TO project_daily_column_stats');
|
||||
}
|
||||
|
||||
function version_58($pdo)
|
||||
{
|
||||
$pdo->exec("ALTER TABLE project_integrations ADD COLUMN slack_webhook_channel VARCHAR(255) DEFAULT ''");
|
||||
$pdo->exec("INSERT INTO settings VALUES ('integration_slack_webhook_channel', '')");
|
||||
}
|
||||
|
||||
function version_57($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,31 @@ use Core\Security;
|
|||
use PDO;
|
||||
use Model\Link;
|
||||
|
||||
const VERSION = 73;
|
||||
const VERSION = 75;
|
||||
|
||||
function version_75($pdo)
|
||||
{
|
||||
$pdo->exec("
|
||||
CREATE TABLE project_daily_stats (
|
||||
id INTEGER PRIMARY KEY,
|
||||
day TEXT NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
avg_lead_time INTEGER NOT NULL DEFAULT 0,
|
||||
avg_cycle_time INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
)
|
||||
");
|
||||
|
||||
$pdo->exec('CREATE UNIQUE INDEX project_daily_stats_idx ON project_daily_stats(day, project_id)');
|
||||
|
||||
$pdo->exec('ALTER TABLE project_daily_summaries RENAME TO project_daily_column_stats');
|
||||
}
|
||||
|
||||
function version_74($pdo)
|
||||
{
|
||||
$pdo->exec("ALTER TABLE project_integrations ADD COLUMN slack_webhook_channel TEXT DEFAULT ''");
|
||||
$pdo->exec("INSERT INTO settings VALUES ('integration_slack_webhook_channel', '')");
|
||||
}
|
||||
|
||||
function version_73($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ class ClassProvider implements ServiceProviderInterface
|
|||
'ProjectActivity',
|
||||
'ProjectAnalytic',
|
||||
'ProjectDuplication',
|
||||
'ProjectDailySummary',
|
||||
'ProjectDailyColumnStats',
|
||||
'ProjectDailyStats',
|
||||
'ProjectIntegration',
|
||||
'ProjectPermission',
|
||||
'Subtask',
|
||||
|
|
@ -42,6 +43,7 @@ class ClassProvider implements ServiceProviderInterface
|
|||
'SubtaskTimeTracking',
|
||||
'Swimlane',
|
||||
'Task',
|
||||
'TaskAnalytic',
|
||||
'TaskCreation',
|
||||
'TaskDuplication',
|
||||
'TaskExport',
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ class ProjectDailySummarySubscriber extends \Core\Base implements EventSubscribe
|
|||
public function execute(TaskEvent $event)
|
||||
{
|
||||
if (isset($event['project_id'])) {
|
||||
$this->projectDailySummary->updateTotals($event['project_id'], date('Y-m-d'));
|
||||
$this->projectDailyColumnStats->updateTotals($event['project_id'], date('Y-m-d'));
|
||||
$this->projectDailyStats->updateTotals($event['project_id'], date('Y-m-d'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Average time spent into each column') ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if (empty($metrics)): ?>
|
||||
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
|
||||
<?php else: ?>
|
||||
<section id="analytic-avg-time-column">
|
||||
|
||||
<div id="chart" data-metrics='<?= json_encode($metrics) ?>' data-label="<?= t('Average time spent') ?>"></div>
|
||||
|
||||
<table class="table-stripped">
|
||||
<tr>
|
||||
<th><?= t('Column') ?></th>
|
||||
<th><?= t('Average time spent') ?></th>
|
||||
</tr>
|
||||
<?php foreach ($metrics as $column): ?>
|
||||
<tr>
|
||||
<td><?= $this->e($column['title']) ?></td>
|
||||
<td><?= $this->dt->duration($column['average']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
|
||||
<p class="alert alert-info">
|
||||
<?= t('This chart show the average time spent into each column for the last %d tasks.', 1000) ?>
|
||||
</p>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Average Lead and Cycle time') ?></h2>
|
||||
</div>
|
||||
|
||||
<div class="listing">
|
||||
<ul>
|
||||
<li><?= t('Average lead time: ').'<strong>'.$this->dt->duration($average['avg_lead_time']) ?></strong></li>
|
||||
<li><?= t('Average cycle time: ').'<strong>'.$this->dt->duration($average['avg_cycle_time']) ?></strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?php if (empty($metrics)): ?>
|
||||
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
|
||||
<?php else: ?>
|
||||
<section id="analytic-lead-cycle-time">
|
||||
|
||||
<div id="chart" data-metrics='<?= json_encode($metrics) ?>' data-label-cycle="<?= t('Cycle Time') ?>" data-label-lead="<?= t('Lead Time') ?>"></div>
|
||||
|
||||
<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'leadAndCycleTime', array('project_id' => $project['id'])) ?>" autocomplete="off">
|
||||
|
||||
<?= $this->form->csrf() ?>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<?= $this->form->label(t('Start Date'), 'from') ?>
|
||||
<?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
</div>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<?= $this->form->label(t('End Date'), 'to') ?>
|
||||
<?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
</div>
|
||||
|
||||
<div class="form-inline-group">
|
||||
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="alert alert-info">
|
||||
<?= t('This chart show the average lead and cycle time for the last %d tasks over the time.', 1000) ?>
|
||||
</p>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
|
@ -13,5 +13,11 @@
|
|||
<li>
|
||||
<?= $this->url->link(t('Burndown chart'), 'analytic', 'burndown', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
<li>
|
||||
<?= $this->url->link(t('Average time into each column'), 'analytic', 'averageTimeByColumn', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
<li>
|
||||
<?= $this->url->link(t('Lead and cycle time'), 'analytic', 'leadAndCycleTime', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -10,53 +10,55 @@
|
|||
|
||||
<?= $this->render('board/task_menu', array('task' => $task)) ?>
|
||||
|
||||
<div class="task-board-collapsed" style="display: none">
|
||||
<?php if (! empty($task['assignee_username'])): ?>
|
||||
<span title="<?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>">
|
||||
<?= $this->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?>
|
||||
</span> -
|
||||
<?php endif ?>
|
||||
<span class="tooltip" title="<?= $this->e($task['title']) ?>"
|
||||
<?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title') ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="task-board-expanded">
|
||||
|
||||
<?php if ($task['reference']): ?>
|
||||
<span class="task-board-reference" title="<?= t('Reference') ?>">
|
||||
(<?= $task['reference'] ?>)
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<span class="task-board-user <?= $this->user->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>">
|
||||
<?= $this->url->link(
|
||||
(! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')),
|
||||
'board',
|
||||
'changeAssignee',
|
||||
array('task_id' => $task['id'], 'project_id' => $task['project_id']),
|
||||
false,
|
||||
'task-board-popover',
|
||||
t('Change assignee')
|
||||
) ?>
|
||||
</span>
|
||||
|
||||
<?php if ($task['is_active'] == 1): ?>
|
||||
<div class="task-board-days">
|
||||
<span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->datetime->age($task['date_creation']) ?></span>
|
||||
<span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->datetime->age($task['date_moved']) ?></span>
|
||||
<?php if ($this->board->isCollapsed($project['id'])): ?>
|
||||
<div class="task-board-collapsed">
|
||||
<?php if (! empty($task['assignee_username'])): ?>
|
||||
<span title="<?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>">
|
||||
<?= $this->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?>
|
||||
</span> -
|
||||
<?php endif ?>
|
||||
<span class="tooltip" title="<?= $this->e($task['title']) ?>"
|
||||
<?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title') ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="task-board-closed"><i class="fa fa-ban fa-fw"></i><?= t('Closed') ?></div>
|
||||
<?php endif ?>
|
||||
<?php else: ?>
|
||||
<div class="task-board-expanded">
|
||||
|
||||
<div class="task-board-title">
|
||||
<?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
|
||||
<?php if ($task['reference']): ?>
|
||||
<span class="task-board-reference" title="<?= t('Reference') ?>">
|
||||
(<?= $task['reference'] ?>)
|
||||
</span>
|
||||
<?php endif ?>
|
||||
|
||||
<span class="task-board-user <?= $this->user->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>">
|
||||
<?= $this->url->link(
|
||||
(! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')),
|
||||
'board',
|
||||
'changeAssignee',
|
||||
array('task_id' => $task['id'], 'project_id' => $task['project_id']),
|
||||
false,
|
||||
'task-board-popover',
|
||||
t('Change assignee')
|
||||
) ?>
|
||||
</span>
|
||||
|
||||
<?php if ($task['is_active'] == 1): ?>
|
||||
<div class="task-board-days">
|
||||
<span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->dt->age($task['date_creation']) ?></span>
|
||||
<span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->dt->age($task['date_moved']) ?></span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="task-board-closed"><i class="fa fa-ban fa-fw"></i><?= t('Closed') ?></div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="task-board-title">
|
||||
<?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
|
||||
</div>
|
||||
|
||||
<?= $this->render('board/task_footer', array(
|
||||
'task' => $task,
|
||||
'not_editable' => $not_editable,
|
||||
)) ?>
|
||||
</div>
|
||||
|
||||
<?= $this->render('board/task_footer', array(
|
||||
'task' => $task,
|
||||
'not_editable' => $not_editable,
|
||||
)) ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@
|
|||
|
||||
<?= $this->form->label(t('Webhook URL'), 'integration_slack_webhook_url') ?>
|
||||
<?= $this->form->text('integration_slack_webhook_url', $values, $errors) ?>
|
||||
<?= $this->form->label(t('Channel/Group/User (Optional)'), 'integration_slack_webhook_channel') ?>
|
||||
<?= $this->form->text('integration_slack_webhook_channel', $values, $errors) ?>
|
||||
|
||||
<p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
<ul>
|
||||
<?php if (isset($board_selector) && ! empty($board_selector)): ?>
|
||||
<li>
|
||||
<select id="board-selector" tabindex=="-1" data-notfound="<?= t('No results match:') ?>" data-placeholder="<?= t('Display another project') ?>" data-board-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>">
|
||||
<select id="board-selector" tabindex="-1" data-notfound="<?= t('No results match:') ?>" data-placeholder="<?= t('Display another project') ?>" data-board-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>">
|
||||
<option value=""></option>
|
||||
<?php foreach($board_selector as $board_id => $board_name): ?>
|
||||
<option value="<?= $board_id ?>"><?= $this->e($board_name) ?></option>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@
|
|||
<ul>
|
||||
<?php if (isset($is_board)): ?>
|
||||
<li>
|
||||
<span class="filter-collapse">
|
||||
<i class="fa fa-compress fa-fw"></i> <a href="#" class="filter-collapse-link" title="<?= t('Keyboard shortcut: "%s"', 's') ?>"><?= t('Collapse tasks') ?></a>
|
||||
</span>
|
||||
<span class="filter-expand" style="display: none">
|
||||
<i class="fa fa-expand fa-fw"></i> <a href="#" class="filter-expand-link" title="<?= t('Keyboard shortcut: "%s"', 's') ?>"><?= t('Expand tasks') ?></a>
|
||||
</span>
|
||||
<?php if ($this->board->isCollapsed($project['id'])): ?>
|
||||
<i class="fa fa-expand fa-fw"></i>
|
||||
<?= $this->url->link(t('Expand tasks'), 'board', 'expand', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
|
||||
<?php else: ?>
|
||||
<i class="fa fa-compress fa-fw"></i>
|
||||
<?= $this->url->link(t('Collapse tasks'), 'board', 'collapse', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
|
||||
<?php endif ?>
|
||||
</li>
|
||||
<li>
|
||||
<span class="filter-compact">
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@
|
|||
|
||||
<?= $this->form->label(t('Webhook URL'), 'slack_webhook_url') ?>
|
||||
<?= $this->form->text('slack_webhook_url', $values, $errors) ?>
|
||||
<?= $this->form->label(t('Channel/Group/User (Optional)'), 'slack_webhook_channel') ?>
|
||||
<?= $this->form->text('slack_webhook_channel', $values, $errors) ?>
|
||||
|
||||
<p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p>
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
<?php if ($subtask['is_timer_started']): ?>
|
||||
<i class="fa fa-pause"></i>
|
||||
<?= $this->url->link(t('Stop timer'), 'timer', 'subtask', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
|
||||
(<?= $this->datetime->age($subtask['timer_start_date']) ?>)
|
||||
(<?= $this->dt->age($subtask['timer_start_date']) ?>)
|
||||
<?php else: ?>
|
||||
<i class="fa fa-play-circle-o"></i>
|
||||
<?= $this->url->link(t('Start timer'), 'timer', 'subtask', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Analytics') ?></h2>
|
||||
</div>
|
||||
|
||||
<div class="listing">
|
||||
<ul>
|
||||
<li><?= t('Lead time: ').'<strong>'.$this->dt->duration($lead_time) ?></strong></li>
|
||||
<li><?= t('Cycle time: ').'<strong>'.$this->dt->duration($cycle_time) ?></strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 id="analytic-task-time-column"><?= t('Time spent into each column') ?></h3>
|
||||
<div id="chart" data-metrics='<?= json_encode($time_spent_columns) ?>' data-label="<?= t('Time spent') ?>"></div>
|
||||
<table class="table-stripped">
|
||||
<tr>
|
||||
<th><?= t('Column') ?></th>
|
||||
<th><?= t('Time spent') ?></th>
|
||||
</tr>
|
||||
<?php foreach ($time_spent_columns as $column): ?>
|
||||
<tr>
|
||||
<td><?= $this->e($column['title']) ?></td>
|
||||
<td><?= $this->dt->duration($column['time_spent']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<ul>
|
||||
<li><?= t('The lead time is the duration between the task creation and the completion.') ?></li>
|
||||
<li><?= t('The cycle time is the duration between the start date and the completion.') ?></li>
|
||||
<li><?= t('If the task is not closed the current time is used instead of the completion date.') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?= $this->asset->js('assets/js/vendor/d3.v3.min.js') ?>
|
||||
<?= $this->asset->js('assets/js/vendor/c3.min.js') ?>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<ul>
|
||||
<li>
|
||||
<i class="fa fa-th fa-fw"></i>
|
||||
<?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, 'swimlane-'.$task['swimlane_id']) ?>
|
||||
<?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, $task['swimlane_id'] != 0 ? 'swimlane-'.$task['swimlane_id'] : '') ?>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-calendar fa-fw"></i>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@
|
|||
<?= $this->url->link(t('Summary'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
|
||||
</li>
|
||||
<li>
|
||||
<?= $this->url->link(t('Activity stream'), 'task', 'activites', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
|
||||
<?= $this->url->link(t('Activity stream'), 'activity', 'task', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
|
||||
</li>
|
||||
<li>
|
||||
<?= $this->url->link(t('Transitions'), 'task', 'transitions', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
|
||||
</li>
|
||||
<li>
|
||||
<?= $this->url->link(t('Analytics'), 'task', 'analytics', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
|
||||
</li>
|
||||
<?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?>
|
||||
<li>
|
||||
<?= $this->url->link(t('Time tracking'), 'task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
<form method="post" action="<?= $this->url->href('task', 'time', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" class="form-inline task-time-form" autocomplete="off">
|
||||
|
||||
<?php if (empty($values['date_started'])): ?>
|
||||
<?= $this->url->link('<i class="fa fa-play"></i>', 'task', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-show-start-link', t('Set automatically the start date')) ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?= $this->form->csrf() ?>
|
||||
<?= $this->form->hidden('id', $values) ?>
|
||||
|
||||
<?= $this->form->label(t('Start date'), 'date_started') ?>
|
||||
<?= $this->form->text('date_started', $values, array(), array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
<?= $this->form->text('date_started', $values, array(), array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-datetime') ?>
|
||||
|
||||
<?= $this->form->label(t('Time estimated'), 'time_estimated') ?>
|
||||
<?= $this->form->numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<td><?= $this->e($transition['src_column']) ?></td>
|
||||
<td><?= $this->e($transition['dst_column']) ?></td>
|
||||
<td><?= $this->url->link($this->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?></td>
|
||||
<td><?= n(round($transition['time_spent'] / 3600, 2)).' '.t('hours') ?></td>
|
||||
<td><?= $this->dt->duration($transition['time_spent']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@
|
|||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->label(t('Start time'), 'start') ?>
|
||||
<?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
|
||||
<?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
|
||||
|
||||
<?= $this->form->label(t('End time'), 'end') ?>
|
||||
<?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
|
||||
<?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@
|
|||
<?= $this->form->checkbox('all_day', t('All day'), 1) ?>
|
||||
|
||||
<?= $this->form->label(t('Start time'), 'start') ?>
|
||||
<?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
|
||||
<?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
|
||||
|
||||
<?= $this->form->label(t('End time'), 'end') ?>
|
||||
<?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
|
||||
<?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
|
||||
|
||||
<?= $this->form->label(t('Comment'), 'comment') ?>
|
||||
<?= $this->form->text('comment', $values, $errors) ?>
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@
|
|||
<?= $this->form->checkbox('all_day', t('All day'), 1) ?>
|
||||
|
||||
<?= $this->form->label(t('Start time'), 'start') ?>
|
||||
<?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
|
||||
<?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
|
||||
|
||||
<?= $this->form->label(t('End time'), 'end') ?>
|
||||
<?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
|
||||
<?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
|
||||
|
||||
<?= $this->form->label(t('Comment'), 'comment') ?>
|
||||
<?= $this->form->text('comment', $values, $errors) ?>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
</tr>
|
||||
<?php foreach ($timetable as $slot): ?>
|
||||
<tr>
|
||||
<td><?= $this->datetime->getWeekDay($slot['day']) ?></td>
|
||||
<td><?= $this->dt->getWeekDay($slot['day']) ?></td>
|
||||
<td><?= $slot['start'] ?></td>
|
||||
<td><?= $slot['end'] ?></td>
|
||||
<td>
|
||||
|
|
@ -32,13 +32,13 @@
|
|||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->label(t('Day'), 'day') ?>
|
||||
<?= $this->form->select('day', $this->datetime->getWeekDays(), $values, $errors) ?>
|
||||
<?= $this->form->select('day', $this->dt->getWeekDays(), $values, $errors) ?>
|
||||
|
||||
<?= $this->form->label(t('Start time'), 'start') ?>
|
||||
<?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
|
||||
<?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
|
||||
|
||||
<?= $this->form->label(t('End time'), 'end') ?>
|
||||
<?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
|
||||
<?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
// Automatically parse environment configuration (Heroku)
|
||||
if (getenv('DATABASE_URL')) {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -52,3 +52,8 @@ hr {
|
|||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* datepicker */
|
||||
#ui-datepicker-div {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ ul.form-errors li {
|
|||
display: inline;
|
||||
}
|
||||
|
||||
input.form-datetime,
|
||||
input.form-date {
|
||||
width: 150px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
/* datepicker */
|
||||
#ui-datepicker-div {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
/* pagination */
|
||||
.pagination {
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/* task inside the board */
|
||||
.task-board {
|
||||
position: relative;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 2px;
|
||||
border: 1px solid #000;
|
||||
padding: 3px;
|
||||
font-size: 0.9em;
|
||||
padding: 2px;
|
||||
font-size: 0.85em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
|
@ -44,11 +44,12 @@ a.task-board-collapsed-title {
|
|||
.task-board .dropdown {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.task-board-title {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
|
|
@ -77,8 +78,6 @@ a.task-board-nobody {
|
|||
|
||||
.task-board-category-container {
|
||||
text-align: right;
|
||||
padding-bottom: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.task-board-category {
|
||||
|
|
@ -289,3 +288,12 @@ span.task-board-date-overdue {
|
|||
.task-show-file-table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.task-show-start-link {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.task-show-start-link:hover,
|
||||
.task-show-start-link:focus {
|
||||
color: red;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
/*! jQuery Timepicker Addon - v1.5.5 - 2015-05-24
|
||||
* http://trentrichardson.com/examples/timepicker
|
||||
* Copyright (c) 2015 Trent Richardson; Licensed MIT */
|
||||
|
||||
.ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dt{float:left;clear:left;padding:0 0 0 5px}.ui-timepicker-div dl dd{margin:0 10px 10px 40%}.ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label{background:0 0;border:0;margin:0;padding:0}.ui-timepicker-div .ui_tpicker_unit_hide{display:none}.ui-timepicker-rtl{direction:rtl}.ui-timepicker-rtl dl{text-align:right;padding:0 5px 0 0}.ui-timepicker-rtl dl dt{float:right;clear:right}.ui-timepicker-rtl dl dd{margin:0 40% 10px 10px}.ui-timepicker-div.ui-timepicker-oneLine{padding-right:2px}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time,.ui-timepicker-div.ui-timepicker-oneLine dt{display:none}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label{display:block;padding-top:2px}.ui-timepicker-div.ui-timepicker-oneLine dl{text-align:right}.ui-timepicker-div.ui-timepicker-oneLine dl dd,.ui-timepicker-div.ui-timepicker-oneLine dl dd>div{display:inline-block;margin:0}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before{content:':';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before{content:'.';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide,.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide:before{display:none}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,9 @@
|
|||
var metrics = $("#chart").data("metrics");
|
||||
var columns = [];
|
||||
var groups = [];
|
||||
var categories = [];
|
||||
var inputFormat = d3.time.format("%Y-%m-%d");
|
||||
var outputFormat = d3.time.format($("#chart").data("date-format"));
|
||||
|
||||
for (var i = 0; i < metrics.length; i++) {
|
||||
|
||||
|
|
@ -19,7 +22,12 @@
|
|||
}
|
||||
}
|
||||
else {
|
||||
|
||||
columns[j].push(metrics[i][j]);
|
||||
|
||||
if (j == 0) {
|
||||
categories.push(outputFormat(inputFormat.parse(metrics[i][j])));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,16 +35,13 @@
|
|||
c3.generate({
|
||||
data: {
|
||||
columns: columns,
|
||||
x: metrics[0][0],
|
||||
type: 'area-spline',
|
||||
groups: [groups]
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
type: 'timeseries',
|
||||
tick: {
|
||||
format: $("#chart").data("date-format")
|
||||
}
|
||||
type: 'category',
|
||||
categories: categories
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -47,6 +52,9 @@
|
|||
{
|
||||
var metrics = $("#chart").data("metrics");
|
||||
var columns = [[$("#chart").data("label-total")]];
|
||||
var categories = [];
|
||||
var inputFormat = d3.time.format("%Y-%m-%d");
|
||||
var outputFormat = d3.time.format($("#chart").data("date-format"));
|
||||
|
||||
for (var i = 0; i < metrics.length; i++) {
|
||||
|
||||
|
|
@ -66,21 +74,22 @@
|
|||
|
||||
columns[0][i] += metrics[i][j];
|
||||
}
|
||||
|
||||
if (j == 0) {
|
||||
categories.push(outputFormat(inputFormat.parse(metrics[i][j])));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c3.generate({
|
||||
data: {
|
||||
columns: columns,
|
||||
x: metrics[0][0]
|
||||
columns: columns
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
type: 'timeseries',
|
||||
tick: {
|
||||
format: $("#chart").data("date-format")
|
||||
}
|
||||
type: 'category',
|
||||
categories: categories
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -125,11 +134,13 @@
|
|||
// Draw budget chart
|
||||
function drawBudget()
|
||||
{
|
||||
var categories = [];
|
||||
var metrics = $("#chart").data("metrics");
|
||||
var labels = $("#chart").data("labels");
|
||||
var inputFormat = d3.time.format("%Y-%m-%d");
|
||||
var outputFormat = d3.time.format($("#chart").data("date-format"));
|
||||
|
||||
var columns = [
|
||||
[labels["date"]],
|
||||
[labels["in"]],
|
||||
[labels["left"]],
|
||||
[labels["out"]]
|
||||
|
|
@ -141,30 +152,178 @@
|
|||
colors[labels["out"]] = '#DF3A01';
|
||||
|
||||
for (var i = 0; i < metrics.length; i++) {
|
||||
columns[0].push(metrics[i]["date"]);
|
||||
columns[1].push(metrics[i]["in"]);
|
||||
columns[2].push(metrics[i]["left"]);
|
||||
columns[3].push(metrics[i]["out"]);
|
||||
categories.push(outputFormat(inputFormat.parse(metrics[i]["date"])));
|
||||
columns[0].push(metrics[i]["in"]);
|
||||
columns[1].push(metrics[i]["left"]);
|
||||
columns[2].push(metrics[i]["out"]);
|
||||
}
|
||||
|
||||
c3.generate({
|
||||
data: {
|
||||
x: columns[0][0],
|
||||
columns: columns,
|
||||
colors: colors,
|
||||
type : 'bar'
|
||||
},
|
||||
bar: {
|
||||
width: {
|
||||
ratio: 0.25
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
x: {
|
||||
show: true
|
||||
},
|
||||
y: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
type: 'timeseries',
|
||||
type: 'category',
|
||||
categories: categories
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Draw chart for average time spent into each column
|
||||
function drawAvgTimeColumn()
|
||||
{
|
||||
var metrics = $("#chart").data("metrics");
|
||||
var plots = [$("#chart").data("label")];
|
||||
var categories = [];
|
||||
|
||||
for (var column_id in metrics) {
|
||||
plots.push(metrics[column_id].average);
|
||||
categories.push(metrics[column_id].title);
|
||||
}
|
||||
|
||||
c3.generate({
|
||||
data: {
|
||||
columns: [plots],
|
||||
type: 'bar'
|
||||
},
|
||||
bar: {
|
||||
width: {
|
||||
ratio: 0.5
|
||||
}
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
type: 'category',
|
||||
categories: categories
|
||||
},
|
||||
y: {
|
||||
tick: {
|
||||
format: $("#chart").data("date-format")
|
||||
format: formatDuration
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Draw chart for average time spent into each column
|
||||
function drawTaskTimeColumn()
|
||||
{
|
||||
var metrics = $("#chart").data("metrics");
|
||||
var plots = [$("#chart").data("label")];
|
||||
var categories = [];
|
||||
|
||||
for (var i = 0; i < metrics.length; i++) {
|
||||
plots.push(metrics[i].time_spent);
|
||||
categories.push(metrics[i].title);
|
||||
}
|
||||
|
||||
c3.generate({
|
||||
data: {
|
||||
columns: [plots],
|
||||
type: 'bar'
|
||||
},
|
||||
bar: {
|
||||
width: {
|
||||
ratio: 0.5
|
||||
}
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
type: 'category',
|
||||
categories: categories
|
||||
},
|
||||
y: {
|
||||
tick: {
|
||||
format: formatDuration
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Draw lead and cycle time for the project
|
||||
function drawLeadAndCycleTime()
|
||||
{
|
||||
var metrics = $("#chart").data("metrics");
|
||||
var cycle = [$("#chart").data("label-cycle")];
|
||||
var lead = [$("#chart").data("label-lead")];
|
||||
var categories = [];
|
||||
|
||||
var types = {};
|
||||
types[$("#chart").data("label-cycle")] = 'area';
|
||||
types[$("#chart").data("label-lead")] = 'area-spline';
|
||||
|
||||
var colors = {};
|
||||
colors[$("#chart").data("label-lead")] = '#afb42b';
|
||||
colors[$("#chart").data("label-cycle")] = '#4e342e';
|
||||
|
||||
for (var i = 0; i < metrics.length; i++) {
|
||||
cycle.push(parseInt(metrics[i].avg_cycle_time));
|
||||
lead.push(parseInt(metrics[i].avg_lead_time));
|
||||
categories.push(metrics[i].day);
|
||||
}
|
||||
|
||||
c3.generate({
|
||||
data: {
|
||||
columns: [
|
||||
lead,
|
||||
cycle
|
||||
],
|
||||
types: types,
|
||||
colors: colors
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
type: 'category',
|
||||
categories: categories
|
||||
},
|
||||
y: {
|
||||
tick: {
|
||||
format: formatDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(d)
|
||||
{
|
||||
if (d >= 86400) {
|
||||
return Math.round(d/86400) + "d";
|
||||
}
|
||||
else if (d >= 3600) {
|
||||
return Math.round(d/3600) + "h";
|
||||
}
|
||||
else if (d >= 60) {
|
||||
return Math.round(d/60) + "m";
|
||||
}
|
||||
|
||||
return d + "s";
|
||||
}
|
||||
|
||||
jQuery(document).ready(function() {
|
||||
|
||||
if (Kanboard.Exists("analytic-task-repartition")) {
|
||||
|
|
@ -182,6 +341,15 @@
|
|||
else if (Kanboard.Exists("budget-chart")) {
|
||||
drawBudget();
|
||||
}
|
||||
else if (Kanboard.Exists("analytic-avg-time-column")) {
|
||||
drawAvgTimeColumn();
|
||||
}
|
||||
else if (Kanboard.Exists("analytic-task-time-column")) {
|
||||
drawTaskTimeColumn();
|
||||
}
|
||||
else if (Kanboard.Exists("analytic-lead-cycle-time")) {
|
||||
drawLeadAndCycleTime();
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -277,6 +277,15 @@ var Kanboard = (function() {
|
|||
constrainInput: false
|
||||
});
|
||||
|
||||
// Datetime picker
|
||||
$(".form-datetime").datetimepicker({
|
||||
controlType: 'select',
|
||||
oneLine: true,
|
||||
dateFormat: 'yy-mm-dd',
|
||||
// timeFormat: 'h:mm tt',
|
||||
constrainInput: false
|
||||
});
|
||||
|
||||
// Markdown Preview for textareas
|
||||
$("#markdown-preview").click(Kanboard.MarkdownPreview);
|
||||
$("#markdown-write").click(Kanboard.MarkdownWriter);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
});
|
||||
|
||||
Mousetrap.bind("s", function() {
|
||||
stack_toggle();
|
||||
window.location = $(".board-display-mode").attr("href");
|
||||
});
|
||||
|
||||
Mousetrap.bind("c", function() {
|
||||
|
|
@ -28,74 +28,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Collapse/Expand tasks
|
||||
function stack_load_events()
|
||||
{
|
||||
$(".filter-expand-link").click(function(e) {
|
||||
e.preventDefault();
|
||||
stack_expand();
|
||||
Kanboard.SetStorageItem(stack_key(), "expanded");
|
||||
});
|
||||
|
||||
$(".filter-collapse-link").click(function(e) {
|
||||
e.preventDefault();
|
||||
stack_collapse();
|
||||
Kanboard.SetStorageItem(stack_key(), "collapsed");
|
||||
});
|
||||
|
||||
stack_show();
|
||||
}
|
||||
|
||||
function stack_key()
|
||||
{
|
||||
var projectId = $('#board').data('project-id');
|
||||
return "board_stacking_" + projectId;
|
||||
}
|
||||
|
||||
function stack_collapse()
|
||||
{
|
||||
$(".filter-collapse").hide();
|
||||
$(".task-board-collapsed").show();
|
||||
|
||||
$(".filter-expand").show();
|
||||
$(".task-board-expanded").hide();
|
||||
}
|
||||
|
||||
function stack_expand()
|
||||
{
|
||||
$(".filter-collapse").show();
|
||||
$(".task-board-collapsed").hide();
|
||||
|
||||
$(".filter-expand").hide();
|
||||
$(".task-board-expanded").show();
|
||||
}
|
||||
|
||||
function stack_toggle()
|
||||
{
|
||||
var state = Kanboard.GetStorageItem(stack_key()) || "expanded";
|
||||
|
||||
if (state === "expanded") {
|
||||
stack_collapse();
|
||||
Kanboard.SetStorageItem(stack_key(), "collapsed");
|
||||
}
|
||||
else {
|
||||
stack_expand();
|
||||
Kanboard.SetStorageItem(stack_key(), "expanded");
|
||||
}
|
||||
}
|
||||
|
||||
function stack_show()
|
||||
{
|
||||
var state = Kanboard.GetStorageItem(stack_key()) || "expanded";
|
||||
|
||||
if (state === "expanded") {
|
||||
stack_expand();
|
||||
}
|
||||
else {
|
||||
stack_collapse();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the board
|
||||
function board_load_events()
|
||||
{
|
||||
|
|
@ -243,7 +175,6 @@
|
|||
$("#main").append(data);
|
||||
Kanboard.InitAfterAjax();
|
||||
board_load_events();
|
||||
stack_show();
|
||||
compactview_reload();
|
||||
}
|
||||
});
|
||||
|
|
@ -263,7 +194,6 @@
|
|||
Kanboard.InitAfterAjax();
|
||||
board_unload_events();
|
||||
board_load_events();
|
||||
stack_show();
|
||||
compactview_reload();
|
||||
}
|
||||
}
|
||||
|
|
@ -312,7 +242,6 @@
|
|||
|
||||
if (Kanboard.Exists("board")) {
|
||||
board_load_events();
|
||||
stack_load_events();
|
||||
compactview_load_events();
|
||||
keyboard_shortcuts();
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,7 @@
|
|||
"eluceo/ical": "*",
|
||||
"erusev/parsedown" : "1.5.3",
|
||||
"fabiang/xmpp" : "0.6.1",
|
||||
"fguillot/json-rpc" : "dev-master",
|
||||
"fguillot/json-rpc" : "1.0.0",
|
||||
"fguillot/picodb" : "1.0.0",
|
||||
"fguillot/simpleLogger" : "0.0.2",
|
||||
"fguillot/simple-validator" : "0.0.3",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"hash": "1ef3f084b6c8651977b1bbc84d86cb69",
|
||||
"hash": "0048471872ea99cd30c53c0398c7d9f2",
|
||||
"packages": [
|
||||
{
|
||||
"name": "christian-riesen/base32",
|
||||
|
|
@ -260,16 +260,16 @@
|
|||
},
|
||||
{
|
||||
"name": "fguillot/json-rpc",
|
||||
"version": "dev-master",
|
||||
"version": "v1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fguillot/JsonRPC.git",
|
||||
"reference": "ac3ddcf8f74777d72b8044d6d128f41aabe292f2"
|
||||
"reference": "5a11f1414780a200f09b78d20ab72b5cee4faa95"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/ac3ddcf8f74777d72b8044d6d128f41aabe292f2",
|
||||
"reference": "ac3ddcf8f74777d72b8044d6d128f41aabe292f2",
|
||||
"url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/5a11f1414780a200f09b78d20ab72b5cee4faa95",
|
||||
"reference": "5a11f1414780a200f09b78d20ab72b5cee4faa95",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -292,7 +292,7 @@
|
|||
],
|
||||
"description": "Simple Json-RPC client/server library that just works",
|
||||
"homepage": "https://github.com/fguillot/JsonRPC",
|
||||
"time": "2015-07-01 19:26:37"
|
||||
"time": "2015-07-01 19:50:31"
|
||||
},
|
||||
{
|
||||
"name": "fguillot/picodb",
|
||||
|
|
@ -819,7 +819,6 @@
|
|||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {
|
||||
"fguillot/json-rpc": 20,
|
||||
"swiftmailer/swiftmailer": 0,
|
||||
"symfony/console": 0
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
Analytics for tasks
|
||||
===================
|
||||
|
||||
Each task have an analytics section available from the left menu in the task view.
|
||||
|
||||
Lead and cycle time
|
||||
-------------------
|
||||
|
||||

|
||||
|
||||
- The lead time is the time between the task creation and the date of completion (task closed).
|
||||
- The cycle time is the time between the start date and the date of completion.
|
||||
- If the task is not closed the current time is used instead of the date of completion.
|
||||
- If the start date is not specified, the cycle time is not calculated.
|
||||
|
||||
Note: You can configure an automatic action to define automatically the start date when you move a task to the column of your choice.
|
||||
|
||||
Time spent into each column
|
||||
---------------------------
|
||||
|
||||

|
||||
|
||||
- This chart show the total time spent into each column for the task.
|
||||
- The time spent is calculated until the task is closed.
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
Analytics
|
||||
=========
|
||||
|
||||
Each project have an analytics section. Depending how you are using Kanboard, you can see those reports:
|
||||
|
||||
User repartition
|
||||
----------------
|
||||
|
||||
|
|
@ -20,7 +22,8 @@ Cumulative flow diagram
|
|||
|
||||

|
||||
|
||||
This chart show the number of tasks cumulatively for each column over the time.
|
||||
- This chart show the number of tasks cumulatively for each column over the time.
|
||||
- Everyday, the total number of tasks is recorded for each column.
|
||||
|
||||
Note: You need to have at least 2 days of data to see the graph.
|
||||
|
||||
|
|
@ -30,13 +33,37 @@ Burndown chart
|
|||

|
||||
|
||||
The [burn down chart](http://en.wikipedia.org/wiki/Burn_down_chart) is available for each project.
|
||||
This chart is a graphical representation of work left to do versus time.
|
||||
|
||||
Kanboard use the complexity or story point to generate this diagram.
|
||||
- This chart is a graphical representation of work left to do versus time.
|
||||
- Kanboard use the complexity or story point to generate this diagram.
|
||||
- Everyday, the sum of the story points for each column is calculated.
|
||||
|
||||
Average time spent into each column
|
||||
-----------------------------------
|
||||
|
||||

|
||||
|
||||
This chart show the average time spent into each column for the last 1000 tasks.
|
||||
|
||||
- Kanboard use the task transitions to calculate the data.
|
||||
- The time spent is calculated until the task is closed.
|
||||
|
||||
Average Lead and Cycle time
|
||||
---------------------------
|
||||
|
||||

|
||||
|
||||
This chart show the average lead and cycle time for the last 1000 tasks over the time.
|
||||
|
||||
- The lead time is the time between the task creation and the date of completion.
|
||||
- The cycle time is time between the specified start date of the task to completion date.
|
||||
- If the task is not closed, the current time is used instead of the date of completion.
|
||||
|
||||
Those metrics are calculated and recorded everyday for the whole project.
|
||||
|
||||
Don't forget to run the daily job for stats calculation
|
||||
-------------------------------------------------------
|
||||
|
||||
To generate accurate analytics data, you should run the daily cronjob **project daily summaries** just before midnight.
|
||||
To generate accurate analytics data, you should run the daily cronjob **project daily statistics**.
|
||||
|
||||
[Read the documentation about Kanboard CLI](http://kanboard.net/documentation/cli)
|
||||
[Read the documentation about Kanboard CLI](cli.markdown)
|
||||
|
|
|
|||
|
|
@ -306,9 +306,19 @@ Response example:
|
|||
"name": "API test",
|
||||
"is_active": "1",
|
||||
"token": "",
|
||||
"last_modified": "1410263246",
|
||||
"last_modified": "1436119135",
|
||||
"is_public": "0",
|
||||
"description": "A sample project"
|
||||
"is_private": "0",
|
||||
"is_everybody_allowed": "0",
|
||||
"default_swimlane": "Default swimlane",
|
||||
"show_default_swimlane": "1",
|
||||
"description": "test",
|
||||
"identifier": "",
|
||||
"url": {
|
||||
"board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
|
||||
"calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
|
||||
"list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -345,9 +355,19 @@ Response example:
|
|||
"name": "Test",
|
||||
"is_active": "1",
|
||||
"token": "",
|
||||
"last_modified": "0",
|
||||
"last_modified": "1436119135",
|
||||
"is_public": "0",
|
||||
"description": "A sample project"
|
||||
"is_private": "0",
|
||||
"is_everybody_allowed": "0",
|
||||
"default_swimlane": "Default swimlane",
|
||||
"show_default_swimlane": "1",
|
||||
"description": "test",
|
||||
"identifier": "",
|
||||
"url": {
|
||||
"board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
|
||||
"calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
|
||||
"list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -366,7 +386,7 @@ Request example:
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "getAllProjects",
|
||||
"id": 134982303
|
||||
"id": 2134420212
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -375,25 +395,26 @@ Response example:
|
|||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 134982303,
|
||||
"id": 2134420212,
|
||||
"result": [
|
||||
{
|
||||
"id": "2",
|
||||
"name": "PHP client",
|
||||
"is_active": "1",
|
||||
"token": "",
|
||||
"last_modified": "0",
|
||||
"is_public": "0",
|
||||
"description": "PHP client project"
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Test",
|
||||
"name": "API test",
|
||||
"is_active": "1",
|
||||
"token": "",
|
||||
"last_modified": "0",
|
||||
"last_modified": "1436119570",
|
||||
"is_public": "0",
|
||||
"description": "Test project"
|
||||
"is_private": "0",
|
||||
"is_everybody_allowed": "0",
|
||||
"default_swimlane": "Default swimlane",
|
||||
"show_default_swimlane": "1",
|
||||
"description": null,
|
||||
"identifier": "",
|
||||
"url": {
|
||||
"board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
|
||||
"calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
|
||||
"list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1970,8 +1991,9 @@ Response example:
|
|||
"recurrence_timeframe": "0",
|
||||
"recurrence_basedate": "0",
|
||||
"recurrence_parent": null,
|
||||
"recurrence_child": null
|
||||
}
|
||||
"recurrence_child": null,
|
||||
"url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -2033,7 +2055,8 @@ Response example:
|
|||
"recurrence_timeframe": "0",
|
||||
"recurrence_basedate": "0",
|
||||
"recurrence_parent": null,
|
||||
"recurrence_child": null
|
||||
"recurrence_child": null,
|
||||
"url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=5&project_id=1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -2097,7 +2120,8 @@ Response example:
|
|||
"recurrence_timeframe": "0",
|
||||
"recurrence_basedate": "0",
|
||||
"recurrence_parent": null,
|
||||
"recurrence_child": null
|
||||
"recurrence_child": null,
|
||||
"url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=1"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
|
|
@ -2128,7 +2152,8 @@ Response example:
|
|||
"recurrence_timeframe": "0",
|
||||
"recurrence_basedate": "0",
|
||||
"recurrence_parent": null,
|
||||
"recurrence_child": null
|
||||
"recurrence_child": null,
|
||||
"url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=2&project_id=1"
|
||||
},
|
||||
...
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,36 +13,35 @@ Usage
|
|||
- Run the command `./kanboard`
|
||||
|
||||
```bash
|
||||
$ ./kanboard
|
||||
Kanboard version master
|
||||
|
||||
Usage:
|
||||
command [options] [arguments]
|
||||
command [options] [arguments]
|
||||
|
||||
Options:
|
||||
--help (-h) Display this help message
|
||||
--quiet (-q) Do not output any message
|
||||
--verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
|
||||
--version (-V) Display this application version
|
||||
--ansi Force ANSI output
|
||||
--no-ansi Disable ANSI output
|
||||
--no-interaction (-n) Do not ask any interactive question
|
||||
-h, --help Display this help message
|
||||
-q, --quiet Do not output any message
|
||||
-V, --version Display this application version
|
||||
--ansi Force ANSI output
|
||||
--no-ansi Disable ANSI output
|
||||
-n, --no-interaction Do not ask any interactive question
|
||||
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
|
||||
|
||||
Available commands:
|
||||
help Displays help for a command
|
||||
list Lists commands
|
||||
export
|
||||
export:daily-project-summary Daily project summary CSV export (number of tasks per column and per day)
|
||||
export:subtasks Subtasks CSV export
|
||||
export:tasks Tasks CSV export
|
||||
export:transitions Task transitions CSV export
|
||||
locale
|
||||
locale:compare Compare application translations with the fr_FR locale
|
||||
locale:sync Synchronize all translations based on the fr_FR locale
|
||||
notification
|
||||
notification:overdue-tasks Send notifications for overdue tasks
|
||||
projects
|
||||
projects:daily-summary Calculate daily summary data for all projects
|
||||
help Displays help for a command
|
||||
list Lists commands
|
||||
export
|
||||
export:daily-project-column-stats Daily project column stats CSV export (number of tasks per column and per day)
|
||||
export:subtasks Subtasks CSV export
|
||||
export:tasks Tasks CSV export
|
||||
export:transitions Task transitions CSV export
|
||||
locale
|
||||
locale:compare Compare application translations with the fr_FR locale
|
||||
locale:sync Synchronize all translations based on the fr_FR locale
|
||||
notification
|
||||
notification:overdue-tasks Send notifications for overdue tasks
|
||||
projects
|
||||
projects:daily-stats Calculate daily statistics for all projects
|
||||
```
|
||||
|
||||
Available commands
|
||||
|
|
@ -97,13 +96,13 @@ Example:
|
|||
The exported data will be printed on the standard output:
|
||||
|
||||
```bash
|
||||
./kanboard export:daily-project-summary <project_id> <start_date> <end_date>
|
||||
./kanboard export:daily-project-column-stats <project_id> <start_date> <end_date>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
./kanboard export:daily-project-summary 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv
|
||||
./kanboard export:daily-project-column-stats 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv
|
||||
```
|
||||
|
||||
### Send notifications for overdue tasks
|
||||
|
|
@ -133,12 +132,12 @@ Cronjob example:
|
|||
0 8 * * * cd /path/to/kanboard && ./kanboard notification:overdue-tasks >/dev/null 2>&1
|
||||
```
|
||||
|
||||
### Run daily project summaries calculation
|
||||
### Run daily project stats calculation
|
||||
|
||||
You can add a background task that calculate the daily project summaries everyday:
|
||||
You can add a background task to calculate the project statistics everyday:
|
||||
|
||||
```bash
|
||||
$ ./kanboard projects:daily-summary
|
||||
$ ./kanboard projects:daily-stats
|
||||
Run calculation for Project #0
|
||||
Run calculation for Project #1
|
||||
Run calculation for Project #10
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ This feature allow you to import Kanboard tasks in almost any calendar program (
|
|||
Calendar subscriptions are **read-only** access, you cannot create tasks from an external calendar software.
|
||||
The Calendar feed export follow the iCal standard.
|
||||
|
||||
Note: Only tasks within the date range of -2 months to +6 months are exported to the iCalendar feed.
|
||||
|
||||
Project calendars
|
||||
-----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ Using Kanboard
|
|||
- [Recurring tasks](recurring-tasks.markdown)
|
||||
- [Create tasks by email](create-tasks-by-email.markdown)
|
||||
- [Subtasks](subtasks.markdown)
|
||||
- [Analytics for tasks](analytics-tasks.markdown)
|
||||
|
||||
### Working with users
|
||||
|
||||
|
|
|
|||
|
|
@ -26,3 +26,13 @@ This feature use the [Incoming webhook](https://api.slack.com/incoming-webhooks)
|
|||
3. Copy the webhook url to the Kanboard settings page: **Settings > Integrations > Slack** or **Project settings > Integrations > Slack**
|
||||
|
||||
Now, Kanboard events will be sent to the Slack channel.
|
||||
|
||||
### Overriding Channel (Optional)
|
||||
|
||||
Optionally you can override the channel, private group or send direct messages by filling up **Channel/Group/User** text box. Leaving it empty will post to the channel configured during webhook configuration.
|
||||
|
||||
Examples:
|
||||
|
||||
- Send messages to another channel: **#mychannel1**
|
||||
- Send messages to a private group: **#myprivategroup1**
|
||||
- Send messages directly to someone: **@anotheruser1**
|
||||
|
|
|
|||
4
kanboard
4
kanboard
|
|
@ -12,8 +12,8 @@ $application = new Application('Kanboard', APP_VERSION);
|
|||
$application->add(new Console\TaskOverdueNotification($container));
|
||||
$application->add(new Console\SubtaskExport($container));
|
||||
$application->add(new Console\TaskExport($container));
|
||||
$application->add(new Console\ProjectDailySummaryCalculation($container));
|
||||
$application->add(new Console\ProjectDailySummaryExport($container));
|
||||
$application->add(new Console\ProjectDailyStatsCalculation($container));
|
||||
$application->add(new Console\ProjectDailyColumnStatsExport($container));
|
||||
$application->add(new Console\TransitionExport($container));
|
||||
$application->add(new Console\LocaleSync($container));
|
||||
$application->add(new Console\LocaleComparator($container));
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
require __DIR__.'/../app/common.php';
|
||||
|
||||
use Model\ProjectDailySummary;
|
||||
use Model\ProjectDailyColumnStats;
|
||||
use Model\TaskCreation;
|
||||
use Model\TaskStatus;
|
||||
|
||||
$pds = new ProjectDailySummary($container);
|
||||
$pds = new ProjectDailyColumnStats($container);
|
||||
$taskCreation = new TaskCreation($container);
|
||||
$taskStatus = new TaskStatus($container);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
require __DIR__.'/../app/common.php';
|
||||
|
||||
use Model\ProjectDailySummary;
|
||||
use Model\ProjectDailyColumnStats;
|
||||
use Model\TaskCreation;
|
||||
use Model\TaskPosition;
|
||||
|
||||
$pds = new ProjectDailySummary($container);
|
||||
$pds = new ProjectDailyColumnStats($container);
|
||||
$taskCreation = new TaskCreation($container);
|
||||
$taskPosition = new TaskPosition($container);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../app/common.php';
|
||||
|
||||
use Model\Project;
|
||||
use Model\ProjectDailyStats;
|
||||
|
||||
$p = new Project($container);
|
||||
$pds = new ProjectDailyStats($container);
|
||||
|
||||
$p->create(array('name' => 'Test Lead/Cycle time'));
|
||||
|
||||
$container['db']->table('tasks')->insert(array(
|
||||
'title' => 'Lead time = 4d | Cycle time = 3d',
|
||||
'date_creation' => strtotime('-7 days'),
|
||||
'date_started' => strtotime('-6 days'),
|
||||
'date_completed' => strtotime('-3 days'),
|
||||
'is_active' => 0,
|
||||
'project_id' => 1,
|
||||
'column_id' => 1,
|
||||
));
|
||||
|
||||
$container['db']->table('tasks')->insert(array(
|
||||
'title' => 'Lead time = 1d | Cycle time = 1d',
|
||||
'date_creation' => strtotime('-7 days'),
|
||||
'date_started' => strtotime('-7 days'),
|
||||
'date_completed' => strtotime('-6 days'),
|
||||
'is_active' => 0,
|
||||
'project_id' => 1,
|
||||
'column_id' => 1,
|
||||
));
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-6 days')));
|
||||
|
||||
$container['db']->table('tasks')->insert(array(
|
||||
'title' => 'Lead time = 7d | Cycle time = 5d',
|
||||
'date_creation' => strtotime('-7 days'),
|
||||
'date_started' => strtotime('-5 days'),
|
||||
'date_completed' => strtotime('today'),
|
||||
'is_active' => 0,
|
||||
'project_id' => 1,
|
||||
'column_id' => 1,
|
||||
));
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-5 days')));
|
||||
|
||||
$container['db']->table('tasks')->insert(array(
|
||||
'title' => 'Lead time = 1d | Cycle time = 0',
|
||||
'date_creation' => strtotime('-3 days'),
|
||||
'date_started' => 0,
|
||||
'date_completed' => 0,
|
||||
'is_active' => 0,
|
||||
'project_id' => 1,
|
||||
'column_id' => 1,
|
||||
));
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-4 days')));
|
||||
|
||||
$container['db']->table('tasks')->insert(array(
|
||||
'title' => 'Lead time = 1d | Cycle time = 1d',
|
||||
'date_creation' => strtotime('-3 days'),
|
||||
'date_started' => strtotime('-3 days'),
|
||||
'date_completed' => 0,
|
||||
'is_active' => 0,
|
||||
'project_id' => 1,
|
||||
'column_id' => 1,
|
||||
));
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-3 days')));
|
||||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
print_css="print links table board task comment subtask markdown"
|
||||
app_css="base links title table form button alert tooltip header board task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown screenshot filters"
|
||||
vendor_css="jquery-ui.min chosen.min fullcalendar.min font-awesome.min c3.min"
|
||||
vendor_css="jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min"
|
||||
|
||||
app_js="base board calendar analytic swimlane screenshot"
|
||||
vendor_js="jquery-1.11.1.min jquery-ui.min jquery.ui.touch-punch.min chosen.jquery.min dropit.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min app.min"
|
||||
vendor_js="jquery-1.11.1.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min dropit.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min app.min"
|
||||
lang_js="da de es fi fr hu it ja nl pl pt-br ru sv sr th tr zh-cn"
|
||||
|
||||
function merge_css {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ class Api extends PHPUnit_Framework_TestCase
|
|||
|
||||
if ($projects) {
|
||||
foreach ($projects as $project) {
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=board&action=show&project_id='.$project['id'], $project['url']['board']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=calendar&action=show&project_id='.$project['id'], $project['url']['calendar']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=listing&action=show&project_id='.$project['id'], $project['url']['list']);
|
||||
$this->assertTrue($this->client->removeProject($project['id']));
|
||||
}
|
||||
}
|
||||
|
|
@ -80,6 +83,9 @@ class Api extends PHPUnit_Framework_TestCase
|
|||
$project = $this->client->getProjectById(1);
|
||||
$this->assertNotEmpty($project);
|
||||
$this->assertEquals(1, $project['id']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=board&action=show&project_id='.$project['id'], $project['url']['board']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=calendar&action=show&project_id='.$project['id'], $project['url']['calendar']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=listing&action=show&project_id='.$project['id'], $project['url']['list']);
|
||||
}
|
||||
|
||||
public function testGetProjectByName()
|
||||
|
|
@ -87,6 +93,9 @@ class Api extends PHPUnit_Framework_TestCase
|
|||
$project = $this->client->getProjectByName('API test');
|
||||
$this->assertNotEmpty($project);
|
||||
$this->assertEquals(1, $project['id']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=board&action=show&project_id='.$project['id'], $project['url']['board']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=calendar&action=show&project_id='.$project['id'], $project['url']['calendar']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=listing&action=show&project_id='.$project['id'], $project['url']['list']);
|
||||
|
||||
$project = $this->client->getProjectByName(array('name' => 'API test'));
|
||||
$this->assertNotEmpty($project);
|
||||
|
|
@ -97,6 +106,18 @@ class Api extends PHPUnit_Framework_TestCase
|
|||
$this->assertNull($project);
|
||||
}
|
||||
|
||||
public function testGetAllProjects()
|
||||
{
|
||||
$projects = $this->client->getAllProjects();
|
||||
$this->assertNotEmpty($projects);
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=board&action=show&project_id='.$project['id'], $project['url']['board']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=calendar&action=show&project_id='.$project['id'], $project['url']['calendar']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=listing&action=show&project_id='.$project['id'], $project['url']['list']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testUpdateProject()
|
||||
{
|
||||
$project = $this->client->getProjectById(1);
|
||||
|
|
@ -385,6 +406,7 @@ class Api extends PHPUnit_Framework_TestCase
|
|||
$this->assertNotFalse($task);
|
||||
$this->assertTrue(is_array($task));
|
||||
$this->assertEquals('Task #1', $task['title']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'], $task['url']);
|
||||
}
|
||||
|
||||
public function testGetAllTasks()
|
||||
|
|
@ -394,6 +416,7 @@ class Api extends PHPUnit_Framework_TestCase
|
|||
$this->assertNotFalse($tasks);
|
||||
$this->assertTrue(is_array($tasks));
|
||||
$this->assertEquals('Task #1', $tasks[0]['title']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=task&action=show&task_id='.$tasks[0]['id'].'&project_id='.$tasks[0]['project_id'], $tasks[0]['url']);
|
||||
|
||||
$tasks = $this->client->getAllTasks(2, 0);
|
||||
|
||||
|
|
@ -1030,5 +1053,6 @@ class Api extends PHPUnit_Framework_TestCase
|
|||
$this->assertNotEmpty($task);
|
||||
$this->assertEquals('Task with external ticket number', $task['title']);
|
||||
$this->assertEquals('TICKET-1234', $task['reference']);
|
||||
$this->assertEquals('http://127.0.0.1:8000/?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'], $task['url']);
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,8 @@ abstract class Base extends PHPUnit_Framework_TestCase
|
|||
|
||||
public function setUp()
|
||||
{
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
if (DB_DRIVER === 'mysql') {
|
||||
$pdo = new PDO('mysql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD);
|
||||
$pdo->exec('DROP DATABASE '.DB_NAME);
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ class DateParserTest extends Base
|
|||
$this->assertEquals('2014-03-05', date('Y-m-d', $d->getTimestamp('2014-03-05')));
|
||||
$this->assertEquals('2014-03-05', date('Y-m-d', $d->getTimestamp('2014_03_05')));
|
||||
$this->assertEquals('2014-03-05', date('Y-m-d', $d->getTimestamp('03/05/2014')));
|
||||
$this->assertEquals('2014-03-25 17:18', date('Y-m-d H:i', $d->getTimestamp('03/25/2014 5:18 pm')));
|
||||
$this->assertEquals('2014-03-25 05:18', date('Y-m-d H:i', $d->getTimestamp('03/25/2014 5:18 am')));
|
||||
$this->assertEquals('2014-03-25 05:18', date('Y-m-d H:i', $d->getTimestamp('03/25/2014 5:18am')));
|
||||
$this->assertEquals('2014-03-25 23:14', date('Y-m-d H:i', $d->getTimestamp('03/25/2014 23:14')));
|
||||
$this->assertEquals('2014-03-29 23:14', date('Y-m-d H:i', $d->getTimestamp('2014_03_29 23:14')));
|
||||
$this->assertEquals('2014-03-29 23:14', date('Y-m-d H:i', $d->getTimestamp('2014-03-29 23:14')));
|
||||
}
|
||||
|
||||
public function testConvert()
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
require_once __DIR__.'/Base.php';
|
||||
|
||||
use Helper\Datetime;
|
||||
use Helper\Dt;
|
||||
|
||||
class DatetimeHelperTest extends Base
|
||||
{
|
||||
public function testAge()
|
||||
{
|
||||
$h = new Datetime($this->container);
|
||||
$h = new Dt($this->container);
|
||||
|
||||
$this->assertEquals('<15m', $h->age(0, 30));
|
||||
$this->assertEquals('<30m', $h->age(0, 1000));
|
||||
|
|
@ -20,7 +20,7 @@ class DatetimeHelperTest extends Base
|
|||
|
||||
public function testGetDayHours()
|
||||
{
|
||||
$h = new Datetime($this->container);
|
||||
$h = new Dt($this->container);
|
||||
|
||||
$slots = $h->getDayHours();
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ class DatetimeHelperTest extends Base
|
|||
|
||||
public function testGetWeekDays()
|
||||
{
|
||||
$h = new Datetime($this->container);
|
||||
$h = new Dt($this->container);
|
||||
|
||||
$slots = $h->getWeekDays();
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class DatetimeHelperTest extends Base
|
|||
|
||||
public function testGetWeekDay()
|
||||
{
|
||||
$h = new Datetime($this->container);
|
||||
$h = new Dt($this->container);
|
||||
|
||||
$this->assertEquals('Monday', $h->getWeekDay(1));
|
||||
$this->assertEquals('Sunday', $h->getWeekDay(7));
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@
|
|||
require_once __DIR__.'/Base.php';
|
||||
|
||||
use Model\Project;
|
||||
use Model\ProjectDailySummary;
|
||||
use Model\ProjectDailyColumnStats;
|
||||
use Model\Task;
|
||||
use Model\TaskCreation;
|
||||
use Model\TaskStatus;
|
||||
|
||||
class ProjectDailySummaryTest extends Base
|
||||
class ProjectDailyColumnStatsTest extends Base
|
||||
{
|
||||
public function testUpdateTotals()
|
||||
{
|
||||
$p = new Project($this->container);
|
||||
$pds = new ProjectDailySummary($this->container);
|
||||
$pds = new ProjectDailyColumnStats($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$ts = new TaskStatus($this->container);
|
||||
|
||||
|
|
@ -175,6 +175,28 @@ class TaskCreationTest extends Base
|
|||
$this->assertEquals(1, $task['creator_id']);
|
||||
}
|
||||
|
||||
public function testThatCreatorIsDefined()
|
||||
{
|
||||
$p = new Project($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFinder($this->container);
|
||||
|
||||
$_SESSION = array();
|
||||
$_SESSION['user']['id'] = 1;
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'test')));
|
||||
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
|
||||
|
||||
$task = $tf->getById(1);
|
||||
$this->assertNotEmpty($task);
|
||||
$this->assertNotFalse($task);
|
||||
|
||||
$this->assertEquals(1, $task['id']);
|
||||
$this->assertEquals(1, $task['creator_id']);
|
||||
|
||||
$_SESSION = array();
|
||||
}
|
||||
|
||||
public function testColumnId()
|
||||
{
|
||||
$p = new Project($this->container);
|
||||
|
|
@ -284,29 +306,35 @@ class TaskCreationTest extends Base
|
|||
|
||||
public function testDateStarted()
|
||||
{
|
||||
$date = '2014-11-23';
|
||||
$timestamp = strtotime('+2days');
|
||||
$p = new Project($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFinder($this->container);
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'test')));
|
||||
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => $date)));
|
||||
$this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => $timestamp)));
|
||||
|
||||
// Set only a date
|
||||
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '2014-11-24')));
|
||||
|
||||
$task = $tf->getById(1);
|
||||
$this->assertNotEmpty($task);
|
||||
$this->assertNotFalse($task);
|
||||
$this->assertEquals('2014-11-24 '.date('H:i'), date('Y-m-d H:i', $task['date_started']));
|
||||
|
||||
$this->assertEquals(1, $task['id']);
|
||||
$this->assertEquals($date, date('Y-m-d', $task['date_started']));
|
||||
// Set a datetime
|
||||
$this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '2014-11-24 16:25')));
|
||||
|
||||
$task = $tf->getById(2);
|
||||
$this->assertNotEmpty($task);
|
||||
$this->assertNotFalse($task);
|
||||
$this->assertEquals('2014-11-24 16:25', date('Y-m-d H:i', $task['date_started']));
|
||||
|
||||
$this->assertEquals(2, $task['id']);
|
||||
$this->assertEquals($timestamp, $task['date_started']);
|
||||
// Set a datetime
|
||||
$this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => '2014-11-24 6:25pm')));
|
||||
|
||||
$task = $tf->getById(3);
|
||||
$this->assertEquals('2014-11-24 18:25', date('Y-m-d H:i', $task['date_started']));
|
||||
|
||||
// Set a timestamp
|
||||
$this->assertEquals(4, $tc->create(array('project_id' => 1, 'title' => 'test', 'date_started' => time())));
|
||||
|
||||
$task = $tf->getById(4);
|
||||
$this->assertEquals(time(), $task['date_started'], '', 1);
|
||||
}
|
||||
|
||||
public function testTime()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,36 @@ use Model\Swimlane;
|
|||
|
||||
class TaskDuplicationTest extends Base
|
||||
{
|
||||
public function testThatDuplicateDefineCreator()
|
||||
{
|
||||
$td = new TaskDuplication($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFinder($this->container);
|
||||
$p = new Project($this->container);
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'test1')));
|
||||
$this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
|
||||
|
||||
$task = $tf->getById(1);
|
||||
$this->assertNotEmpty($task);
|
||||
$this->assertEquals(1, $task['position']);
|
||||
$this->assertEquals(1, $task['project_id']);
|
||||
$this->assertEquals(0, $task['creator_id']);
|
||||
|
||||
$_SESSION = array();
|
||||
$_SESSION['user']['id'] = 1;
|
||||
|
||||
// We duplicate our task
|
||||
$this->assertEquals(2, $td->duplicate(1));
|
||||
|
||||
// Check the values of the duplicated task
|
||||
$task = $tf->getById(2);
|
||||
$this->assertNotEmpty($task);
|
||||
$this->assertEquals(1, $task['creator_id']);
|
||||
|
||||
$_SESSION = array();
|
||||
}
|
||||
|
||||
public function testDuplicateSameProject()
|
||||
{
|
||||
$td = new TaskDuplication($this->container);
|
||||
|
|
|
|||
|
|
@ -9,9 +9,60 @@ use Model\TaskCreation;
|
|||
use Model\DateParser;
|
||||
use Model\Category;
|
||||
use Model\Subtask;
|
||||
use Model\Config;
|
||||
|
||||
class TaskFilterTest extends Base
|
||||
{
|
||||
public function testIcalEventsWithCreatorAndDueDate()
|
||||
{
|
||||
$dp = new DateParser($this->container);
|
||||
$p = new Project($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFilter($this->container);
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'test')));
|
||||
$this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'creator_id' => 1, 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
|
||||
|
||||
$events = $tf->create()->filterByDueDateRange(strtotime('-1 month'), strtotime('+1 month'))->addAllDayIcalEvents();
|
||||
$ics = $events->render();
|
||||
|
||||
$this->assertContains('UID:task-#1-date_due', $ics);
|
||||
$this->assertContains('DTSTART;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('-2 days')), $ics);
|
||||
$this->assertContains('DTEND;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('-2 days')), $ics);
|
||||
$this->assertContains('URL:http://localhost/?controller=task&action=show&task_id=1&project_id=1', $ics);
|
||||
$this->assertContains('SUMMARY:#1 task1', $ics);
|
||||
$this->assertContains('ATTENDEE:MAILTO:admin@kanboard.local', $ics);
|
||||
$this->assertContains('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE', $ics);
|
||||
}
|
||||
|
||||
public function testIcalEventsWithAssigneeAndDueDate()
|
||||
{
|
||||
$dp = new DateParser($this->container);
|
||||
$p = new Project($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFilter($this->container);
|
||||
$u = new User($this->container);
|
||||
$c = new Config($this->container);
|
||||
|
||||
$this->assertNotFalse($c->save(array('application_url' => 'http://kb/')));
|
||||
$this->assertEquals('http://kb/', $c->get('application_url'));
|
||||
|
||||
$this->assertNotFalse($u->update(array('id' => 1, 'email' => 'bob@localhost')));
|
||||
$this->assertEquals(1, $p->create(array('name' => 'test')));
|
||||
$this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'owner_id' => 1, 'date_due' => $dp->getTimestampFromIsoFormat('+5 days'))));
|
||||
|
||||
$events = $tf->create()->filterByDueDateRange(strtotime('-1 month'), strtotime('+1 month'))->addAllDayIcalEvents();
|
||||
$ics = $events->render();
|
||||
|
||||
$this->assertContains('UID:task-#1-date_due', $ics);
|
||||
$this->assertContains('DTSTART;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('+5 days')), $ics);
|
||||
$this->assertContains('DTEND;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('+5 days')), $ics);
|
||||
$this->assertContains('URL:http://kb/?controller=task&action=show&task_id=1&project_id=1', $ics);
|
||||
$this->assertContains('SUMMARY:#1 task1', $ics);
|
||||
$this->assertContains('ORGANIZER:MAILTO:bob@localhost', $ics);
|
||||
$this->assertContains('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE', $ics);
|
||||
}
|
||||
|
||||
public function testSearchWithEmptyResult()
|
||||
{
|
||||
$dp = new DateParser($this->container);
|
||||
|
|
@ -441,19 +492,13 @@ class TaskFilterTest extends Base
|
|||
$this->assertEquals('my task title is awesome', $tasks[0]['title']);
|
||||
$this->assertEquals('my task title is amazing', $tasks[1]['title']);
|
||||
|
||||
|
||||
$tf->search('assignee:nobody');
|
||||
$tasks = $tf->findAll();
|
||||
$this->assertNotEmpty($tasks);
|
||||
$this->assertCount(1, $tasks);
|
||||
$this->assertEquals('my task title is amazing', $tasks[0]['title']);
|
||||
|
||||
|
||||
|
||||
$this->assertEquals('my task title is amazing', $tasks[0]['title']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testCopy()
|
||||
{
|
||||
$tf = new TaskFilter($this->container);
|
||||
|
|
|
|||
|
|
@ -202,15 +202,29 @@ class TaskModificationTest extends Base
|
|||
$task = $tf->getById(1);
|
||||
$this->assertEquals(0, $task['date_started']);
|
||||
|
||||
// Set only a date
|
||||
$this->assertTrue($tm->update(array('id' => 1, 'date_started' => '2014-11-24')));
|
||||
|
||||
$task = $tf->getById(1);
|
||||
$this->assertEquals('2014-11-24', date('Y-m-d', $task['date_started']));
|
||||
$this->assertEquals('2014-11-24 '.date('H:i'), date('Y-m-d H:i', $task['date_started']));
|
||||
|
||||
// Set a datetime
|
||||
$this->assertTrue($tm->update(array('id' => 1, 'date_started' => '2014-11-24 16:25')));
|
||||
|
||||
$task = $tf->getById(1);
|
||||
$this->assertEquals('2014-11-24 16:25', date('Y-m-d H:i', $task['date_started']));
|
||||
|
||||
// Set a datetime
|
||||
$this->assertTrue($tm->update(array('id' => 1, 'date_started' => '2014-11-24 6:25pm')));
|
||||
|
||||
$task = $tf->getById(1);
|
||||
$this->assertEquals('2014-11-24 18:25', date('Y-m-d H:i', $task['date_started']));
|
||||
|
||||
// Set a timestamp
|
||||
$this->assertTrue($tm->update(array('id' => 1, 'date_started' => time())));
|
||||
|
||||
$task = $tf->getById(1);
|
||||
$this->assertEquals(date('Y-m-d'), date('Y-m-d', $task['date_started']));
|
||||
$this->assertEquals(time(), $task['date_started'], '', 1);
|
||||
}
|
||||
|
||||
public function testChangeTimeEstimated()
|
||||
|
|
|
|||
|
|
@ -11,49 +11,88 @@ use Model\Swimlane;
|
|||
|
||||
class TaskPositionTest extends Base
|
||||
{
|
||||
public function testCalculatePositionBadPosition()
|
||||
public function testMoveTaskToWrongPosition()
|
||||
{
|
||||
$tp = new TaskPosition($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFinder($this->container);
|
||||
$p = new Project($this->container);
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'Project #1')));
|
||||
$this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
|
||||
|
||||
$this->assertFalse($tp->calculatePositions(1, 1, 2, 0));
|
||||
$this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1)));
|
||||
$this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1)));
|
||||
|
||||
// We move the task 2 to the position 0
|
||||
$this->assertFalse($tp->movePosition(1, 1, 3, 0));
|
||||
|
||||
// Check tasks position
|
||||
$task = $tf->getById(1);
|
||||
$this->assertEquals(1, $task['id']);
|
||||
$this->assertEquals(1, $task['column_id']);
|
||||
$this->assertEquals(1, $task['position']);
|
||||
|
||||
$task = $tf->getById(2);
|
||||
$this->assertEquals(2, $task['id']);
|
||||
$this->assertEquals(1, $task['column_id']);
|
||||
$this->assertEquals(2, $task['position']);
|
||||
}
|
||||
|
||||
public function testCalculatePositionBadColumn()
|
||||
public function testMoveTaskToGreaterPosition()
|
||||
{
|
||||
$tp = new TaskPosition($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFinder($this->container);
|
||||
$p = new Project($this->container);
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'Project #1')));
|
||||
$this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
|
||||
|
||||
$this->assertFalse($tp->calculatePositions(1, 1, 10, 1));
|
||||
$this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1)));
|
||||
$this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1)));
|
||||
|
||||
// We move the task 2 to the position 42
|
||||
$this->assertTrue($tp->movePosition(1, 1, 1, 42));
|
||||
|
||||
// Check tasks position
|
||||
$task = $tf->getById(1);
|
||||
$this->assertEquals(1, $task['id']);
|
||||
$this->assertEquals(1, $task['column_id']);
|
||||
$this->assertEquals(2, $task['position']);
|
||||
|
||||
$task = $tf->getById(2);
|
||||
$this->assertEquals(2, $task['id']);
|
||||
$this->assertEquals(1, $task['column_id']);
|
||||
$this->assertEquals(1, $task['position']);
|
||||
}
|
||||
|
||||
public function testCalculatePositions()
|
||||
public function testMoveTaskToEmptyColumn()
|
||||
{
|
||||
$tp = new TaskPosition($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFinder($this->container);
|
||||
$p = new Project($this->container);
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'Project #1')));
|
||||
$this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1)));
|
||||
|
||||
$positions = $tp->calculatePositions(1, 1, 2, 1);
|
||||
$this->assertNotFalse($positions);
|
||||
$this->assertNotEmpty($positions);
|
||||
$this->assertEmpty($positions[1]);
|
||||
$this->assertEmpty($positions[3]);
|
||||
$this->assertEmpty($positions[4]);
|
||||
$this->assertEquals(array(1), $positions[2]);
|
||||
$this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1)));
|
||||
$this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 1)));
|
||||
|
||||
// We move the task 2 to the column 3
|
||||
$this->assertTrue($tp->movePosition(1, 1, 3, 1));
|
||||
|
||||
// Check tasks position
|
||||
$task = $tf->getById(1);
|
||||
$this->assertEquals(1, $task['id']);
|
||||
$this->assertEquals(3, $task['column_id']);
|
||||
$this->assertEquals(1, $task['position']);
|
||||
|
||||
$task = $tf->getById(2);
|
||||
$this->assertEquals(2, $task['id']);
|
||||
$this->assertEquals(1, $task['column_id']);
|
||||
$this->assertEquals(1, $task['position']);
|
||||
}
|
||||
|
||||
public function testMoveTaskWithColumnThatNotChange()
|
||||
public function testMoveTaskToAnotherColumn()
|
||||
{
|
||||
$tp = new TaskPosition($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
|
|
@ -116,40 +155,6 @@ class TaskPositionTest extends Base
|
|||
$this->assertEquals(3, $task['position']);
|
||||
}
|
||||
|
||||
public function testMoveTaskWithBadPreviousPosition()
|
||||
{
|
||||
$tp = new TaskPosition($this->container);
|
||||
$tc = new TaskCreation($this->container);
|
||||
$tf = new TaskFinder($this->container);
|
||||
$p = new Project($this->container);
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'Project #1')));
|
||||
$this->assertEquals(1, $this->container['db']->table('tasks')->insert(array('title' => 'A', 'column_id' => 1, 'project_id' => 1, 'position' => 1)));
|
||||
|
||||
// Both tasks have the same position
|
||||
$this->assertEquals(2, $this->container['db']->table('tasks')->insert(array('title' => 'B', 'column_id' => 2, 'project_id' => 1, 'position' => 1)));
|
||||
$this->assertEquals(3, $this->container['db']->table('tasks')->insert(array('title' => 'C', 'column_id' => 2, 'project_id' => 1, 'position' => 1)));
|
||||
|
||||
// Move the first column to the last position of the 2nd column
|
||||
$this->assertTrue($tp->movePosition(1, 1, 2, 3));
|
||||
|
||||
// Check tasks position
|
||||
$task = $tf->getById(2);
|
||||
$this->assertEquals(2, $task['id']);
|
||||
$this->assertEquals(2, $task['column_id']);
|
||||
$this->assertEquals(1, $task['position']);
|
||||
|
||||
$task = $tf->getById(3);
|
||||
$this->assertEquals(3, $task['id']);
|
||||
$this->assertEquals(2, $task['column_id']);
|
||||
$this->assertEquals(2, $task['position']);
|
||||
|
||||
$task = $tf->getById(1);
|
||||
$this->assertEquals(1, $task['id']);
|
||||
$this->assertEquals(2, $task['column_id']);
|
||||
$this->assertEquals(3, $task['position']);
|
||||
}
|
||||
|
||||
public function testMoveTaskTop()
|
||||
{
|
||||
$tp = new TaskPosition($this->container);
|
||||
|
|
|
|||
|
|
@ -38,22 +38,26 @@ class UrlHelperTest extends Base
|
|||
{
|
||||
$h = new Url($this->container);
|
||||
|
||||
$this->assertEquals('http://localhost/', $h->server());
|
||||
|
||||
$_SERVER['PHP_SELF'] = '/';
|
||||
$_SERVER['SERVER_NAME'] = 'localhost';
|
||||
$_SERVER['SERVER_NAME'] = 'kb';
|
||||
$_SERVER['SERVER_PORT'] = 1234;
|
||||
|
||||
$this->assertEquals('http://localhost:1234/', $h->server());
|
||||
$this->assertEquals('http://kb:1234/', $h->server());
|
||||
}
|
||||
|
||||
public function testBase()
|
||||
{
|
||||
$h = new Url($this->container);
|
||||
|
||||
$this->assertEquals('http://localhost/', $h->base());
|
||||
|
||||
$_SERVER['PHP_SELF'] = '/';
|
||||
$_SERVER['SERVER_NAME'] = 'localhost';
|
||||
$_SERVER['SERVER_NAME'] = 'kb';
|
||||
$_SERVER['SERVER_PORT'] = 1234;
|
||||
|
||||
$this->assertEquals('http://localhost:1234/', $h->base());
|
||||
$this->assertEquals('http://kb:1234/', $h->base());
|
||||
|
||||
$c = new Config($this->container);
|
||||
$c->save(array('application_url' => 'https://mykanboard/'));
|
||||
|
|
|
|||
Loading…
Reference in New Issue