Commit f1ddf54f authored by gerv%gerv.net's avatar gerv%gerv.net

Bug 171437 - Enhancements to generic reporting. Reporting menu, 3D tables,…

Bug 171437 - Enhancements to generic reporting. Reporting menu, 3D tables, rearranged UI, better API for new report types. Patch by gerv; r=joel.
parent 5cdbfa01
......@@ -199,6 +199,35 @@ sub ProcessMultipartFormFields {
}
}
sub CanonicaliseParams {
my ($buffer, $exclude) = (@_);
my %pieces;
# Split the buffer up into key/value pairs, and store the non-empty ones
my @args = split('&', $buffer);
foreach my $arg (@args) {
my ($name, $value) = split('=', $arg, 2);
if ($value) {
push(@{$pieces{$name}}, $value);
}
}
# Reconstruct the URL by concatenating the sorted param=value pairs
my @parameters;
foreach my $key (sort keys %pieces) {
# Leave this key out if it's in the exclude list
next if lsearch($exclude, $key) != -1;
foreach my $value (@{$pieces{$key}}) {
push(@parameters, "$key=$value");
}
}
return join("&", @parameters);
}
# check and see if a given field exists, is non-empty, and is set to a
# legal value. assume a browser bug and abort appropriately if not.
# if $legalsRef is not passed, just check to make sure the value exists and
......
......@@ -131,7 +131,7 @@ sub PrefillForm {
"bug_file_loc_type", "status_whiteboard",
"status_whiteboard_type", "bug_id",
"bugidtype", "keywords", "keywords_type",
"x_axis_field", "y_axis_field")
"x_axis_field", "y_axis_field", "z_axis_field")
{
# This is a bit of a hack. The default, empty list has
# three entries to accommodate the needs of the email fields -
......
......@@ -37,13 +37,22 @@ GetVersionTable();
quietly_check_login();
# If other report types are added, some of this code can be moved into a sub,
# and the correct sub chosen based on $::FORM{'type'}.
if ($::FORM{'action'} ne "plot") {
print "Content-Type: text/html\n\n";
$template->process("reports/menu.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
}
$::FORM{'y_axis_field'} || ThrowCodeError("no_y_axis_defined");
if ($::FORM{'z_axis_field'} && !$::FORM{'x_axis_field'}) {
ThrowUserError("z_axis_defined_with_no_x_axis");
}
my $col_field = $::FORM{'x_axis_field'};
my $row_field = $::FORM{'y_axis_field'};
my $tbl_field = $::FORM{'z_axis_field'};
my %columns;
$columns{'bug_severity'} = "bugs.bug_severity";
......@@ -61,10 +70,11 @@ $columns{'op_sys'} = "bugs.op_sys";
$columns{'votes'} = "bugs.votes";
$columns{'keywords'} = "bugs.keywords";
$columns{'target_milestone'} = "bugs.target_milestone";
# One which means "nothing". Any number would do, really. It just gets SELECTed
# so that we always select 3 items in the query.
$columns{''} = "42217354";
my @axis_fields = ($row_field);
# The X axis (horizontal) is optional
push(@axis_fields, $col_field) if $col_field;
my @axis_fields = ($row_field, $col_field, $tbl_field);
my @selectnames = map($columns{$_}, @axis_fields);
......@@ -72,59 +82,42 @@ my $search = new Bugzilla::Search('fields' => \@selectnames,
'url' => $::buffer);
my $query = $search->getSQL();
$query =~ s/DISTINCT//;
SendSQL($query, $::userid);
# We have a hash for each direction for the totals, and a hash of hashes for
# the data itself.
# We have a hash of hashes for the data itself, and a hash to hold the
# row/col/table names.
my %data;
my %row_totals;
my %col_totals;
my $grand_total;
my %names;
# Read the bug data and increment the counts.
while (MoreSQLData()) {
my ($row, $col) = FetchSQLData();
$row = "" if !defined($row);
$col = "" if !defined($col);
my ($row, $col, $tbl) = FetchSQLData();
$col = "" if ($col == $columns{''});
$tbl = "" if ($tbl == $columns{''});
$data{$row}{$col}++;
$row_totals{$row}++;
$col_totals{$col}++;
$grand_total++;
$data{$tbl}{$col}{$row}++;
$names{"col"}{$col}++;
$names{"row"}{$row}++;
$names{"tbl"}{$tbl}++;
}
$vars->{'data'} = \%data;
$vars->{'row_totals'} = \%row_totals;
$vars->{'col_totals'} = \%col_totals;
$vars->{'grand_total'} = $grand_total;
# Determine the labels for the rows and columns
my @row_names = sort(keys(%row_totals));
my @col_names = sort(keys(%col_totals));
$vars->{'row_names'} = \@row_names;
$vars->{'col_names'} = \@col_names;
$vars->{'row_field'} = $row_field;
$vars->{'col_field'} = $col_field;
$vars->{'row_field'} = $row_field;
$vars->{'tbl_field'} = $tbl_field;
$vars->{'names'} = \%names;
$vars->{'data'} = \%data;
$vars->{'time'} = time();
$::buffer =~ s/format=[^&]*&?//g;
# Calculate the base query URL for the hyperlinked numbers
my $buglistbase = $::buffer;
$buglistbase =~ s/$row_field=[^&]*&?//g;
$buglistbase =~ s/$col_field=[^&]*&?//g;
$vars->{'buglistbase'} = $buglistbase;
$vars->{'buglistbase'} = CanonicaliseParams($::buffer,
["x_axis_field", "y_axis_field", "z_axis_field", @axis_fields]);
$vars->{'buffer'} = $::buffer;
$::FORM{'type'} =~ s/[^a-zA-Z\-]//g;
# Generate and return the result from the appropriate template.
my $format = GetFormat("reports/$::FORM{'type'}",
$::FORM{'format'},
$::FORM{'ctype'});
my $format = GetFormat("reports/report", $::FORM{'format'}, $::FORM{'ctype'});
print "Content-Type: $format->{'contenttype'}\n\n";
$template->process("$format->{'template'}", $vars)
|| ThrowTemplateError($template->error());
......@@ -49,7 +49,7 @@
<input type="submit" value="Find"> bug #
<input name="id" size="6"> |
<a href="reports.cgi">Reports</a>
<a href="report.cgi">Reports</a>
| <a href="request.cgi">Requests</a>
......
......@@ -521,6 +521,11 @@
[% title = "Wrong Token" %]
That token cannot be used to change your email address.
[% ELSIF error == "z_axis_defined_with_no_x_axis" %]
[% title = "Nonsensical Options" %]
You've defined a field for multiple tables without having defined
a horizontal axis for those tables.
[% ELSIF error == "zero_length_file" %]
[% title = "File Is Empty" %]
The file you are trying to attach is empty!
......
<!-- 1.0@bugzilla.org -->
[%# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Gervase Markham <gerv@gerv.net>
#%]
[%# INTERFACE:
# This template has no interface. It's a list of the available report
# types in Bugzilla.
#%]
[% PROCESS global/header.html.tmpl
title = "Reporting and Charting Kitchen"
%]
<p>
Bugzilla allows you to view and track the state of your bug database in
all manner of exciting ways.
</p>
<h2>Current State</h2>
<ul>
<li>
<strong><a href="query.cgi">Search</a></strong> -
list sets of bugs.
</li>
<li>
<strong>
<a href="query.cgi?format=report-table">Tabular reports</a>
</strong> -
tables of bug counts in 1, 2 or 3 dimensions, as HTML or CSV.
</li>
</ul>
<h2>Change Over Time</h2>
<ul>
<li>
<strong><a href="reports.cgi">Charts</a></strong> -
plot the status and/or resolution of bugs against
time, for each product in your database.
</li>
</ul>
[% PROCESS global/footer.html.tmpl %]
......@@ -21,21 +21,37 @@
[%# INTERFACE:
# See report.html.tmpl.
#%]
[% row_field FILTER csv -%]
[% tbl_names = names.tbl.keys.sort %]
[% col_names = names.col.keys.sort %]
[% row_names = names.row.keys.sort %]
[% IF col_field -%]
[% FOREACH tbl = tbl_names %]
[% IF tbl_field -%]
[% tbl FILTER html %]
[% END %]
[% row_field FILTER csv -%]
[% IF col_field -%]
\ [% col_field FILTER csv -%],
[% FOREACH col = col_names -%]
[% col FILTER csv -%],
[% END -%]
[% ELSE -%]
,Number of bugs,
[% END %]
[% FOREACH col = col_names -%]
[% col FILTER csv -%],
[% END -%]
[% ELSE -%]
[% -%],Number of bugs
[% END %]
[% FOREACH row = row_names %]
[% row FILTER csv -%],
[% FOREACH col = col_names %]
[% IF data.$row AND data.$row.$col %][% data.$row.$col -%],[% ELSE %]0,[% END %]
[% END %]
[% FOREACH row = row_names %]
[% row FILTER csv -%],
[% FOREACH col = col_names %]
[% IF data.$tbl AND data.$tbl.$col AND data.$tbl.$col.$row %]
[% data.$tbl.$col.$row -%],
[% ELSE %]
[% -%]0,
[% END %]
[% END %]
[% END %]
[% END %]
......@@ -22,41 +22,85 @@
[%# INTERFACE:
# basequery: The base query for this table, in URL form
# row_field: string. The field name for the data in table rows
# col_field: string. The field name for the data in table columns
# col_names: array of strings. Values for the columns
# row_names: array of strings. Values for the rows
# col_totals: hash of integers. Totals for the columns, indexed by col_names.
# row_totals: hash of integers. Totals for the rows, indexed by row_names.
# data: hash of hash of numbers. Bug counts indexed by col_names and
# row_names values.
# data: hash of hash of hash of numbers. Bug counts.
# names: hash of hash of strings. Names of tables, rows and columns.
# col_field: string. Name of the field being plotted as columns.
# row_field: string. Name of the field being plotted as rows.
# tbl_field: string. Name of the field being plotted as tables.
#%]
[% PROCESS "global/field-descs.html.tmpl" %]
[% tbl_field_disp = field_descs.$tbl_field || tbl_field %]
[% col_field_disp = field_descs.$col_field || col_field %]
[% row_field_disp = field_descs.$row_field || row_field %]
[% title = BLOCK %]
Report:
[% "$tbl_field_disp / " IF tbl_field %]
[% "$col_field_disp / " IF col_field %]
[% row_field_disp %]
[% END %]
[% PROCESS global/header.html.tmpl
title = "Report"
onload = "selectProduct(document.forms['queryform']);"
style = "
.t1 { background-color: #ffffff }
.t2 { background-color: #dfefff }
.t3 { background-color: #dddddd }
.t4 { background-color: #c3d3ed }
.ttotal { background-color: #cfffdf }
.t1 { background-color: #ffffff } /* white */
.t2 { background-color: #dfefff } /* light blue */
.t3 { background-color: #dddddd } /* grey */
.t4 { background-color: #c3d3ed } /* darker blue */
.ttotal { background-color: #cfffdf } /* light green */
"
%]
<div align="right">
[% time2str("%Y-%m-%d %H:%M:%S", time) %]
</div>
[% tbl_names = names.tbl.keys.sort %]
[% col_names = names.col.keys.sort %]
[% row_names = names.row.keys.sort %]
[% total_name = "Total" %]
[% IF tbl_field %]
[%# Calculate and set up the Total table %]
[% FOREACH tbl = tbl_names %]
[% FOREACH row = row_names %]
[% FOREACH col = col_names %]
[% data.$total_name.$col.$row =
data.$total_name.$col.$row + data.$tbl.$col.$row %]
[% END %]
[% END %]
[% END %]
[% tbl_names.push(total_name) %]
[% END %]
<div align="center">
[% FOREACH tbl = tbl_names %]
<table>
[% IF tbl_field %]
<tr>
<td>
</td>
<td align="center">
<h2>[% tbl FILTER html %]</h2>
</td>
</tr>
[% END %]
<tr>
<td>
</td>
<td align="center">
<strong>[% col_field FILTER html %]</strong>
<strong>[% col_field_disp FILTER html %]</strong>
</td>
</tr>
<tr>
<td valign="middle">
<strong>[% row_field FILTER html %]</strong>
<strong>[% row_field_disp FILTER html %]</strong>
</td>
<td>
......@@ -64,16 +108,17 @@
[% classes = [ [ "t1", "t2" ] , [ "t3", "t4" ] ] %]
[% col_idx = 0 %]
[% row_idx = 0 %]
[% grand_total = 0 %]
<table>
[% IF col_names %]
<table border="1">
[% IF col_field %]
<tr>
<td class="[% classes.$row_idx.$col_idx %]">
</td>
[% FOREACH col = col_names %]
[%# If no col header, skip the col. This makes display look right if
there's no defined X axis. Not doing this gives us two cols. %]
[% NEXT IF col == "" %]
[% col_totals.$col = 0 %]
[% NEXT IF col == "" %]
[% col_idx = 1 - col_idx %]
<td class="[% classes.$row_idx.$col_idx %]">
[% col FILTER html %]
......@@ -86,27 +131,37 @@
[% END %]
[% FOREACH row = row_names %]
[% row_total = 0 %]
[% row_idx = 1 - row_idx %]
<tr>
<td class="[% classes.$row_idx.$col_idx %]">
<td class="[% classes.$row_idx.$col_idx %]" align="right">
[% row FILTER html %]
</td>
[% FOREACH col = col_names %]
[% row_total = row_total + data.$tbl.$col.$row %]
[% col_totals.$col = col_totals.$col + data.$tbl.$col.$row %]
[% NEXT IF col == "" %]
[% col_idx = 1 - col_idx %]
<td class="[% classes.$row_idx.$col_idx %]" align="center">
[% IF data.$row.$col AND data.$row.$col > 0 %]
[% IF data.$tbl.$col.$row AND data.$tbl.$col.$row > 0 %]
<a href="buglist.cgi?[% buglistbase %]&
[% row_field FILTER url_quote %]=[% row FILTER url_quote %]&
[% tbl_field FILTER url_quote %]=[% tbl FILTER url_quote %]&amp;
[% row_field FILTER url_quote %]=[% row FILTER url_quote %]&amp;
[% col_field FILTER url_quote %]=[% col FILTER url_quote %]">
[% data.$row.$col %]</a>
[% data.$tbl.$col.$row %]</a>
[% ELSE %]
.
[% END %]
</td>
[% END %]
<td class="ttotal">
[% row_totals.$row %]
<td class="ttotal" align="right">
<a href="buglist.cgi?[% buglistbase %]&
[% tbl_field FILTER url_quote %]=[% tbl FILTER url_quote %]&amp;
[% row_field FILTER url_quote %]=[% row FILTER url_quote %]">
[% row_total %]</a>
[% grand_total = grand_total + row_total %]
</td>
</tr>
[% END %]
......@@ -118,13 +173,18 @@
</td>
[% FOREACH col = col_names %]
[% NEXT IF col == "" %]
<td class="ttotal" align="center">
[% col_totals.$col %]
<a href="buglist.cgi?[% buglistbase %]&
[% tbl_field FILTER url_quote %]=[% tbl FILTER url_quote %]&amp;
[% col_field FILTER url_quote %]=[% col FILTER url_quote %]">
[% col_totals.$col %]</a>
<strong>
</td>
[% END %]
<td class="ttotal">
<td class="ttotal" align="right">
<strong>
[% grand_total %]
<a href="buglist.cgi?[% buglistbase %]">[% grand_total %]</a>
</strong>
</td>
</tr>
......@@ -134,6 +194,10 @@
</td>
</tr>
</table>
<br>
[% END %]
<a href="query.cgi?[% buffer %]&format=report-table">Edit this report</a>
</div>
......
......@@ -29,47 +29,51 @@
onload = "selectProduct(document.forms['reportform']);"
%]
[% PROCESS "global/field-descs.html.tmpl" %]
<p>
Produce a table of bug counts by choosing two fields to plot against each
other, and then refining your set of bugs using the rest of the form.
Produce a table of bug counts by choosing one or more fields to plot against
each other, and then refining your set of bugs using the rest of the form. If
you choose a third axis, it will be represented by multiple tables of data.
</p>
[% button_name = "Generate Report" %]
<form method="get" action="report.cgi" name="reportform">
<table>
<table align="center">
<tr>
<th align="center">
Vertical Axis
</th>
<th align="center">
Horizontal Axis
</th>
<th>
&nbsp;&nbsp;
</th>
<th align="center">
Format
</th>
<td>
</td>
<td align="center">
<b>Horizontal Axis:</b>
[% PROCESS select sel = { name => 'x_axis_field', noop = 1 } %]
</td>
<td>&nbsp;&nbsp;</td>
<td rowspan="2">
<b>Format:</b><br>
<input type="radio" name="ctype" value="html" checked>HTML<br>
<input type="radio" name="ctype" value="csv">CSV
</td>
</tr>
<tr>
<td align="center">
<td valign="middle" align="center">
<b>Vertical Axis:</b><br>
[% PROCESS select sel = { name => 'y_axis_field' } %]
</td>
<td align="center">
[% PROCESS select sel = { name => 'x_axis_field', noop = 1 } %]
</td>
<td>
&nbsp;&nbsp;
</td>
<td>
<input type="radio" name="ctype" value="html" checked>HTML
<input type="radio" name="ctype" value="csv">CSV
<td width="150px" height="150px">
<table border="1" width="100%" height="100%">
<tr>
<td align="center" valign="middle">
<b>Multiple Tables:</b><br>
[% PROCESS select sel = { name => 'z_axis_field', noop = 1 } %]
</td>
</tr>
</table>
</td>
</tr>
</table>
</table>
<hr>
......@@ -77,7 +81,8 @@
<br>
<input type="submit" value="[% button_name %]">
<input type="hidden" name="type" value="table">
<input type="hidden" name="format" value="table">
<input type="hidden" name="action" value="plot">
<hr>
[% PROCESS "search/boolean-charts.html.tmpl" %]
......@@ -91,35 +96,24 @@
[%############################################################################%]
[% BLOCK select %]
[% fields = [
{ name => "", description => "---" },
{ name => "product", description => "Product" },
{ name => "component", description => "Component" },
{ name => "version", description => "Version" },
{ name => "rep_platform", description => "Platform" },
{ name => "op_sys", description => "OS" },
{ name => "bug_status", description => "Status" },
{ name => "resolution", description => "Resolution" },
{ name => "bug_severity", description => "Severity" },
{ name => "priority", description => "Priority" },
{ name => "target_milestone", description => "Target Milestone" },
{ name => "keywords", description => "Keywords" },
{ name => "assigned_to", description => "Assignee" },
{ name => "reporter", description => "Reporter" },
{ name => "qa_contact", description => "QA Contact" },
{ name => "votes", description => "Votes" } ] %]
[% fields = ["product", "component", "version", "rep_platform",
"op_sys", "bug_status", "resolution", "bug_severity",
"priority", "target_milestone", "keywords", "assigned_to",
"reporter", "qa_contact", "votes" ] %]
<select name="[% sel.name %]">
[% IF sel.noop %]
<option value="">&lt;none&gt;</option>
[% END %]
[% FOREACH field = fields %]
[% NEXT IF field.name == "" AND !sel.noop %]
[% NEXT IF field.name == "target_milestone" AND
!Param('usetargetmilestone') %]
[% NEXT IF field.name == "qa_contact" AND !Param('useqacontact') %]
[% NEXT IF field.name == "votes" AND !Param('usevotes') %]
[% NEXT IF field == "target_milestone" AND !Param('usetargetmilestone') %]
[% NEXT IF field == "qa_contact" AND !Param('useqacontact') %]
[% NEXT IF field == "votes" AND !Param('usevotes') %]
<option value="[% field.name FILTER html %]"
[% " selected" IF default.${sel.name}.0 == field.name %]>
[% field.description FILTER html %]</option>
<option value="[% field FILTER html %]"
[% " selected" IF default.${sel.name}.0 == field %]>
[% field_descs.$field || field FILTER html %]</option>
[% END %]
</select>
[% END %]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment