diff options
-rw-r--r-- | Bugzilla/Search.pm | 4 | ||||
-rwxr-xr-x | checksetup.pl | 2 | ||||
-rw-r--r-- | globals.pl | 12 | ||||
-rwxr-xr-x | query.cgi | 4 | ||||
-rwxr-xr-x | report.cgi | 130 | ||||
-rw-r--r-- | t/004template.t | 1 | ||||
-rw-r--r-- | template/en/default/global/code-error.html.tmpl | 4 | ||||
-rw-r--r-- | template/en/default/reports/table.csv.tmpl | 41 | ||||
-rw-r--r-- | template/en/default/reports/table.html.tmpl | 143 | ||||
-rw-r--r-- | template/en/default/search/search-report-table.html.tmpl | 125 |
10 files changed, 465 insertions, 1 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index d6e7a9b7f..642965eb2 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -813,6 +813,10 @@ sub init { $suppseen{$str} = 1; } } + + # Make sure we create a legal SQL query. + @andlist = ("1 = 1") if !@andlist; + my $query = ("SELECT DISTINCT " . join(', ', @fields) . ", COUNT(DISTINCT ugmap.group_id) AS cntuseringroups, " . diff --git a/checksetup.pl b/checksetup.pl index b752f9b65..27bcf26f9 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -563,6 +563,7 @@ $contenttypes = { "rdf" => "application/xml" , "xml" => "text/xml" , "js" => "application/x-javascript" , + "csv" => "text/plain" , }; '); @@ -932,6 +933,7 @@ END js => sub { return $_; }, html_linebreak => sub { return $_; }, url_quote => sub { return $_; }, + csv => sub { return $_; }, }, }) || die ("Could not create Template: " . Template->error() . "\n"); diff --git a/globals.pl b/globals.pl index 242c8a5f6..64031bc85 100644 --- a/globals.pl +++ b/globals.pl @@ -1564,6 +1564,18 @@ $::template ||= Template->new( # filter should be used for a full URL that may have # characters that need encoding. url_quote => \&url_quote , + + # In CSV, quotes are doubled, and any value containing a quote or a + # comma is enclosed in quotes. + csv => sub + { + my ($var) = @_; + $var =~ s/"/""/; + if ($var =~ /",/) { + $var = "\"$var\""; + } + return $var; + } , } , } ) || die("Template creation failed: " . Template->error()); @@ -130,7 +130,9 @@ sub PrefillForm { "long_desc", "long_desc_type", "bug_file_loc", "bug_file_loc_type", "status_whiteboard", "status_whiteboard_type", "bug_id", - "bugidtype", "keywords", "keywords_type") { + "bugidtype", "keywords", "keywords_type", + "x_axis_field", "y_axis_field") + { # This is a bit of a hack. The default, empty list has # three entries to accommodate the needs of the email fields - # we use each position to denote the relevant field. Array diff --git a/report.cgi b/report.cgi new file mode 100755 index 000000000..525897771 --- /dev/null +++ b/report.cgi @@ -0,0 +1,130 @@ +#!/usr/bonsaitools/bin/perl -wT +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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> +# <rdean@cambianetworks.com> + +use diagnostics; +use strict; +use lib "."; + +require "CGI.pl"; + +use vars qw($template $vars); + +use Bugzilla::Search; + +ConnectToDatabase(); + +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'}. + +$::FORM{'y_axis_field'} || ThrowCodeError("no_y_axis_defined"); + +my $col_field = $::FORM{'x_axis_field'}; +my $row_field = $::FORM{'y_axis_field'}; + +my %columns; +$columns{'bug_severity'} = "bugs.bug_severity"; +$columns{'priority'} = "bugs.priority"; +$columns{'rep_platform'} = "bugs.rep_platform"; +$columns{'assigned_to'} = "map_assigned_to.login_name"; +$columns{'reporter'} = "map_reporter.login_name"; +$columns{'qa_contact'} = "map_qa_contact.login_name"; +$columns{'bug_status'} = "bugs.bug_status"; +$columns{'resolution'} = "bugs.resolution"; +$columns{'component'} = "map_components.name"; +$columns{'product'} = "map_products.name"; +$columns{'version'} = "bugs.version"; +$columns{'op_sys'} = "bugs.op_sys"; +$columns{'votes'} = "bugs.votes"; +$columns{'keywords'} = "bugs.keywords"; +$columns{'target_milestone'} = "bugs.target_milestone"; + +my @axis_fields = ($row_field); +# The X axis (horizontal) is optional +push(@axis_fields, $col_field) if $col_field; + +my @selectnames = map($columns{$_}, @axis_fields); + +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. +my %data; +my %row_totals; +my %col_totals; +my $grand_total; + +# Read the bug data and increment the counts. +while (MoreSQLData()) { + my ($row, $col) = FetchSQLData(); + $row = "" if !defined($row); + $col = "" if !defined($col); + + $data{$row}{$col}++; + $row_totals{$row}++; + $col_totals{$col}++; + $grand_total++; +} + +$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; + +$::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->{'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'}); +print "Content-Type: $format->{'contenttype'}\n\n"; +$template->process("$format->{'template'}", $vars) + || ThrowTemplateError($template->error()); diff --git a/t/004template.t b/t/004template.t index 41d515435..02541d351 100644 --- a/t/004template.t +++ b/t/004template.t @@ -81,6 +81,7 @@ my $template = Template->new( js => sub { return $_ } , strike => sub { return $_ } , url_quote => sub { return $_ } , + csv => sub { return $_ } , }, } ); diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl index 55fac2aca..bf93977ad 100644 --- a/template/en/default/global/code-error.html.tmpl +++ b/template/en/default/global/code-error.html.tmpl @@ -78,6 +78,10 @@ [% ELSIF error == "no_bug_data" %] No data when fetching bug [% bug_id %]. + [% ELSIF error == "no_y_axis_defined" %] + No Y axis was defined when creating report. The X axis is optional, + but the Y axis is compulsory. + [% ELSIF error == "template_error" %] [% template_error_msg %] diff --git a/template/en/default/reports/table.csv.tmpl b/template/en/default/reports/table.csv.tmpl new file mode 100644 index 000000000..96b0c3971 --- /dev/null +++ b/template/en/default/reports/table.csv.tmpl @@ -0,0 +1,41 @@ +[%# 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: + # See report.html.tmpl. + #%] +[% 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 row = row_names %] +[% row FILTER csv -%], +[% FOREACH col = col_names %] +[% IF data.$row AND data.$row.$col %][% data.$row.$col -%],[% ELSE %]0,[% END %] +[% END %] + +[% END %] diff --git a/template/en/default/reports/table.html.tmpl b/template/en/default/reports/table.html.tmpl new file mode 100644 index 000000000..d9a04d5fd --- /dev/null +++ b/template/en/default/reports/table.html.tmpl @@ -0,0 +1,143 @@ + <!-- 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> + # <rdean@cambianetworks.com> + #%] + +[%# 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. + #%] + +[% 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 } + " +%] + +<div align="center"> + <table> + <tr> + <td> + </td> + <td align="center"> + <strong>[% col_field FILTER html %]</strong> + </td> + </tr> + + <tr> + <td valign="middle"> + <strong>[% row_field FILTER html %]</strong> + </td> + <td> + + +[% classes = [ [ "t1", "t2" ] , [ "t3", "t4" ] ] %] +[% col_idx = 0 %] +[% row_idx = 0 %] + +<table> + [% IF col_names %] + <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_idx = 1 - col_idx %] + <td class="[% classes.$row_idx.$col_idx %]"> + [% col FILTER html %] + </td> + [% END %] + <td class="ttotal"> + Total + </td> + </tr> + [% END %] + + [% FOREACH row = row_names %] + [% row_idx = 1 - row_idx %] + <tr> + <td class="[% classes.$row_idx.$col_idx %]"> + [% row FILTER html %] + </td> + [% FOREACH col = col_names %] + [% 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 %] + <a href="buglist.cgi?[% buglistbase %]& + [% row_field FILTER url_quote %]=[% row FILTER url_quote %]& + [% col_field FILTER url_quote %]=[% col FILTER url_quote %]"> + [% data.$row.$col %]</a> + [% ELSE %] + . + [% END %] + </td> + [% END %] + <td class="ttotal"> + [% row_totals.$row %] + </td> + </tr> + [% END %] + + <tr> + [% row_idx = 1 - row_idx %] + <td class="ttotal"> + Total + </td> + [% FOREACH col = col_names %] + [% NEXT IF col == "" %] + <td class="ttotal" align="center"> + [% col_totals.$col %] + </td> + [% END %] + <td class="ttotal"> + <strong> + [% grand_total %] + </strong> + </td> + </tr> +</table> + + + </td> + </tr> + </table> + + <a href="query.cgi?[% buffer %]&format=report-table">Edit this report</a> +</div> + +<br> + +[% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/search/search-report-table.html.tmpl b/template/en/default/search/search-report-table.html.tmpl new file mode 100644 index 000000000..32f816135 --- /dev/null +++ b/template/en/default/search/search-report-table.html.tmpl @@ -0,0 +1,125 @@ +<!-- 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. However, to use it, you need to fulfill + # the interfaces of the templates it contains. + #%] + +[% PROCESS global/header.html.tmpl + title = "Generate Report" + onload = "selectProduct(document.forms['reportform']);" +%] + +<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. +</p> + +[% button_name = "Generate Report" %] + +<form method="get" action="report.cgi" name="reportform"> + +<table> + <tr> + <th align="center"> + Vertical Axis + </th> + <th align="center"> + Horizontal Axis + </th> + <th> + + </th> + <th align="center"> + Format + </th> + </tr> + + <tr> + <td align="center"> + [% PROCESS select sel = { name => 'y_axis_field' } %] + </td> + <td align="center"> + [% PROCESS select sel = { name => 'x_axis_field', noop = 1 } %] + </td> + <td> + + </td> + <td> + <input type="radio" name="ctype" value="html" checked>HTML + <input type="radio" name="ctype" value="csv">CSV + </td> + </tr> +</table> + +<hr> + +[% PROCESS search/form.html.tmpl %] + +<br> +<input type="submit" value="[% button_name %]"> +<input type="hidden" name="type" value="table"> +<hr> + +[% PROCESS "search/boolean-charts.html.tmpl" %] + +</form> + +[% PROCESS global/footer.html.tmpl %] + +[%############################################################################%] +[%# Block for SELECT fields #%] +[%############################################################################%] + +[% 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" } ] %] + + <select name="[% sel.name %]"> + [% 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') %] + + <option value="[% field.name FILTER html %]" + [% " selected" IF default.${sel.name}.0 == field.name %]> + [% field.description FILTER html %]</option> + [% END %] + </select> +[% END %] |