Merge branch 'feature.issue-summary' into develop
This commit is contained in:
commit
9bbcd571ec
@ -133,6 +133,47 @@ class IDF_Project extends Pluf_Model
|
|||||||
return $projects[0];
|
return $projects[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of open/closed issues.
|
||||||
|
*
|
||||||
|
* @param string Status ('open'), 'closed'
|
||||||
|
* @param IDF_Tag Subfilter with a label (null)
|
||||||
|
* @return int Count
|
||||||
|
*/
|
||||||
|
public function getIssueCountByOwner($status='open')
|
||||||
|
{
|
||||||
|
switch ($status) {
|
||||||
|
case 'open':
|
||||||
|
$tags = implode(',', $this->getTagIdsByStatus('open'));
|
||||||
|
break;
|
||||||
|
case 'closed':
|
||||||
|
default:
|
||||||
|
$tags = implode(',', $this->getTagIdsByStatus('closed'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$sqlIssueTable = Pluf::factory('IDF_Issue')->getSqlTable();
|
||||||
|
$query = <<<"QUERY"
|
||||||
|
SELECT uid AS id,COUNT(uid) AS nb
|
||||||
|
FROM (
|
||||||
|
SELECT COALESCE(owner, -1) AS uid
|
||||||
|
FROM $sqlIssueTable
|
||||||
|
WHERE status IN ($tags)
|
||||||
|
) AS ff
|
||||||
|
GROUP BY uid
|
||||||
|
QUERY;
|
||||||
|
$db = Pluf::db();
|
||||||
|
$dbData = $db->select($query);
|
||||||
|
$ownerStatistics = array();
|
||||||
|
foreach ($dbData as $k => $v) {
|
||||||
|
$key = ($v['id'] === '-1') ? null : $v['id'];
|
||||||
|
$ownerStatistics[$key] = (int)$v['nb'];
|
||||||
|
}
|
||||||
|
|
||||||
|
arsort($ownerStatistics);
|
||||||
|
|
||||||
|
return $ownerStatistics;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of open/closed issues.
|
* Returns the number of open/closed issues.
|
||||||
*
|
*
|
||||||
|
@ -77,6 +77,79 @@ class IDF_Views_Issue
|
|||||||
$params, $request);
|
$params, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View the issue summary.
|
||||||
|
* TODO Add thoses data in cache, and process it only after an issue update
|
||||||
|
*/
|
||||||
|
public $summary_precond = array('IDF_Precondition::accessIssues');
|
||||||
|
public function summary($request, $match)
|
||||||
|
{
|
||||||
|
$tagStatistics = array();
|
||||||
|
$ownerStatistics = array();
|
||||||
|
$status = array();
|
||||||
|
$isTrackerEmpty = false;
|
||||||
|
|
||||||
|
$prj = $request->project;
|
||||||
|
$opened = $prj->getIssueCountByStatus('open');
|
||||||
|
$closed = $prj->getIssueCountByStatus('closed');
|
||||||
|
|
||||||
|
// Check if the tracker is empty
|
||||||
|
if ($opened === 0 && $closed === 0) {
|
||||||
|
$isTrackerEmpty = true;
|
||||||
|
} else {
|
||||||
|
if ($opened > 0 || $closed > 0) {
|
||||||
|
// Issue status statistics
|
||||||
|
$status['Open'] = array($opened, (int)(100 * $opened / ($opened + $closed)));
|
||||||
|
$status['Closed'] = array($closed, (int)(100 * $closed / ($opened + $closed)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($opened > 0) {
|
||||||
|
// Issue owner statistics
|
||||||
|
$owners = $prj->getIssueCountByOwner('open');
|
||||||
|
foreach ($owners as $user => $nb) {
|
||||||
|
if ($user === '') {
|
||||||
|
$key = __('Not assigned');
|
||||||
|
} else {
|
||||||
|
$obj = Pluf::factory('Pluf_User')->getOne(array('filter'=>'id='.$user));
|
||||||
|
$key = $obj->first_name . ' ' . $obj->last_name;
|
||||||
|
}
|
||||||
|
$ownerStatistics[$key] = array($nb, (int)(100 * $nb / $opened));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue class tag statistics
|
||||||
|
$tags = $prj->getTagCloud();
|
||||||
|
foreach ($tags as $t) {
|
||||||
|
$tagStatistics[$t->class][$t->name] = array($t->nb_use, $t->id);
|
||||||
|
}
|
||||||
|
foreach($tagStatistics as $k => $v) {
|
||||||
|
$nbIssueInClass = 0;
|
||||||
|
foreach ($v as $val) {
|
||||||
|
$nbIssueInClass += $val[0];
|
||||||
|
}
|
||||||
|
foreach ($v as $kk => $vv) {
|
||||||
|
$tagStatistics[$k][$kk] = array($vv[0], (int)(100 * $vv[0] / $nbIssueInClass), $vv[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
krsort($tagStatistics);
|
||||||
|
arsort($ownerStatistics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = sprintf(__('Summary of tracked issues in %s.'), (string) $prj);
|
||||||
|
|
||||||
|
return Pluf_Shortcuts_RenderToResponse('idf/issues/summary.html',
|
||||||
|
array('page_title' => $title,
|
||||||
|
'trackerEmpty' => $isTrackerEmpty,
|
||||||
|
'project' => $prj,
|
||||||
|
'tagStatistics' => $tagStatistics,
|
||||||
|
'ownerStatistics' => $ownerStatistics,
|
||||||
|
'status' => $status,
|
||||||
|
),
|
||||||
|
$request);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View the issues watch list of a given user.
|
* View the issues watch list of a given user.
|
||||||
* Limited to a specified project
|
* Limited to a specified project
|
||||||
|
@ -118,6 +118,11 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/$#',
|
|||||||
'model' => 'IDF_Views_Issue',
|
'model' => 'IDF_Views_Issue',
|
||||||
'method' => 'index');
|
'method' => 'index');
|
||||||
|
|
||||||
|
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/summary/$#',
|
||||||
|
'base' => $base,
|
||||||
|
'model' => 'IDF_Views_Issue',
|
||||||
|
'method' => 'summary');
|
||||||
|
|
||||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/search/$#',
|
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/search/$#',
|
||||||
'base' => $base,
|
'base' => $base,
|
||||||
'model' => 'IDF_Views_Issue',
|
'model' => 'IDF_Views_Issue',
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
{block tabissues} class="active"{/block}
|
{block tabissues} class="active"{/block}
|
||||||
{block subtabs}
|
{block subtabs}
|
||||||
<div id="sub-tabs">
|
<div id="sub-tabs">
|
||||||
<a {if $inOpenIssues}class="active" {/if}href="{url 'IDF_Views_Issue::index', array($project.shortname)}">{trans 'Open Issues'}</a>
|
<a {if $inSummaryIssues}class="active" {/if}href="{url 'IDF_Views_Issue::summary', array($project.shortname)}">{trans 'Summary'}</a>
|
||||||
|
| <a {if $inOpenIssues}class="active" {/if}href="{url 'IDF_Views_Issue::index', array($project.shortname)}">{trans 'Open Issues'}</a>
|
||||||
{if !$user.isAnonymous()} | <a {if $inCreate}class="active" {/if}href="{url 'IDF_Views_Issue::create', array($project.shortname)}">{trans 'New Issue'}</a> | <a {if $inMyIssues}class="active" {/if}href="{url 'IDF_Views_Issue::myIssues', array($project.shortname, 'submit')}">{trans 'My Issues'}</a>
|
{if !$user.isAnonymous()} | <a {if $inCreate}class="active" {/if}href="{url 'IDF_Views_Issue::create', array($project.shortname)}">{trans 'New Issue'}</a> | <a {if $inMyIssues}class="active" {/if}href="{url 'IDF_Views_Issue::myIssues', array($project.shortname, 'submit')}">{trans 'My Issues'}</a>
|
||||||
| <a {if $inWatchList}class="active" {/if}href="{url 'IDF_Views_Issue::watchList', array($project.shortname, 'open')}">{trans 'My watch list'}</a>{/if} |
|
| <a {if $inWatchList}class="active" {/if}href="{url 'IDF_Views_Issue::watchList', array($project.shortname, 'open')}">{trans 'My watch list'}</a>{/if} |
|
||||||
<form class="star" action="{url 'IDF_Views_Issue::search', array($project.shortname)}" method="get">
|
<form class="star" action="{url 'IDF_Views_Issue::search', array($project.shortname)}" method="get">
|
||||||
|
95
src/IDF/templates/idf/issues/summary.html
Normal file
95
src/IDF/templates/idf/issues/summary.html
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
{extends "idf/issues/base.html"}
|
||||||
|
|
||||||
|
{block docclass}yui-t2{assign $inSummaryIssues=true}{/block}
|
||||||
|
|
||||||
|
{block body}
|
||||||
|
{if $trackerEmpty}
|
||||||
|
{aurl 'create_url', 'IDF_Views_Issue::create', array($project.shortname)}
|
||||||
|
<p>{blocktrans}The issue tracker is empty.<br />You can create your first issue <a href="{$create_url}">here</a>.{/blocktrans}</p>
|
||||||
|
{else}
|
||||||
|
<div class='issue-summary'>
|
||||||
|
{foreach $tagStatistics as $key => $class}
|
||||||
|
<div>
|
||||||
|
<h2>{blocktrans}Unresolved: By {$key}{/blocktrans}</h2>
|
||||||
|
<table class='issue-summary'>
|
||||||
|
<tbody>
|
||||||
|
{foreach $class as $key => $value}
|
||||||
|
<tr>
|
||||||
|
<td class="name"><a href="{url 'IDF_Views_Issue::listLabel', array($project.shortname, $value[2], 'open')}">{$key}</a></td>
|
||||||
|
<td class="count">{$value[0]}</td>
|
||||||
|
<td class="graph">
|
||||||
|
<table class='graph'>
|
||||||
|
<tbody><tr>
|
||||||
|
<td style="width:{$value[1] * 0.8 + 1}%" class="graph-color" valign="center">
|
||||||
|
<div class="colour-bar"></div>
|
||||||
|
</td>
|
||||||
|
<td class="graph-percent">{$value[1]}%</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/foreach}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='issue-summary'>
|
||||||
|
{if $status}
|
||||||
|
<div>
|
||||||
|
<h2>{blocktrans}Status Summary{/blocktrans}</h2>
|
||||||
|
<table class='issue-summary'>
|
||||||
|
<tbody>
|
||||||
|
{foreach $status as $key => $value}
|
||||||
|
<tr>
|
||||||
|
<td class="name"><a href="{url 'IDF_Views_Issue::listStatus', array($project.shortname, $key)}">{$key}</a></td>
|
||||||
|
<td class="count">{$value[0]}</td>
|
||||||
|
<td class="graph">
|
||||||
|
<table class='graph'>
|
||||||
|
<tbody><tr>
|
||||||
|
<td style="width:{$value[1] * 0.8 + 1}%" class="graph-color" valign="center">
|
||||||
|
<div class="colour-bar"></div>
|
||||||
|
</td>
|
||||||
|
<td class="graph-percent">{$value[1]}%</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/foreach}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{if $ownerStatistics}
|
||||||
|
<div>
|
||||||
|
<h2>{blocktrans}Unresolved: By Assignee{/blocktrans}</h2>
|
||||||
|
<table class='issue-summary'>
|
||||||
|
<tbody>
|
||||||
|
{foreach $ownerStatistics as $key => $value}
|
||||||
|
<tr>
|
||||||
|
<td class="name">{$key}</td>
|
||||||
|
<td class="count">{$value[0]}</td>
|
||||||
|
<td class="graph">
|
||||||
|
<table class='graph'>
|
||||||
|
<tbody><tr>
|
||||||
|
<td style="width:{$value[1] * 0.8 + 1}%" class="graph-color" valign="center">
|
||||||
|
<div class="colour-bar"></div>
|
||||||
|
</td>
|
||||||
|
<td class="graph-percent">{$value[1]}%</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/foreach}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/block}
|
90
test/IDF/ProjectTest.php
Normal file
90
test/IDF/ProjectTest.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||||
|
/*
|
||||||
|
# ***** BEGIN LICENSE BLOCK *****
|
||||||
|
# This file is part of InDefero, an open source project management application.
|
||||||
|
# Copyright (C) 2008-2011 Céondo Ltd and contributors.
|
||||||
|
#
|
||||||
|
# InDefero is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# InDefero is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
#
|
||||||
|
# ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
class IDF_ProjectTest extends PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
public function testGetIssueCountByOwner()
|
||||||
|
{
|
||||||
|
// Add users
|
||||||
|
$user1 = new Pluf_User();
|
||||||
|
$user1->login = 'user1';
|
||||||
|
$user1->create();
|
||||||
|
$user2 = new Pluf_User();
|
||||||
|
$user2->login = 'user2';
|
||||||
|
$user2->create();
|
||||||
|
|
||||||
|
// Add a project
|
||||||
|
$prj = new IDF_Project();
|
||||||
|
$prj->create();
|
||||||
|
$tag = $prj->getTagIdsByStatus('open');
|
||||||
|
|
||||||
|
// First test with no issue
|
||||||
|
$stats = $prj->getIssueCountByOwner();
|
||||||
|
$this->assertEquals($stats, array());
|
||||||
|
|
||||||
|
// Add some issues
|
||||||
|
$issue1 = new IDF_Issue();
|
||||||
|
$issue1->project = $prj;
|
||||||
|
$issue1->submitter = $user1;
|
||||||
|
$issue1->owner = $user1;
|
||||||
|
$issue1->status = new IDF_Tag($tag[0]);
|
||||||
|
$issue1->create();
|
||||||
|
|
||||||
|
$issue2 = new IDF_Issue();
|
||||||
|
$issue2->project = $prj;
|
||||||
|
$issue2->submitter = $user2;
|
||||||
|
$issue2->owner = $user1;
|
||||||
|
$issue2->status = new IDF_Tag($tag[0]);
|
||||||
|
$issue2->create();
|
||||||
|
|
||||||
|
$issue3 = new IDF_Issue();
|
||||||
|
$issue3->project = $prj;
|
||||||
|
$issue3->submitter = $user2;
|
||||||
|
$issue3->status = new IDF_Tag($tag[0]);
|
||||||
|
$issue3->create();
|
||||||
|
|
||||||
|
$issue4 = new IDF_Issue();
|
||||||
|
$issue4->project = $prj;
|
||||||
|
$issue4->submitter = $user2;
|
||||||
|
$issue4->owner = $user2;
|
||||||
|
$issue4->status = new IDF_Tag($tag[0]);
|
||||||
|
$issue4->create();
|
||||||
|
|
||||||
|
// 2nd test
|
||||||
|
$stats = $prj->getIssueCountByOwner();
|
||||||
|
$expected = array(0 => 1,
|
||||||
|
$user2->id => 1,
|
||||||
|
$user1->id => 2);
|
||||||
|
$this->assertEquals($stats, $expected);
|
||||||
|
|
||||||
|
// Clean DB
|
||||||
|
$issue4->delete();
|
||||||
|
$issue3->delete();
|
||||||
|
$issue2->delete();
|
||||||
|
$issue1->delete();
|
||||||
|
$prj->delete();
|
||||||
|
$user2->delete();
|
||||||
|
$user1->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1104,3 +1104,53 @@ div.p-list-private {
|
|||||||
right: -3px;
|
right: -3px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Issue summary
|
||||||
|
*/
|
||||||
|
div.issue-summary {
|
||||||
|
float: left;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.issue-summary > div {
|
||||||
|
margin-right: 3em;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.issue-summary h2 {
|
||||||
|
border-bottom: 1px solid #A5E26A;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.issue-summary {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.issue-summary tr td {
|
||||||
|
border: 0;
|
||||||
|
padding: .1em .005em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.issue-summary td.graph {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.issue-summary td.count {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.graph {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.issue-summary td.graph-color {
|
||||||
|
background: #3C78B5;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.issue-summary td.graph-percent {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user