Move analytic logic to separate classes

This commit is contained in:
Frederic Guillot 2016-01-15 21:03:19 -05:00
parent 1bbd4faf56
commit c58478c0ab
13 changed files with 424 additions and 134 deletions

View File

@ -0,0 +1,50 @@
<?php
namespace Kanboard\Analytic;
use Kanboard\Core\Base;
use Kanboard\Model\Task;
/**
* Estimated/Spent Time Comparison
*
* @package analytic
* @author Frederic Guillot
*/
class EstimatedTimeComparisonAnalytic extends Base
{
/**
* Build report
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function build($project_id)
{
$rows = $this->db->table(Task::TABLE)
->columns('SUM(time_estimated) AS time_estimated', 'SUM(time_spent) AS time_spent', 'is_active')
->eq('project_id', $project_id)
->groupBy('is_active')
->findAll();
$metrics = array(
'open' => array(
'time_spent' => 0,
'time_estimated' => 0,
),
'closed' => array(
'time_spent' => 0,
'time_estimated' => 0,
),
);
foreach ($rows as $row) {
$key = $row['is_active'] == Task::STATUS_OPEN ? 'open' : 'closed';
$metrics[$key]['time_spent'] = $row['time_spent'];
$metrics[$key]['time_estimated'] = $row['time_estimated'];
}
return $metrics;
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Kanboard\Analytic;
use Kanboard\Core\Base;
/**
* Task Distribution
*
* @package analytic
* @author Frederic Guillot
*/
class TaskDistributionAnalytic extends Base
{
/**
* Build report
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function build($project_id)
{
$metrics = array();
$total = 0;
$columns = $this->board->getColumns($project_id);
foreach ($columns as $column) {
$nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
$total += $nb_tasks;
$metrics[] = array(
'column_title' => $column['title'],
'nb_tasks' => $nb_tasks,
);
}
if ($total === 0) {
return array();
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
return $metrics;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Kanboard\Analytic;
use Kanboard\Core\Base;
/**
* User Distribution
*
* @package analytic
* @author Frederic Guillot
*/
class UserDistributionAnalytic extends Base
{
/**
* Build Report
*
* @access public
* @param integer $project_id
* @return array
*/
public function build($project_id)
{
$metrics = array();
$total = 0;
$tasks = $this->taskFinder->getAll($project_id);
$users = $this->projectUserRole->getAssignableUsersList($project_id);
foreach ($tasks as $task) {
$user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
$total++;
if (! isset($metrics[$user])) {
$metrics[$user] = array(
'nb_tasks' => 0,
'percentage' => 0,
'user' => $user,
);
}
$metrics[$user]['nb_tasks']++;
}
if ($total === 0) {
return array();
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
ksort($metrics);
return array_values($metrics);
}
}

View File

@ -88,7 +88,7 @@ class Analytic extends Base
$this->response->html($this->layout('analytic/tasks', array(
'project' => $project,
'metrics' => $this->projectAnalytic->getTaskRepartition($project['id']),
'metrics' => $this->taskDistributionAnalytic->build($project['id']),
'title' => t('Task repartition for "%s"', $project['name']),
)));
}
@ -104,7 +104,7 @@ class Analytic extends Base
$this->response->html($this->layout('analytic/users', array(
'project' => $project,
'metrics' => $this->projectAnalytic->getUserRepartition($project['id']),
'metrics' => $this->userDistributionAnalytic->build($project['id']),
'title' => t('User repartition for "%s"', $project['name']),
)));
}
@ -177,7 +177,7 @@ class Analytic extends Base
{
$project = $this->getProject();
$params = $this->getProjectFilters('analytic', 'compareHours');
$query = $this->taskFilter->search('status:all')->filterByProject($params['project']['id'])->getQuery();
$query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery();
$paginator = $this->paginator
->setUrl('analytic', 'compareHours', array('project_id' => $project['id']))
@ -186,12 +186,10 @@ class Analytic extends Base
->setQuery($query)
->calculate();
$stats = $this->projectAnalytic->getHoursByStatus($project['id']);
$this->response->html($this->layout('analytic/compare_hours', array(
'project' => $project,
'paginator' => $paginator,
'metrics' => $stats,
'metrics' => $this->estimatedTimeComparisonAnalytic->build($project['id']),
'title' => t('Compare hours for "%s"', $project['name']),
)));
}

View File

@ -10,6 +10,9 @@ use Pimple\Container;
* @package core
* @author Frederic Guillot
*
* @property \Kanboard\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic
* @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic
* @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic
* @property \Kanboard\Core\Action\ActionManager $actionManager
* @property \Kanboard\Core\Cache\MemoryCache $memoryCache
* @property \Kanboard\Core\Event\EventManager $eventManager

View File

@ -10,82 +10,6 @@ namespace Kanboard\Model;
*/
class ProjectAnalytic extends Base
{
/**
* Get tasks repartition
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getTaskRepartition($project_id)
{
$metrics = array();
$total = 0;
$columns = $this->board->getColumns($project_id);
foreach ($columns as $column) {
$nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
$total += $nb_tasks;
$metrics[] = array(
'column_title' => $column['title'],
'nb_tasks' => $nb_tasks,
);
}
if ($total === 0) {
return array();
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
return $metrics;
}
/**
* Get users repartition
*
* @access public
* @param integer $project_id
* @return array
*/
public function getUserRepartition($project_id)
{
$metrics = array();
$total = 0;
$tasks = $this->taskFinder->getAll($project_id);
$users = $this->projectUserRole->getAssignableUsersList($project_id);
foreach ($tasks as $task) {
$user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
$total++;
if (! isset($metrics[$user])) {
$metrics[$user] = array(
'nb_tasks' => 0,
'percentage' => 0,
'user' => $user,
);
}
$metrics[$user]['nb_tasks']++;
}
if ($total === 0) {
return array();
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
ksort($metrics);
return array_values($metrics);
}
/**
* Get the average lead and cycle time
*
@ -179,49 +103,4 @@ class ProjectAnalytic extends Base
return $stats;
}
/**
* Get the time spent and estimated into each status
*
* @access public
* @param integer $project_id
* @return array
*/
public function getHoursByStatus($project_id)
{
$stats = array();
// Get the times related to each task
$tasks = $this->db
->table(Task::TABLE)
->columns('id', 'time_estimated', 'time_spent', 'is_active')
->eq('project_id', $project_id)
->desc('id')
->limit(1000)
->findAll();
// Init values
$stats['closed'] = array(
'time_spent' => 0,
'time_estimated' => 0,
);
$stats['open'] = array(
'time_spent' => 0,
'time_estimated' => 0,
);
// Add times spent and estimated to each status
foreach ($tasks as &$task) {
if ($task['is_active']) {
$stats['open']['time_estimated'] += $task['time_estimated'];
$stats['open']['time_spent'] += $task['time_spent'];
} else {
$stats['closed']['time_estimated'] += $task['time_estimated'];
$stats['closed']['time_spent'] += $task['time_spent'];
}
}
return $stats;
}
}

View File

@ -14,6 +14,11 @@ use Kanboard\Core\Http\Client as HttpClient;
class ClassProvider implements ServiceProviderInterface
{
private $classes = array(
'Analytic' => array(
'TaskDistributionAnalytic',
'UserDistributionAnalytic',
'EstimatedTimeComparisonAnalytic',
),
'Model' => array(
'Action',
'ActionParameter',

View File

@ -4,8 +4,8 @@
<div class="listing">
<ul>
<li><?= t('Estimated hours: ').'<strong>'.$this->e($metrics['open']['time_estimated']+$metrics['open']['time_estimated']) ?></strong></li>
<li><?= t('Actual hours: ').'<strong>'.$this->e($metrics['open']['time_spent']+$metrics['closed']['time_spent']) ?></strong></li>
<li><?= t('Estimated hours: ').'<strong>'.$this->e($metrics['open']['time_estimated'] + $metrics['closed']['time_estimated']) ?></strong></li>
<li><?= t('Actual hours: ').'<strong>'.$this->e($metrics['open']['time_spent'] + $metrics['closed']['time_spent']) ?></strong></li>
</ul>
</div>
@ -13,7 +13,12 @@
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
<?php else: ?>
<section id="analytic-compare-hours">
<div id="chart" data-metrics='<?= json_encode($metrics, JSON_HEX_APOS)?>' data-label-spent="<?= t('Hours Spent') ?>" data-label-estimated="<?= t('Hours Estimated') ?>"></div>
<div id="chart"
data-metrics='<?= json_encode($metrics, JSON_HEX_APOS)?>'
data-label-spent="<?= t('Hours Spent') ?>"
data-label-estimated="<?= t('Hours Estimated') ?>"
data-label-closed="<?= t('Closed') ?>"
data-label-open="<?= t('Open') ?>"></div>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('No tasks found.') ?></p>

File diff suppressed because one or more lines are too long

View File

@ -4,14 +4,16 @@ function CompareHoursColumnChart(app) {
CompareHoursColumnChart.prototype.execute = function() {
var metrics = $("#chart").data("metrics");
var labelOpen = $("#chart").data("label-open");
var labelClosed = $("#chart").data("label-closed");
var spent = [$("#chart").data("label-spent")];
var estimated = [$("#chart").data("label-estimated")];
var categories = [];
for (var status in metrics) {
spent.push(parseInt(metrics[status].time_spent));
estimated.push(parseInt(metrics[status].time_estimated));
categories.push(status);
spent.push(parseFloat(metrics[status].time_spent));
estimated.push(parseFloat(metrics[status].time_estimated));
categories.push(status == 'open' ? labelOpen : labelClosed);
}
c3.generate({

View File

@ -0,0 +1,99 @@
<?php
require_once __DIR__.'/../Base.php';
use Kanboard\Model\TaskCreation;
use Kanboard\Model\Project;
use Kanboard\Analytic\EstimatedTimeComparisonAnalytic;
class EstimatedTimeComparisonAnalyticTest extends Base
{
public function testBuild()
{
$taskCreationModel = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$estimatedTimeComparisonAnalytic = new EstimatedTimeComparisonAnalytic($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
$this->assertEquals(2, $projectModel->create(array('name' => 'test1')));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 5.5)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 1.75)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 1.25, 'is_active' => 0)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_spent' => 8.25)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_spent' => 0.25)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_spent' => 0.5, 'is_active' => 0)));
$expected = array(
'open' => array(
'time_spent' => 8.5,
'time_estimated' => 7.25,
),
'closed' => array(
'time_spent' => 0.5,
'time_estimated' => 1.25,
)
);
$this->assertEquals($expected, $estimatedTimeComparisonAnalytic->build(1));
}
public function testBuildWithNoClosedTask()
{
$taskCreationModel = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$estimatedTimeComparisonAnalytic = new EstimatedTimeComparisonAnalytic($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
$this->assertEquals(2, $projectModel->create(array('name' => 'test1')));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 5.5)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 1.75)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_spent' => 8.25)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_spent' => 0.25)));
$expected = array(
'open' => array(
'time_spent' => 8.5,
'time_estimated' => 7.25,
),
'closed' => array(
'time_spent' => 0,
'time_estimated' => 0,
)
);
$this->assertEquals($expected, $estimatedTimeComparisonAnalytic->build(1));
}
public function testBuildWithOnlyClosedTask()
{
$taskCreationModel = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$estimatedTimeComparisonAnalytic = new EstimatedTimeComparisonAnalytic($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
$this->assertEquals(2, $projectModel->create(array('name' => 'test1')));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 5.5, 'is_active' => 0)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_estimated' => 1.75, 'is_active' => 0)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_spent' => 8.25, 'is_active' => 0)));
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'time_spent' => 0.25, 'is_active' => 0)));
$expected = array(
'closed' => array(
'time_spent' => 8.5,
'time_estimated' => 7.25,
),
'open' => array(
'time_spent' => 0,
'time_estimated' => 0,
)
);
$this->assertEquals($expected, $estimatedTimeComparisonAnalytic->build(1));
}
}

