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()
|
||||
{
|
||||
// $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(
|
||||
'projectActivity', // Order is important
|
||||
'projectDailySummary',
|
||||
'action',
|
||||
'project',
|
||||
'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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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.',
|
||||
'Edit Project' => 'Modifier le projet',
|
||||
'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',
|
||||
'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',
|
||||
@@ -592,4 +592,6 @@ return array(
|
||||
'This value is required' => 'Cette valeur est obligatoire',
|
||||
'This value must be numeric' => 'Cette valeur doit être numérique',
|
||||
'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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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 must be numeric' => '',
|
||||
// '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)
|
||||
{
|
||||
$this->db->startTransaction();
|
||||
|
||||
foreach ($columns as $column_id => $column) {
|
||||
|
||||
$position = 1;
|
||||
@@ -336,14 +334,11 @@ class Task extends Base
|
||||
$position++;
|
||||
|
||||
if (! $result) {
|
||||
$this->db->cancelTransaction();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->closeTransaction();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,25 @@ namespace Schema;
|
||||
use PDO;
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,24 @@ namespace Schema;
|
||||
use PDO;
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,24 @@ namespace Schema;
|
||||
use Core\Security;
|
||||
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)
|
||||
{
|
||||
|
||||
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>
|
||||
<?= Helper\a(t('User repartition'), 'analytic', 'users', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
<li>
|
||||
<?= Helper\a(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
Reference in New Issue
Block a user