Merge branch 'feature.issue-summary' into develop

feature.content-md5
William MARTIN 2011-06-20 11:37:26 +02:00
commit 9bbcd571ec
7 changed files with 356 additions and 1 deletions

View File

@ -132,6 +132,47 @@ class IDF_Project extends Pluf_Model
}
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.

View File

@ -77,6 +77,79 @@ class IDF_Views_Issue
$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.
* Limited to a specified project

View File

@ -117,6 +117,11 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'index');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/summary/$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'summary');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/search/$#',
'base' => $base,

View File

@ -2,7 +2,8 @@
{block tabissues} class="active"{/block}
{block subtabs}
<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>
| <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">

View 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}

View 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();
}
}

View File

@ -1104,3 +1104,53 @@ div.p-list-private {
right: -3px;
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;
}