View File

@ -0,0 +1,62 @@
<?php
require_once __DIR__.'/../Base.php';
use Kanboard\Model\TaskCreation;
use Kanboard\Model\Project;
use Kanboard\Analytic\TaskDistributionAnalytic;
class TaskDistributionAnalyticTest extends Base
{
public function testBuild()
{
$projectModel = new Project($this->container);
$taskDistributionModel = new TaskDistributionAnalytic($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
$this->assertEquals(2, $projectModel->create(array('name' => 'test1')));
$this->createTasks(1, 20, 1);
$this->createTasks(2, 30, 1);
$this->createTasks(3, 40, 1);
$this->createTasks(4, 10, 1);
$expected = array(
array(
'column_title' => 'Backlog',
'nb_tasks' => 20,
'percentage' => 20.0,
),
array(
'column_title' => 'Ready',
'nb_tasks' => 30,
'percentage' => 30.0,
),
array(
'column_title' => 'Work in progress',
'nb_tasks' => 40,
'percentage' => 40.0,
),
array(
'column_title' => 'Done',
'nb_tasks' => 10,
'percentage' => 10.0,
)
);
$this->assertEquals($expected, $taskDistributionModel->build(1));
}
private function createTasks($column_id, $nb_active, $nb_inactive)
{
$taskCreationModel = new TaskCreation($this->container);
for ($i = 0; $i < $nb_active; $i++) {
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'column_id' => $column_id, 'is_active' => 1)));
}
for ($i = 0; $i < $nb_inactive; $i++) {
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'column_id' => $column_id, 'is_active' => 0)));
}
}
}

