Add cumulative flow diagram
This commit is contained in:
@@ -81,4 +81,48 @@ class Analytic extends Base
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show cumulative flow diagram
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
public function cfd()
|
||||||
|
{
|
||||||
|
$project = $this->getProject();
|
||||||
|
$values = $this->request->getValues();
|
||||||
|
|
||||||
|
$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'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->isAjax()) {
|
||||||
|
$this->response->json(array(
|
||||||
|
'columns' => array_values($this->board->getColumnsList($project['id'])),
|
||||||
|
'metrics' => $this->projectDailySummary->getRawMetrics($project['id'], $from, $to),
|
||||||
|
'labels' => array(
|
||||||
|
'column' => t('Column'),
|
||||||
|
'day' => t('Date'),
|
||||||
|
'total' => t('Tasks'),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->response->html($this->layout('analytic/cfd', array(
|
||||||
|
'values' => array(
|
||||||
|
'from' => $from,
|
||||||
|
'to' => $to,
|
||||||
|
),
|
||||||
|
'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2,
|
||||||
|
'project' => $project,
|
||||||
|
'date_format' => $this->config->get('application_date_format'),
|
||||||
|
'date_formats' => $this->dateParser->getAvailableFormats(),
|
||||||
|
'title' => t('Cumulative flow diagram for "%s"', $project['name']),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ abstract class Base
|
|||||||
*/
|
*/
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
// $this->container['logger']->addDebug(var_export($this->container['db']->getLogMessages(), true));
|
// foreach ($this->container['db']->getLogMessages() as $message) {
|
||||||
|
// $this->container['logger']->addDebug($message);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,6 +175,7 @@ abstract class Base
|
|||||||
{
|
{
|
||||||
$models = array(
|
$models = array(
|
||||||
'projectActivity', // Order is important
|
'projectActivity', // Order is important
|
||||||
|
'projectDailySummary',
|
||||||
'action',
|
'action',
|
||||||
'project',
|
'project',
|
||||||
'webhook',
|
'webhook',
|
||||||
|
|||||||
28
app/Event/ProjectDailySummaryListener.php
Normal file
28
app/Event/ProjectDailySummaryListener.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project daily summary listener
|
||||||
|
*
|
||||||
|
* @package event
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class ProjectDailySummaryListener extends Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Execute the action
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param array $data Event data dictionary
|
||||||
|
* @return bool True if the action was executed or false when not executed
|
||||||
|
*/
|
||||||
|
public function execute(array $data)
|
||||||
|
{
|
||||||
|
if (isset($data['project_id'])) {
|
||||||
|
return $this->projectDailySummary->updateTotals($data['project_id'], date('Y-m-d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ return array(
|
|||||||
'Column removed successfully.' => 'Colonne supprimée avec succès.',
|
'Column removed successfully.' => 'Colonne supprimée avec succès.',
|
||||||
'Edit Project' => 'Modifier le projet',
|
'Edit Project' => 'Modifier le projet',
|
||||||
'Github Issue' => 'Ticket Github',
|
'Github Issue' => 'Ticket Github',
|
||||||
'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique',
|
'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique.',
|
||||||
'Previous' => 'Précédent',
|
'Previous' => 'Précédent',
|
||||||
'The id must be an integer' => 'L\'id doit être un entier',
|
'The id must be an integer' => 'L\'id doit être un entier',
|
||||||
'The project id must be an integer' => 'L\'id du projet doit être un entier',
|
'The project id must be an integer' => 'L\'id du projet doit être un entier',
|
||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
'This value is required' => 'Cette valeur est obligatoire',
|
'This value is required' => 'Cette valeur est obligatoire',
|
||||||
'This value must be numeric' => 'Cette valeur doit être numérique',
|
'This value must be numeric' => 'Cette valeur doit être numérique',
|
||||||
'Unable to create this task.' => 'Impossible de créer cette tâche',
|
'Unable to create this task.' => 'Impossible de créer cette tâche',
|
||||||
|
'Cumulative flow diagram' => 'Diagramme de flux cumulé',
|
||||||
|
'Cumulative flow diagram for "%s"' => 'Diagramme de flux cumulé pour « %s »',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -592,4 +592,6 @@ return array(
|
|||||||
// 'This value is required' => '',
|
// 'This value is required' => '',
|
||||||
// 'This value must be numeric' => '',
|
// 'This value must be numeric' => '',
|
||||||
// 'Unable to create this task.' => '',
|
// 'Unable to create this task.' => '',
|
||||||
|
// 'Cumulative flow diagram' => '',
|
||||||
|
// 'Cumulative flow diagram for "%s"' => '',
|
||||||
);
|
);
|
||||||
|
|||||||
181
app/Model/ProjectDailySummary.php
Normal file
181
app/Model/ProjectDailySummary.php
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Model;
|
||||||
|
|
||||||
|
use Core\Template;
|
||||||
|
use Event\ProjectDailySummaryListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project daily summary
|
||||||
|
*
|
||||||
|
* @package model
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class ProjectDailySummary extends Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* SQL table name
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const TABLE = 'project_daily_summaries';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
return $this->db->transaction(function($db) use ($project_id, $date) {
|
||||||
|
|
||||||
|
$column_ids = $db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id');
|
||||||
|
|
||||||
|
foreach ($column_ids as $column_id) {
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
'day' => $date,
|
||||||
|
'project_id' => $project_id,
|
||||||
|
'column_id' => $column_id,
|
||||||
|
'total' => 0,
|
||||||
|
));
|
||||||
|
|
||||||
|
$db->table(ProjectDailySummary::TABLE)
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->eq('column_id', $column_id)
|
||||||
|
->eq('day', $date)
|
||||||
|
->update(array(
|
||||||
|
'total' => $db->table(Task::TABLE)
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->eq('column_id', $column_id)
|
||||||
|
->eq('is_active', Task::STATUS_OPEN)
|
||||||
|
->count()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of recorded days for the 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 integer
|
||||||
|
*/
|
||||||
|
public function countDays($project_id, $from, $to)
|
||||||
|
{
|
||||||
|
$rq = $this->db->execute(
|
||||||
|
'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ?',
|
||||||
|
array($from, $to)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $rq->fetchColumn(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(ProjectDailySummary::TABLE)
|
||||||
|
->columns(
|
||||||
|
ProjectDailySummary::TABLE.'.column_id',
|
||||||
|
ProjectDailySummary::TABLE.'.day',
|
||||||
|
ProjectDailySummary::TABLE.'.total',
|
||||||
|
Board::TABLE.'.title AS column_title'
|
||||||
|
)
|
||||||
|
->join(Board::TABLE, 'id', 'column_id')
|
||||||
|
->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
|
||||||
|
->gte('day', $from)
|
||||||
|
->lte('day', $to)
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get aggregated metrics for the project within a data range
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* ['Date', 'Column1', 'Column2'],
|
||||||
|
* ['2014-11-16', 2, 5],
|
||||||
|
* ['2014-11-17', 20, 15],
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @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 getAggregatedMetrics($project_id, $from, $to)
|
||||||
|
{
|
||||||
|
$columns = $this->board->getColumnsList($project_id);
|
||||||
|
$column_ids = array_keys($columns);
|
||||||
|
$metrics = array(array(e('Date')) + $columns);
|
||||||
|
$aggregates = array();
|
||||||
|
|
||||||
|
// Fetch metrics for the project
|
||||||
|
$records = $this->db->table(ProjectDailySummary::TABLE)
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->gte('day', $from)
|
||||||
|
->lte('day', $to)
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
// Aggregate by day
|
||||||
|
foreach ($records as $record) {
|
||||||
|
|
||||||
|
if (! isset($aggregates[$record['day']])) {
|
||||||
|
$aggregates[$record['day']] = array($record['day']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aggregates[$record['day']][$record['column_id']] = $record['total'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate by row
|
||||||
|
foreach ($aggregates as $aggregate) {
|
||||||
|
|
||||||
|
$row = array($aggregate[0]);
|
||||||
|
|
||||||
|
foreach ($column_ids as $column_id) {
|
||||||
|
$row[] = (int) $aggregate[$column_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
$metrics[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach events to be able to record the metrics
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
public function attachEvents()
|
||||||
|
{
|
||||||
|
$events = array(
|
||||||
|
Task::EVENT_CREATE,
|
||||||
|
Task::EVENT_CLOSE,
|
||||||
|
Task::EVENT_OPEN,
|
||||||
|
Task::EVENT_MOVE_COLUMN,
|
||||||
|
);
|
||||||
|
|
||||||
|
$listener = new ProjectDailySummaryListener($this->container);
|
||||||
|
|
||||||
|
foreach ($events as $event_name) {
|
||||||
|
$this->event->attach($event_name, $listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -309,8 +309,6 @@ class Task extends Base
|
|||||||
*/
|
*/
|
||||||
private function savePositions($moved_task_id, array $columns)
|
private function savePositions($moved_task_id, array $columns)
|
||||||
{
|
{
|
||||||
$this->db->startTransaction();
|
|
||||||
|
|
||||||
foreach ($columns as $column_id => $column) {
|
foreach ($columns as $column_id => $column) {
|
||||||
|
|
||||||
$position = 1;
|
$position = 1;
|
||||||
@@ -336,14 +334,11 @@ class Task extends Base
|
|||||||
$position++;
|
$position++;
|
||||||
|
|
||||||
if (! $result) {
|
if (! $result) {
|
||||||
$this->db->cancelTransaction();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->db->closeTransaction();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,25 @@ namespace Schema;
|
|||||||
use PDO;
|
use PDO;
|
||||||
use Core\Security;
|
use Core\Security;
|
||||||
|
|
||||||
const VERSION = 34;
|
const VERSION = 35;
|
||||||
|
|
||||||
|
function version_35($pdo)
|
||||||
|
{
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE project_daily_summaries (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
day CHAR(10) NOT NULL,
|
||||||
|
project_id INT NOT NULL,
|
||||||
|
column_id INT NOT NULL,
|
||||||
|
total INT NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY(id),
|
||||||
|
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB CHARSET=utf8
|
||||||
|
");
|
||||||
|
|
||||||
|
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
|
||||||
|
}
|
||||||
|
|
||||||
function version_34($pdo)
|
function version_34($pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,24 @@ namespace Schema;
|
|||||||
use PDO;
|
use PDO;
|
||||||
use Core\Security;
|
use Core\Security;
|
||||||
|
|
||||||
const VERSION = 15;
|
const VERSION = 16;
|
||||||
|
|
||||||
|
function version_16($pdo)
|
||||||
|
{
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE project_daily_summaries (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
day CHAR(10) NOT NULL,
|
||||||
|
project_id INTEGER NOT NULL,
|
||||||
|
column_id INTEGER NOT NULL,
|
||||||
|
total INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
|
||||||
|
}
|
||||||
|
|
||||||
function version_15($pdo)
|
function version_15($pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,24 @@ namespace Schema;
|
|||||||
use Core\Security;
|
use Core\Security;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
|
||||||
const VERSION = 34;
|
const VERSION = 35;
|
||||||
|
|
||||||
|
function version_35($pdo)
|
||||||
|
{
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE project_daily_summaries (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
day TEXT NOT NULL,
|
||||||
|
project_id INTEGER NOT NULL,
|
||||||
|
column_id INTEGER NOT NULL,
|
||||||
|
total INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
|
||||||
|
}
|
||||||
|
|
||||||
function version_34($pdo)
|
function version_34($pdo)
|
||||||
{
|
{
|
||||||
|
|||||||
26
app/Template/analytic/cfd.php
Normal file
26
app/Template/analytic/cfd.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<h2><?= t('Cumulative flow diagram') ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (! $display_graph): ?>
|
||||||
|
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<section id="analytic-cfd">
|
||||||
|
<div id="chart" data-url="<?= Helper\u('analytic', 'cfd', array('project_id' => $project['id'], 'from' => $values['from'], 'to' => $values['to'])) ?>"></div>
|
||||||
|
</section>
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<form method="post" class="form-inline" action="<?= Helper\u('analytic', 'cfd', array('project_id' => $project['id'])) ?>" autocomplete="off">
|
||||||
|
|
||||||
|
<?= Helper\form_csrf() ?>
|
||||||
|
|
||||||
|
<?= Helper\form_label(t('Start Date'), 'from') ?>
|
||||||
|
<?= Helper\form_text('from', $values, array(), array('required', 'placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?>
|
||||||
|
|
||||||
|
<?= Helper\form_label(t('End Date'), 'to') ?>
|
||||||
|
<?= Helper\form_text('to', $values, array(), array('required', 'placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?>
|
||||||
|
|
||||||
|
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
|
||||||
|
</form>
|
||||||
@@ -7,5 +7,8 @@
|
|||||||
<li>
|
<li>
|
||||||
<?= Helper\a(t('User repartition'), 'analytic', 'users', array('project_id' => $project['id'])) ?>
|
<?= Helper\a(t('User repartition'), 'analytic', 'users', array('project_id' => $project['id'])) ?>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<?= Helper\a(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +33,15 @@ ul.no-bullet li {
|
|||||||
|
|
||||||
.pull-right {
|
.pull-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}/* links */
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
/* links */
|
||||||
a {
|
a {
|
||||||
color: #3366CC;
|
color: #3366CC;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -280,6 +288,10 @@ ul.form-errors li {
|
|||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-inline .form-required {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
input.form-date {
|
input.form-date {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,3 +33,10 @@ ul.no-bullet li {
|
|||||||
.pull-right {
|
.pull-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ ul.form-errors li {
|
|||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-inline .form-required {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
input.form-date {
|
input.form-date {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,64 @@ Kanboard.Analytic = (function() {
|
|||||||
else if (Kanboard.Exists("analytic-user-repartition")) {
|
else if (Kanboard.Exists("analytic-user-repartition")) {
|
||||||
Kanboard.Analytic.UserRepartition.Init();
|
Kanboard.Analytic.UserRepartition.Init();
|
||||||
}
|
}
|
||||||
|
else if (Kanboard.Exists("analytic-cfd")) {
|
||||||
|
Kanboard.Analytic.CFD.Init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
Kanboard.Analytic.CFD = (function() {
|
||||||
|
|
||||||
|
function fetchData()
|
||||||
|
{
|
||||||
|
jQuery.getJSON($("#chart").attr("data-url"), function(data) {
|
||||||
|
drawGraph(data.metrics, data.labels, data.columns);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGraph(metrics, labels, columns)
|
||||||
|
{
|
||||||
|
var series = prepareSeries(metrics, labels);
|
||||||
|
|
||||||
|
var svg = dimple.newSvg("#chart", 800, 380);
|
||||||
|
var chart = new dimple.chart(svg, series);
|
||||||
|
|
||||||
|
var x = chart.addCategoryAxis("x", labels['day']);
|
||||||
|
x.addOrderRule("Date");
|
||||||
|
|
||||||
|
chart.addMeasureAxis("y", labels['total']);
|
||||||
|
|
||||||
|
var s = chart.addSeries(labels['column'], dimple.plot.area);
|
||||||
|
s.addOrderRule(columns.reverse());
|
||||||
|
|
||||||
|
chart.addLegend(10, 10, 500, 30, "left");
|
||||||
|
chart.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSeries(metrics, labels)
|
||||||
|
{
|
||||||
|
var series = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < metrics.length; i++) {
|
||||||
|
|
||||||
|
var row = {};
|
||||||
|
row[labels['column']] = metrics[i]['column_title'];
|
||||||
|
row[labels['day']] = metrics[i]['day'];
|
||||||
|
row[labels['total']] = metrics[i]['total'];
|
||||||
|
series.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Init: fetchData
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
Kanboard.Analytic.TaskRepartition = (function() {
|
Kanboard.Analytic.TaskRepartition = (function() {
|
||||||
|
|
||||||
function fetchData()
|
function fetchData()
|
||||||
|
|||||||
@@ -279,11 +279,64 @@ Kanboard.Analytic = (function() {
|
|||||||
else if (Kanboard.Exists("analytic-user-repartition")) {
|
else if (Kanboard.Exists("analytic-user-repartition")) {
|
||||||
Kanboard.Analytic.UserRepartition.Init();
|
Kanboard.Analytic.UserRepartition.Init();
|
||||||
}
|
}
|
||||||
|
else if (Kanboard.Exists("analytic-cfd")) {
|
||||||
|
Kanboard.Analytic.CFD.Init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
Kanboard.Analytic.CFD = (function() {
|
||||||
|
|
||||||
|
function fetchData()
|
||||||
|
{
|
||||||
|
jQuery.getJSON($("#chart").attr("data-url"), function(data) {
|
||||||
|
drawGraph(data.metrics, data.labels, data.columns);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGraph(metrics, labels, columns)
|
||||||
|
{
|
||||||
|
var series = prepareSeries(metrics, labels);
|
||||||
|
|
||||||
|
var svg = dimple.newSvg("#chart", 800, 380);
|
||||||
|
var chart = new dimple.chart(svg, series);
|
||||||
|
|
||||||
|
var x = chart.addCategoryAxis("x", labels['day']);
|
||||||
|
x.addOrderRule("Date");
|
||||||
|
|
||||||
|
chart.addMeasureAxis("y", labels['total']);
|
||||||
|
|
||||||
|
var s = chart.addSeries(labels['column'], dimple.plot.area);
|
||||||
|
s.addOrderRule(columns.reverse());
|
||||||
|
|
||||||
|
chart.addLegend(10, 10, 500, 30, "left");
|
||||||
|
chart.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSeries(metrics, labels)
|
||||||
|
{
|
||||||
|
var series = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < metrics.length; i++) {
|
||||||
|
|
||||||
|
var row = {};
|
||||||
|
row[labels['column']] = metrics[i]['column_title'];
|
||||||
|
row[labels['day']] = metrics[i]['day'];
|
||||||
|
row[labels['total']] = metrics[i]['total'];
|
||||||
|
series.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Init: fetchData
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
Kanboard.Analytic.TaskRepartition = (function() {
|
Kanboard.Analytic.TaskRepartition = (function() {
|
||||||
|
|
||||||
function fetchData()
|
function fetchData()
|
||||||
|
|||||||
61
scripts/create-sample-cfd.php
Executable file
61
scripts/create-sample-cfd.php
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/../app/common.php';
|
||||||
|
|
||||||
|
use Model\ProjectDailySummary;
|
||||||
|
use Model\Task;
|
||||||
|
|
||||||
|
$pds = new ProjectDailySummary($container);
|
||||||
|
$taskModel = new Task($container);
|
||||||
|
|
||||||
|
for ($i = 1; $i <= 15; $i++) {
|
||||||
|
|
||||||
|
$task = array(
|
||||||
|
'title' => 'Task #'.$i,
|
||||||
|
'project_id' => 1,
|
||||||
|
'column_id' => 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
$taskModel->create($task);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-7 days')));
|
||||||
|
|
||||||
|
$taskModel->movePosition(1, 1, 2, 1);
|
||||||
|
$taskModel->movePosition(1, 2, 2, 1);
|
||||||
|
$taskModel->movePosition(1, 3, 2, 1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-6 days')));
|
||||||
|
|
||||||
|
$taskModel->movePosition(1, 3, 3, 1);
|
||||||
|
$taskModel->movePosition(1, 4, 3, 1);
|
||||||
|
$taskModel->movePosition(1, 5, 3, 1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-5 days')));
|
||||||
|
|
||||||
|
$taskModel->movePosition(1, 5, 4, 1);
|
||||||
|
$taskModel->movePosition(1, 6, 4, 1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-4 days')));
|
||||||
|
|
||||||
|
$taskModel->movePosition(1, 7, 4, 1);
|
||||||
|
$taskModel->movePosition(1, 8, 4, 1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-3 days')));
|
||||||
|
|
||||||
|
$taskModel->movePosition(1, 9, 3, 1);
|
||||||
|
$taskModel->movePosition(1, 10, 2, 1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-2 days')));
|
||||||
|
|
||||||
|
$taskModel->create(array('title' => 'Random task', 'project_id' => 1));
|
||||||
|
$taskModel->movePosition(1, 11, 2, 1);
|
||||||
|
$taskModel->movePosition(1, 12, 4, 1);
|
||||||
|
$taskModel->movePosition(1, 13, 4, 1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-2 days')));
|
||||||
|
|
||||||
|
$taskModel->movePosition(1, 14, 3, 1);
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-1 days')));
|
||||||
|
|
||||||
|
$taskModel->movePosition(1, 15, 4, 1);
|
||||||
|
$taskModel->movePosition(1, 16, 4, 1);
|
||||||
|
|
||||||
|
$taskModel->create(array('title' => 'Random task', 'project_id' => 1));
|
||||||
|
|
||||||
|
$pds->updateTotals(1, date('Y-m-d'));
|
||||||
83
tests/units/ProjectDailySummary.php
Normal file
83
tests/units/ProjectDailySummary.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__.'/Base.php';
|
||||||
|
|
||||||
|
use Model\Project;
|
||||||
|
use Model\ProjectDailySummary;
|
||||||
|
use Model\Task;
|
||||||
|
|
||||||
|
class ProjectDailySummaryTest extends Base
|
||||||
|
{
|
||||||
|
public function testUpdateTotals()
|
||||||
|
{
|
||||||
|
$p = new Project($this->container);
|
||||||
|
$pds = new ProjectDailySummary($this->container);
|
||||||
|
$t = new Task($this->container);
|
||||||
|
|
||||||
|
$this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
|
||||||
|
$this->assertEquals(0, $pds->countDays(1, date('Y-m-d', strtotime('-2days')), date('Y-m-d')));
|
||||||
|
|
||||||
|
for ($i = 0; $i < 10; $i++) {
|
||||||
|
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-2days')));
|
||||||
|
|
||||||
|
for ($i = 0; $i < 15; $i++) {
|
||||||
|
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i < 25; $i++) {
|
||||||
|
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$pds->updateTotals(1, date('Y-m-d', strtotime('-1 day')));
|
||||||
|
|
||||||
|
$this->assertNotFalse($t->close(1));
|
||||||
|
$this->assertNotFalse($t->close(2));
|
||||||
|
|
||||||
|
for ($i = 0; $i < 3; $i++) {
|
||||||
|
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i < 4; $i++) {
|
||||||
|
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$pds->updateTotals(1, date('Y-m-d'));
|
||||||
|
|
||||||
|
$this->assertEquals(3, $pds->countDays(3, date('Y-m-d', strtotime('-2days')), date('Y-m-d')));
|
||||||
|
$metrics = $pds->getAggregatedMetrics(1, date('Y-m-d', strtotime('-2days')), date('Y-m-d'));
|
||||||
|
|
||||||
|
$this->assertNotEmpty($metrics);
|
||||||
|
$this->assertEquals(4, count($metrics));
|
||||||
|
$this->assertEquals(5, count($metrics[0]));
|
||||||
|
$this->assertEquals('Backlog', $metrics[0][1]);
|
||||||
|
|
||||||
|
$this->assertEquals(date('Y-m-d', strtotime('-2days')), $metrics[1][0]);
|
||||||
|
$this->assertEquals(10, $metrics[1][1]);
|
||||||
|
$this->assertEquals(0, $metrics[1][2]);
|
||||||
|
$this->assertEquals(0, $metrics[1][3]);
|
||||||
|
$this->assertEquals(5, $metrics[1][4]);
|
||||||
|
|
||||||
|
$this->assertEquals(date('Y-m-d', strtotime('-1day')), $metrics[2][0]);
|
||||||
|
$this->assertEquals(10, $metrics[2][1]);
|
||||||
|
$this->assertEquals(25, $metrics[2][2]);
|
||||||
|
$this->assertEquals(15, $metrics[2][3]);
|
||||||
|
$this->assertEquals(5, $metrics[2][4]);
|
||||||
|
|
||||||
|
$this->assertEquals(date('Y-m-d'), $metrics[3][0]);
|
||||||
|
$this->assertEquals(8, $metrics[3][1]);
|
||||||
|
$this->assertEquals(30, $metrics[3][2]);
|
||||||
|
$this->assertEquals(18, $metrics[3][3]);
|
||||||
|
$this->assertEquals(9, $metrics[3][4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user