View File

@ -0,0 +1,83 @@
<?php
require_once __DIR__.'/../Base.php';
use Kanboard\Model\TaskCreation;
use Kanboard\Model\Project;
use Kanboard\Model\ProjectUserRole;
use Kanboard\Model\User;
use Kanboard\Analytic\UserDistributionAnalytic;
use Kanboard\Core\Security\Role;
class UserDistributionAnalyticTest extends Base
{
public function testBuild()
{
$projectModel = new Project($this->container);
$projectUserRoleModel = new ProjectUserRole($this->container);
$userModel = new User($this->container);
$userDistributionModel = new UserDistributionAnalytic($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'test1')));
$this->assertEquals(2, $projectModel->create(array('name' => 'test1')));
$this->assertEquals(2, $userModel->create(array('username' => 'user1')));
$this->assertEquals(3, $userModel->create(array('username' => 'user2')));
$this->assertEquals(4, $userModel->create(array('username' => 'user3')));
$this->assertEquals(5, $userModel->create(array('username' => 'user4')));
$this->assertTrue($projectUserRoleModel->addUser(1, 2, Role::PROJECT_MEMBER));
$this->assertTrue($projectUserRoleModel->addUser(1, 3, Role::PROJECT_MEMBER));
$this->assertTrue($projectUserRoleModel->addUser(1, 4, Role::PROJECT_MEMBER));
$this->assertTrue($projectUserRoleModel->addUser(1, 5, Role::PROJECT_MEMBER));
$this->createTasks(0, 10, 1);
$this->createTasks(2, 30, 1);
$this->createTasks(3, 40, 1);
$this->createTasks(4, 10, 1);
$this->createTasks(5, 10, 1);
$expected = array(
array(
'user' => 'Unassigned',
'nb_tasks' => 10,
'percentage' => 10.0,
),
array(
'user' => 'user1',
'nb_tasks' => 30,
'percentage' => 30.0,
),
array(
'user' => 'user2',
'nb_tasks' => 40,
'percentage' => 40.0,
),
array(
'user' => 'user3',
'nb_tasks' => 10,
'percentage' => 10.0,
),
array(
'user' => 'user4',
'nb_tasks' => 10,
'percentage' => 10.0,
)
);
$this->assertEquals($expected, $userDistributionModel->build(1));
}
private function createTasks($user_id, $nb_active, $nb_inactive)
{
$taskCreationModel = new TaskCreation($this->container);
for ($i = 0; $i < $nb_active; $i++) {
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'owner_id' => $user_id, 'is_active' => 1)));
}
for ($i = 0; $i < $nb_inactive; $i++) {
$this->assertNotFalse($taskCreationModel->create(array('project_id' => 1, 'title' => 'test', 'owner_id' => $user_id, 'is_active' => 0)));
}
}
}