summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgerv%gerv.net <>2003-06-26 08:22:50 +0200
committergerv%gerv.net <>2003-06-26 08:22:50 +0200
commit4f6b75a65628b0d86c760309dd81dd03f5c6d308 (patch)
tree5363459b06e75bc620ceab9dfd045b0f66b6c27c
parentda6143f4aae6af35f60b8230b82f649b3b0cbd05 (diff)
downloadbugzilla-4f6b75a65628b0d86c760309dd81dd03f5c6d308.tar.gz
bugzilla-4f6b75a65628b0d86c760309dd81dd03f5c6d308.tar.xz
Bug 16009 - generic charting. Patch by gerv; r,a=justdave.
-rw-r--r--Bugzilla/Chart.pm351
-rw-r--r--Bugzilla/Series.pm262
-rw-r--r--Bugzilla/Template.pm7
-rwxr-xr-xbuglist.cgi18
-rwxr-xr-xchart.cgi312
-rwxr-xr-xchecksetup.pl142
-rwxr-xr-xcollectstats.pl73
-rwxr-xr-xeditcomponents.cgi30
-rwxr-xr-xeditproducts.cgi39
-rwxr-xr-xquery.cgi9
-rw-r--r--template/en/default/filterexceptions.pl31
-rw-r--r--template/en/default/global/code-error.html.tmpl11
-rw-r--r--template/en/default/global/messages.html.tmpl23
-rw-r--r--template/en/default/global/user-error.html.tmpl42
-rw-r--r--template/en/default/reports/chart.csv.tmpl40
-rw-r--r--template/en/default/reports/chart.html.tmpl66
-rw-r--r--template/en/default/reports/chart.png.tmpl56
-rw-r--r--template/en/default/reports/create-chart.html.tmpl281
-rw-r--r--template/en/default/reports/edit-series.html.tmpl57
-rw-r--r--template/en/default/reports/menu.html.tmpl6
-rw-r--r--template/en/default/reports/series-common.html.tmpl117
-rw-r--r--template/en/default/reports/series.html.tmpl96
-rw-r--r--template/en/default/search/search-create-series.html.tmpl67
23 files changed, 2130 insertions, 6 deletions
diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm
new file mode 100644
index 000000000..03b5e4173
--- /dev/null
+++ b/Bugzilla/Chart.pm
@@ -0,0 +1,351 @@
+# -*- 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>
+
+use strict;
+use lib ".";
+
+# This module represents a chart.
+#
+# Note that it is perfectly legal for the 'lines' member variable of this
+# class (which is an array of Bugzilla::Series objects) to have empty members
+# in it. If this is true, the 'labels' array will also have empty members at
+# the same points.
+package Bugzilla::Chart;
+
+use Bugzilla::Util;
+use Bugzilla::Series;
+
+sub new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+
+ # Create a ref to an empty hash and bless it
+ my $self = {};
+ bless($self, $class);
+
+ if ($#_ == 0) {
+ # Construct from a CGI object.
+ $self->init($_[0]);
+ }
+ else {
+ die("CGI object not passed in - invalid number of args \($#_\)($_)");
+ }
+
+ return $self;
+}
+
+sub init {
+ my $self = shift;
+ my $cgi = shift;
+
+ # The data structure is a list of lists (lines) of Series objects.
+ # There is a separate list for the labels.
+ #
+ # The URL encoding is:
+ # line0=67&line0=73&line1=81&line2=67...
+ # &label0=B+/+R+/+NEW&label1=...
+ # &select0=1&select3=1...
+ # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html...
+ # &gt=1&labelgt=Grand+Total
+ foreach my $param ($cgi->param()) {
+ # Store all the lines
+ if ($param =~ /^line(\d+)$/) {
+ foreach my $series_id ($cgi->param($param)) {
+ detaint_natural($series_id)
+ || &::ThrowCodeError("invalid_series_id");
+ push(@{$self->{'lines'}[$1]},
+ new Bugzilla::Series($series_id));
+ }
+ }
+
+ # Store all the labels
+ if ($param =~ /^label(\d+)$/) {
+ $self->{'labels'}[$1] = $cgi->param($param);
+ }
+ }
+
+ # Store the miscellaneous metadata
+ $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0;
+ $self->{'gt'} = $cgi->param('gt') ? 1 : 0;
+ $self->{'labelgt'} = $cgi->param('labelgt');
+ $self->{'datefrom'} = $cgi->param('datefrom');
+ $self->{'dateto'} = $cgi->param('dateto');
+
+ # Make sure the dates are ones we are able to interpret
+ foreach my $date ('datefrom', 'dateto') {
+ if ($self->{$date}) {
+ $self->{$date} = &::str2time($self->{$date})
+ || ThrowUserError("illegal_date", { date => $self->{$date}});
+ }
+ }
+
+ # datefrom can't be after dateto
+ if ($self->{'datefrom'} && $self->{'dateto'} &&
+ $self->{'datefrom'} > $self->{'dateto'})
+ {
+ &::ThrowUserError("misarranged_dates",
+ {'datefrom' => $cgi->param('datefrom'),
+ 'dateto' => $cgi->param('dateto')});
+ }
+}
+
+# Alter Chart so that the selected series are added to it.
+sub add {
+ my $self = shift;
+ my @series_ids = @_;
+
+ # If we are going from < 2 to >= 2 series, add the Grand Total line.
+ if (!$self->{'gt'}) {
+ my $current_size = scalar($self->getSeriesIDs());
+ if ($current_size < 2 &&
+ $current_size + scalar(@series_ids) >= 2)
+ {
+ $self->{'gt'} = 1;
+ }
+ }
+
+ # Create new Series and push them on to the list of lines.
+ # Note that new lines have no label; the display template is responsible
+ # for inventing something sensible.
+ foreach my $series_id (@series_ids) {
+ my $series = new Bugzilla::Series($series_id);
+ push(@{$self->{'lines'}}, [$series]);
+ push(@{$self->{'labels'}}, "");
+ }
+}
+
+# Alter Chart so that the selections are removed from it.
+sub remove {
+ my $self = shift;
+ my @line_ids = @_;
+
+ foreach my $line_id (@line_ids) {
+ if ($line_id == 65536) {
+ # Magic value - delete Grand Total.
+ $self->{'gt'} = 0;
+ }
+ else {
+ delete($self->{'lines'}->[$line_id]);
+ delete($self->{'labels'}->[$line_id]);
+ }
+ }
+}
+
+# Alter Chart so that the selections are summed.
+sub sum {
+ my $self = shift;
+ my @line_ids = @_;
+
+ # We can't add the Grand Total to things.
+ @line_ids = grep(!/^65536$/, @line_ids);
+
+ # We can't add less than two things.
+ return if scalar(@line_ids) < 2;
+
+ my @series;
+ my $label = "";
+ my $biggestlength = 0;
+
+ # We rescue the Series objects of all the series involved in the sum.
+ foreach my $line_id (@line_ids) {
+ my @line = @{$self->{'lines'}->[$line_id]};
+
+ foreach my $series (@line) {
+ push(@series, $series);
+ }
+
+ # We keep the label that labels the line with the most series.
+ if (scalar(@line) > $biggestlength) {
+ $biggestlength = scalar(@line);
+ $label = $self->{'labels'}->[$line_id];
+ }
+ }
+
+ $self->remove(@line_ids);
+
+ push(@{$self->{'lines'}}, \@series);
+ push(@{$self->{'labels'}}, $label);
+}
+
+sub data {
+ my $self = shift;
+ $self->{'_data'} ||= $self->readData();
+ return $self->{'_data'};
+}
+
+# Convert the Chart's data into a plottable form in $self->{'_data'}.
+sub readData {
+ my $self = shift;
+ my @data;
+
+ my $series_ids = join(",", $self->getSeriesIDs());
+
+ # Work out the date boundaries for our data.
+ my $dbh = Bugzilla->dbh;
+
+ # The date used is the one given if it's in a sensible range; otherwise,
+ # it's the earliest or latest date in the database as appropriate.
+ my $datefrom = $dbh->selectrow_array("SELECT MIN(date) FROM series_data " .
+ "WHERE series_id IN ($series_ids)");
+ $datefrom = &::str2time($datefrom);
+
+ if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) {
+ $datefrom = $self->{'datefrom'};
+ }
+
+ my $dateto = $dbh->selectrow_array("SELECT MAX(date) FROM series_data " .
+ "WHERE series_id IN ($series_ids)");
+ $dateto = &::str2time($dateto);
+
+ if ($self->{'dateto'} && $self->{'dateto'} < $dateto) {
+ $dateto = $self->{'dateto'};
+ }
+
+ # Prepare the query which retrieves the data for each series
+ my $query = "SELECT TO_DAYS(date) - TO_DAYS(FROM_UNIXTIME($datefrom)), " .
+ "value FROM series_data " .
+ "WHERE series_id = ? " .
+ "AND date >= FROM_UNIXTIME($datefrom)";
+ if ($dateto) {
+ $query .= " AND date <= FROM_UNIXTIME($dateto)";
+ }
+
+ my $sth = $dbh->prepare($query);
+
+ my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef;
+ my $line_index = 0;
+
+ foreach my $line (@{$self->{'lines'}}) {
+ # Even if we end up with no data, we need an empty arrayref to prevent
+ # errors in the PNG-generating code
+ $data[$line_index] = [];
+
+ foreach my $series (@$line) {
+
+ # Get the data for this series and add it on
+ $sth->execute($series->{'series_id'});
+ my $points = $sth->fetchall_arrayref();
+
+ foreach my $point (@$points) {
+ my ($datediff, $value) = @$point;
+ $data[$line_index][$datediff] ||= 0;
+ $data[$line_index][$datediff] += $value;
+
+ # Add to the grand total, if we are doing that
+ if ($gt_index) {
+ $data[$gt_index][$datediff] ||= 0;
+ $data[$gt_index][$datediff] += $value;
+ }
+ }
+ }
+
+ $line_index++;
+ }
+
+ # Add the x-axis labels into the data structure
+ my $date_progression = generateDateProgression($datefrom, $dateto);
+ unshift(@data, $date_progression);
+
+ if ($self->{'gt'}) {
+ # Add Grand Total to label list
+ push(@{$self->{'labels'}}, $self->{'labelgt'});
+
+ $data[$gt_index] ||= [];
+ }
+
+ return \@data;
+}
+
+# Flatten the data structure into a list of series_ids
+sub getSeriesIDs {
+ my $self = shift;
+ my @series_ids;
+
+ foreach my $line (@{$self->{'lines'}}) {
+ foreach my $series (@$line) {
+ push(@series_ids, $series->{'series_id'});
+ }
+ }
+
+ return @series_ids;
+}
+
+# Class method to get the data necessary to populate the "select series"
+# widgets on various pages.
+sub getVisibleSeries {
+ my %cats;
+
+ # Get all visible series
+ my $dbh = Bugzilla->dbh;
+ my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " .
+ "series.name, series.series_id " .
+ "FROM series " .
+ "LEFT JOIN series_categories AS cc1 " .
+ " ON series.category = cc1.category_id " .
+ "LEFT JOIN series_categories AS cc2 " .
+ " ON series.subcategory = cc2.category_id " .
+ "LEFT JOIN user_series_map AS ucm " .
+ " ON series.series_id = ucm.series_id " .
+ "WHERE ucm.user_id = 0 OR ucm.user_id = $::userid");
+
+ foreach my $series (@$serieses) {
+ my ($cat, $subcat, $name, $series_id) = @$series;
+ $cats{$cat}{$subcat}{$name} = $series_id;
+ }
+
+ return \%cats;
+}
+
+sub generateDateProgression {
+ my ($datefrom, $dateto) = @_;
+ my @progression;
+
+ $dateto = $dateto || time();
+ my $oneday = 60 * 60 * 24;
+
+ # When the from and to dates are converted by str2time(), you end up with
+ # a time figure representing midnight at the beginning of that day. We
+ # adjust the times by 1/3 and 2/3 of a day respectively to prevent
+ # edge conditions in time2str().
+ $datefrom += $oneday / 3;
+ $dateto += (2 * $oneday) / 3;
+
+ while ($datefrom < $dateto) {
+ push (@progression, &::time2str("%Y-%m-%d", $datefrom));
+ $datefrom += $oneday;
+ }
+
+ return \@progression;
+}
+
+sub dump {
+ my $self = shift;
+
+ # Make sure we've read in our data
+ my $data = $self->data;
+
+ require Data::Dumper;
+ print "<pre>Bugzilla::Chart object:\n";
+ print Data::Dumper::Dumper($self);
+ print "</pre>";
+}
+
+1;
diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm
new file mode 100644
index 000000000..bc11389c9
--- /dev/null
+++ b/Bugzilla/Series.pm
@@ -0,0 +1,262 @@
+# -*- 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>
+
+use strict;
+use lib ".";
+
+# This module implements a series - a set of data to be plotted on a chart.
+package Bugzilla::Series;
+
+use Bugzilla;
+use Bugzilla::Util;
+use Bugzilla::User;
+
+sub new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+
+ # Create a ref to an empty hash and bless it
+ my $self = {};
+ bless($self, $class);
+
+ if ($#_ == 0) {
+ if (ref($_[0])) {
+ # We've been given a CGI object
+ $self->readParametersFromCGI($_[0]);
+ $self->createInDatabase();
+ }
+ else {
+ # We've been given a series_id.
+ $self->initFromDatabase($_[0]);
+ }
+ }
+ elsif ($#_ >= 3) {
+ $self->initFromParameters(@_);
+ }
+ else {
+ die("Bad parameters passed in - invalid number of args \($#_\)($_)");
+ }
+
+ return $self->{'already_exists'} ? $self->{'series_id'} : $self;
+}
+
+sub initFromDatabase {
+ my $self = shift;
+ my $series_id = shift;
+
+ &::detaint_natural($series_id)
+ || &::ThrowCodeError("invalid_series_id", { 'series_id' => $series_id });
+
+ my $dbh = Bugzilla->dbh;
+ my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " .
+ "cc2.name, series.name, series.creator, series.frequency, " .
+ "series.query " .
+ "FROM series " .
+ "LEFT JOIN series_categories AS cc1 " .
+ " ON series.category = cc1.category_id " .
+ "LEFT JOIN series_categories AS cc2 " .
+ " ON series.subcategory = cc2.category_id " .
+ "WHERE series.series_id = $series_id");
+
+ if (@series) {
+ $self->initFromParameters(@series);
+ }
+ else {
+ &::ThrowCodeError("invalid_series_id", { 'series_id' => $series_id });
+ }
+}
+
+sub initFromParameters {
+ my $self = shift;
+
+ # The first four parameters are compulsory, unless you immediately call
+ # createInDatabase(), in which case series_id can be left off.
+ ($self->{'series_id'}, $self->{'category'}, $self->{'subcategory'},
+ $self->{'name'}, $self->{'creator'}, $self->{'frequency'},
+ $self->{'query'}) = @_;
+
+ $self->{'public'} = $self->isSubscribed(0);
+ $self->{'subscribed'} = $self->isSubscribed($::userid);
+}
+
+sub createInDatabase {
+ my $self = shift;
+
+ # Lock some tables
+ my $dbh = Bugzilla->dbh;
+ $dbh->do("LOCK TABLES series_categories WRITE, series WRITE, " .
+ "user_series_map WRITE");
+
+ my $category_id = getCategoryID($self->{'category'});
+ my $subcategory_id = getCategoryID($self->{'subcategory'});
+
+ $self->{'creator'} = $::userid;
+
+ # Check for the series currently existing
+ trick_taint($self->{'name'});
+ $self->{'series_id'} = $dbh->selectrow_array("SELECT series_id " .
+ "FROM series WHERE category = $category_id " .
+ "AND subcategory = $subcategory_id AND name = " .
+ $dbh->quote($self->{'name'}));
+
+ if ($self->{'series_id'}) {
+ $self->{'already_exists'} = 1;
+ }
+ else {
+ trick_taint($self->{'query'});
+
+ # Insert the new series into the series table
+ $dbh->do("INSERT INTO series (creator, category, subcategory, " .
+ "name, frequency, query) VALUES ($self->{'creator'}, " .
+ "$category_id, $subcategory_id, " .
+ $dbh->quote($self->{'name'}) . ", $self->{'frequency'}," .
+ $dbh->quote($self->{'query'}) . ")");
+
+ # Retrieve series_id
+ $self->{'series_id'} = $dbh->selectrow_array("SELECT MAX(series_id) " .
+ "FROM series");
+ $self->{'series_id'}
+ || &::ThrowCodeError("missing_series_id", { 'series' => $self });
+
+ # Subscribe user to the newly-created series.
+ $self->subscribe($::userid);
+ # Public series are subscribed to by userid 0.
+ $self->subscribe(0) if ($self->{'public'} && $::userid != 0);
+ }
+
+ $dbh->do("UNLOCK TABLES");
+}
+
+# Get a category or subcategory IDs, creating the category if it doesn't exist.
+sub getCategoryID {
+ my ($category) = @_;
+ my $category_id;
+ my $dbh = Bugzilla->dbh;
+
+ # This seems for the best idiom for "Do A. Then maybe do B and A again."
+ while (1) {
+ # We are quoting this to put it in the DB, so we can remove taint
+ trick_taint($category);
+
+ $category_id = $dbh->selectrow_array("SELECT category_id " .
+ "from series_categories " .
+ "WHERE name =" . $dbh->quote($category));
+ last if $category_id;
+
+ $dbh->do("INSERT INTO series_categories (name) " .
+ "VALUES (" . $dbh->quote($category) . ")");
+ }
+
+ return $category_id;
+}
+
+sub readParametersFromCGI {
+ my $self = shift;
+ my $cgi = shift;
+
+ $self->{'category'} = $cgi->param('category')
+ || $cgi->param('newcategory')
+ || &::ThrowUserError("missing_category");
+
+ $self->{'subcategory'} = $cgi->param('subcategory')
+ || $cgi->param('newsubcategory')
+ || &::ThrowUserError("missing_subcategory");
+
+ $self->{'name'} = $cgi->param('name')
+ || &::ThrowUserError("missing_name");
+
+ $self->{'frequency'} = $cgi->param('frequency');
+ detaint_natural($self->{'frequency'})
+ || &::ThrowUserError("missing_frequency");
+
+ $self->{'public'} = $cgi->param('public') ? 1 : 0;
+
+ $self->{'query'} = $cgi->canonicalise_query("format", "ctype", "action",
+ "category", "subcategory", "name",
+ "frequency", "public", "query_format");
+}
+
+sub alter {
+ my $self = shift;
+ my $cgi = shift;
+
+ my $old_public = $self->{'public'};
+
+ # Note: $self->{'query'} will be meaningless after this call
+ $self->readParametersFromCGI($cgi);
+
+ my $category_id = getCategoryID($self->{'category'});
+ my $subcategory_id = getCategoryID($self->{'subcategory'});
+
+ # Update the entry
+ trick_taint($self->{'name'});
+ my $dbh = Bugzilla->dbh;
+ $dbh->do("UPDATE series SET " .
+ "category = $category_id, subcategory = $subcategory_id " .
+ ", name = " . $dbh->quote($self->{'name'}) .
+ ", frequency = $self->{'frequency'} " .
+ "WHERE series_id = $self->{'series_id'}");
+
+ # Update the publicness of this query.
+ if ($old_public && !$self->{'public'}) {
+ $self->unsubscribe(0);
+ }
+ elsif (!$old_public && $self->{'public'}) {
+ $self->subscribe(0);
+ }
+}
+
+sub subscribe {
+ my $self = shift;
+ my $userid = shift;
+
+ if (!$self->isSubscribed($userid)) {
+ # Subscribe current user to series_id
+ my $dbh = Bugzilla->dbh;
+ $dbh->do("INSERT INTO user_series_map " .
+ "VALUES($userid, $self->{'series_id'})");
+ }
+}
+
+sub unsubscribe {
+ my $self = shift;
+ my $userid = shift;
+
+ if ($self->isSubscribed($userid)) {
+ # Remove current user's subscription to series_id
+ my $dbh = Bugzilla->dbh;
+ $dbh->do("DELETE FROM user_series_map " .
+ "WHERE user_id = $userid AND series_id = $self->{'series_id'}");
+ }
+}
+
+sub isSubscribed {
+ my $self = shift;
+ my $userid = shift;
+
+ my $dbh = Bugzilla->dbh;
+ my $issubscribed = $dbh->selectrow_array("SELECT 1 FROM user_series_map " .
+ "WHERE user_id = $userid " .
+ "AND series_id = $self->{'series_id'}");
+ return $issubscribed;
+}
+
+1;
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index b83079861..6c3e2161a 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -121,6 +121,13 @@ $Template::Stash::LIST_OPS->{ containsany } =
return 0;
};
+# Allow us to still get the scalar if we use the list operation ".0" on it,
+# as we often do for defaults in query.cgi and other places.
+$Template::Stash::SCALAR_OPS->{ 0 } =
+ sub {
+ return $_[0];
+ };
+
# Add a "substr" method to the Template Toolkit's "scalar" object
# that returns a substring of a string.
$Template::Stash::SCALAR_OPS->{ substr } =
diff --git a/buglist.cgi b/buglist.cgi
index 0f7dda0ac..c0c13b033 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -173,6 +173,18 @@ sub LookupNamedQuery {
return $result;
}
+sub LookupSeries {
+ my ($series_id) = @_;
+ detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
+
+ my $dbh = Bugzilla->dbh;
+ my $result = $dbh->selectrow_array("SELECT query FROM series " .
+ "WHERE series_id = $series_id");
+ $result
+ || ThrowCodeError("invalid_series_id", {'series_id' => $series_id});
+ return $result;
+}
+
sub GetQuip {
my $quip;
@@ -256,6 +268,12 @@ if ($::FORM{'cmdtype'} eq "dorem") {
$params = new Bugzilla::CGI($::buffer);
$order = $params->param('order') || $order;
}
+ elsif ($::FORM{'remaction'} eq "runseries") {
+ $::buffer = LookupSeries($::FORM{"series_id"});
+ $vars->{'title'} = "Bug List: $::FORM{'namedcmd'}";
+ $params = new Bugzilla::CGI($::buffer);
+ $order = $params->param('order') || $order;
+ }
elsif ($::FORM{'remaction'} eq "load") {
my $url = "query.cgi?" . LookupNamedQuery($::FORM{"namedcmd"});
print $cgi->redirect(-location=>$url);
diff --git a/chart.cgi b/chart.cgi
new file mode 100755
index 000000000..ceaecbbab
--- /dev/null
+++ b/chart.cgi
@@ -0,0 +1,312 @@
+#!/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>
+
+# Glossary:
+# series: An individual, defined set of data plotted over time.
+# line: A set of one or more series, to be summed and drawn as a single
+# line when the series is plotted.
+# chart: A set of lines
+# So when you select rows in the UI, you are selecting one or more lines, not
+# series.
+
+# Generic Charting TODO:
+#
+# JS-less chart creation - hard.
+# Broken image on error or no data - need to do much better.
+# Centralise permission checking, so UserInGroup('editbugs') not scattered
+# everywhere.
+# Better protection on collectstats.pl for second run in a day
+# User documentation :-)
+#
+# Bonus:
+# Offer subscription when you get a "series already exists" error?
+
+use strict;
+use lib qw(.);
+
+require "CGI.pl";
+use Bugzilla::Chart;
+use Bugzilla::Series;
+
+use vars qw($cgi $template $vars);
+
+# Go back to query.cgi if we are adding a boolean chart parameter.
+if (grep(/^cmd-/, $cgi->param())) {
+ my $params = $cgi->canonicalise_query("format", "ctype", "action");
+ print "Location: query.cgi?format=" . $cgi->param('query_format') .
+ ($params ? "&$params" : "") . "\n\n";
+ exit;
+}
+
+my $template = Bugzilla->template;
+my $action = $cgi->param('action');
+my $series_id = $cgi->param('series_id');
+
+# Because some actions are chosen by buttons, we can't encode them as the value
+# of the action param, because that value is localisation-dependent. So, we
+# encode it in the name, as "action-<action>". Some params even contain the
+# series_id they apply to (e.g. subscribe, unsubscribe.)
+my @actions = grep(/^action-/, $cgi->param());
+if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) {
+ $action = $1;
+ $series_id = $2 if $2;
+}
+
+$action ||= "assemble";
+
+# Go to buglist.cgi if we are doing a search.
+if ($action eq "search") {
+ my $params = $cgi->canonicalise_query("format", "ctype", "action");
+ print "Location: buglist.cgi" . ($params ? "?$params" : "") . "\n\n";
+ exit;
+}
+
+ConnectToDatabase();
+
+confirm_login();
+
+# All these actions relate to chart construction.
+if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) {
+ # These two need to be done before the creation of the Chart object, so
+ # that the changes they make will be reflected in it.
+ if ($action =~ /^subscribe|unsubscribe$/) {
+ my $series = new Bugzilla::Series($series_id);
+ $series->$action($::userid);
+ }
+
+ my $chart = new Bugzilla::Chart($cgi);
+
+ if ($action =~ /^remove|sum$/) {
+ $chart->$action(getSelectedLines());
+ }
+ elsif ($action eq "add") {
+ my @series_ids = getAndValidateSeriesIDs();
+ $chart->add(@series_ids);
+ }
+
+ view($chart);
+}
+elsif ($action eq "plot") {
+ plot();
+}
+elsif ($action eq "wrap") {
+ # For CSV "wrap", we go straight to "plot".
+ if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
+ plot();
+ }
+ else {
+ wrap();
+ }
+}
+elsif ($action eq "create") {
+ assertCanCreate($cgi);
+ my $series = new Bugzilla::Series($cgi);
+
+ if (ref($series)) {
+ $vars->{'message'} = "series_created";
+ }
+ else {
+ $vars->{'message'} = "series_already_exists";
+ $series = new Bugzilla::Series($series);
+ }
+
+ $vars->{'series'} = $series;
+
+ print "Content-Type: text/html\n\n";
+ $template->process("global/message.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+elsif ($action eq "edit") {
+ $series_id || ThrowCodeError("invalid_series_id");
+ assertCanEdit($series_id);
+
+ my $series = new Bugzilla::Series($series_id);
+ edit($series);
+}
+elsif ($action eq "alter") {
+ $series_id || ThrowCodeError("invalid_series_id");
+ assertCanEdit($series_id);
+
+ my $series = new Bugzilla::Series($series_id);
+ $series->alter($cgi);
+ edit($series);
+}
+else {
+ ThrowCodeError("unknown_action");
+}
+
+exit;
+
+# Find any selected series and return either the first or all of them.
+sub getAndValidateSeriesIDs {
+ my @series_ids = grep(/^\d+$/, $cgi->param("name"));
+
+ return wantarray ? @series_ids : $series_ids[0];
+}
+
+# Return a list of IDs of all the lines selected in the UI.
+sub getSelectedLines {
+ my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();
+
+ return @ids;
+}
+
+# Check if the user is the owner of series_id or is an admin.
+sub assertCanEdit {
+ my ($series_id) = @_;
+
+ return if UserInGroup("admin");
+
+ my $dbh = Bugzilla->dbh;
+ my $iscreator = $dbh->selectrow_array("SELECT creator = ? FROM series " .
+ "WHERE series_id = ?", undef,
+ $::userid, $series_id);
+ $iscreator || ThrowUserError("illegal_series_edit");
+}
+
+# Check if the user is permitted to create this series with these parameters.
+sub assertCanCreate {
+ my ($cgi) = shift;
+
+ UserInGroup("editbugs") || ThrowUserError("illegal_series_creation");
+
+ # Only admins may create public queries
+ UserInGroup('admin') || $cgi->delete('public');
+
+ # Check permission for frequency
+ my $min_freq = 7;
+ if ($cgi->param('frequency') < $min_freq && !UserInGroup("admin")) {
+ ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
+ }
+}
+
+sub validateWidthAndHeight {
+ $vars->{'width'} = $cgi->param('width');
+ $vars->{'height'} = $cgi->param('height');
+
+ if (defined($vars->{'width'})) {
+ (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
+ || ThrowCodeError("invalid_dimensions");
+ }
+
+ if (defined($vars->{'height'})) {
+ (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
+ || ThrowCodeError("invalid_dimensions");
+ }
+
+ # The equivalent of 2000 square seems like a very reasonable maximum size.
+ # This is merely meant to prevent accidental or deliberate DOS, and should
+ # have no effect in practice.
+ if ($vars->{'width'} && $vars->{'height'}) {
+ (($vars->{'width'} * $vars->{'height'}) <= 4000000)
+ || ThrowUserError("chart_too_large");
+ }
+}
+
+sub edit {
+ my $series = shift;
+
+ $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+ $vars->{'creator'} = new Bugzilla::User($series->{'creator'});
+
+ # If we've got any parameters, use those in preference to the values
+ # read from the database. This is a bit ugly, but I can't see a better
+ # way to make this work in the no-JS situation.
+ if ($cgi->param('category') || $cgi->param('subcategory') ||
+ $cgi->param('name') || $cgi->param('frequency') ||
+ $cgi->param('public'))
+ {
+ $vars->{'default'} = new Bugzilla::Series($series->{'series_id'},
+ $cgi->param('category') || $series->{'category'},
+ $cgi->param('subcategory') || $series->{'subcategory'},
+ $cgi->param('name') || $series->{'name'},
+ $series->{'creator'},
+ $cgi->param('frequency') || $series->{'frequency'});
+
+ $vars->{'default'}{'public'}
+ = $cgi->param('public') || $series->{'public'};
+ }
+ else {
+ $vars->{'default'} = $series;
+ }
+
+ print "Content-Type: text/html\n\n";
+ $template->process("reports/edit-series.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub plot {
+ validateWidthAndHeight();
+ $vars->{'chart'} = new Bugzilla::Chart($cgi);
+
+ my $format = &::GetFormat("reports/chart",
+ "",
+ $cgi->param('ctype'));
+
+ # Debugging PNGs is a pain; we need to be able to see the error messages
+ if ($cgi->param('debug')) {
+ print "Content-Type: text/html\n\n";
+ $vars->{'chart'}->dump();
+ }
+
+ print "Content-Type: $format->{'ctype'}\n\n";
+ $template->process($format->{'template'}, $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub wrap {
+ validateWidthAndHeight();
+
+ # We create a Chart object so we can validate the parameters
+ my $chart = new Bugzilla::Chart($cgi);
+
+ $vars->{'time'} = time();
+
+ $vars->{'imagebase'} = $cgi->canonicalise_query(
+ "action", "action-wrap", "ctype", "format", "width", "height");
+
+ print "Content-Type:text/html\n\n";
+ $template->process("reports/chart.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub view {
+ my $chart = shift;
+
+ # Set defaults
+ foreach my $field ('category', 'subcategory', 'name', 'ctype') {
+ $vars->{'default'}{$field} = $cgi->param($field) || 0;
+ }
+
+ # Pass the state object to the display UI.
+ $vars->{'chart'} = $chart;
+ $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+
+ print "Content-Type: text/html\n\n";
+
+ # If we have having problems with bad data, we can set debug=1 to dump
+ # the data structure.
+ $chart->dump() if $cgi->param('debug');
+
+ $template->process("reports/create-chart.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
diff --git a/checksetup.pl b/checksetup.pl
index 451078863..df785d832 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -26,6 +26,7 @@
# Jacob Steenhagen <jake@bugzilla.org>
# Bradley Baetz <bbaetz@student.usyd.edu.au>
# Tobias Burnus <burnus@net-b.de>
+# Gervase Markham <gerv@gerv.net>
#
#
# Direct any questions on this source code to
@@ -112,6 +113,8 @@
#
use strict;
+use lib ".";
+
use vars qw( $db_name %answer );
use Bugzilla::Constants;
@@ -1737,6 +1740,42 @@ $table{group_control_map} =
unique(product_id, group_id),
index(group_id)';
+# 2003-06-26 gerv@gerv.net, bug 16009
+# Generic charting over time of arbitrary queries.
+# Queries are disabled when frequency == 0.
+$table{series} =
+ 'series_id mediumint auto_increment primary key,
+ creator mediumint not null,
+ category smallint not null,
+ subcategory smallint not null,
+ name varchar(64) not null,
+ frequency smallint not null,
+ last_viewed datetime default null,
+ query mediumtext not null,
+
+ index(creator),
+ unique(creator, category, subcategory, name)';
+
+$table{series_data} =
+ 'series_id mediumint not null,
+ date datetime not null,
+ value mediumint not null,
+
+ unique(series_id, date)';
+
+$table{user_series_map} =
+ 'user_id mediumint not null,
+ series_id mediumint not null,
+
+ index(series_id),
+ unique(user_id, series_id)';
+
+$table{series_categories} =
+ 'category_id smallint auto_increment primary key,
+ name varchar(64) not null,
+
+ unique(name)';
+
###########################################################################
# Create tables
###########################################################################
@@ -3530,6 +3569,109 @@ if ($mapcnt == 0) {
}
}
+# 2003-06-26 Copy the old charting data into the database, and create the
+# queries that will keep it all running. When the old charting system goes
+# away, if this code ever runs, it'll just find no files and do nothing.
+my $series_exists = $dbh->selectrow_array("SELECT 1 FROM series LIMIT 1");
+
+if (!$series_exists) {
+ print "Migrating old chart data into database ...\n" unless $silent;
+
+ use Bugzilla::Series;
+
+ # We prepare the handle to insert the series data
+ my$seriesdatasth = $dbh->prepare("INSERT INTO series_data " .
+ "(series_id, date, value) " .
+ "VALUES (?, ?, ?)");
+
+ # Fields in the data file (matches the current collectstats.pl)
+ my @statuses =
+ qw(NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED VERIFIED CLOSED);
+ my @resolutions =
+ qw(FIXED INVALID WONTFIX LATER REMIND DUPLICATE WORKSFORME MOVED);
+ my @fields = (@statuses, @resolutions);
+
+ # We have a localisation problem here. Where do we get these values?
+ my $all_name = "-All-";
+ my $open_name = "All Open";
+
+ # We can't give the Series we create a meaningful owner; that's not a big
+ # problem. But we do need to set this global, otherwise Series.pm objects.
+ $::userid = 0;
+
+ my $products = $dbh->selectall_arrayref("SELECT name FROM products");
+
+ foreach my $product ((map { $_->[0] } @$products), "-All-") {
+ # First, create the series
+ my %queries;
+ my %seriesids;
+
+ my $query_prod = "";
+ if ($product ne "-All-") {
+ $query_prod = "product=" . html_quote($product) . "&";
+ }
+
+ # The query for statuses is different to that for resolutions.
+ $queries{$_} = ($query_prod . "status=$_") foreach (@statuses);
+ $queries{$_} = ($query_prod . "resolution=$_") foreach (@resolutions);
+
+ foreach my $field (@fields) {
+ # Create a Series for each field in this product
+ my $series = new Bugzilla::Series(-1, $product, $all_name,
+ $field, $::userid, 1,
+ $queries{$field});
+ $series->createInDatabase();
+ $seriesids{$field} = $series->{'series_id'};
+ }
+
+ # We also add a new query for "Open", so that migrated products get
+ # the same set as new products (see editproducts.cgi.)
+ my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED");
+ my $query = join("&", map { "bug_status=$_" } @openedstatuses);
+ my $series = new Bugzilla::Series(-1, $product, $all_name,
+ $open_name, $::userid, 1,
+ $query_prod . $query);
+ $series->createInDatabase();
+
+ # Now, we attempt to read in historical data, if any
+ # Convert the name in the same way that collectstats.pl does
+ my $product_file = $product;
+ $product_file =~ s/\//-/gs;
+ $product_file = "data/mining/$product_file";
+
+ # There are many reasons that this might fail (e.g. no stats for this
+ # product), so we don't worry if it does.
+ open(IN, $product_file) or next;
+
+ # The data files should be in a standard format, even for old
+ # Bugzillas, because of the conversion code further up this file.
+ my %data;
+
+ while (<IN>) {
+ if (/^(\d+\|.*)/) {
+ my @numbers = split(/\||\r/, $1);
+ for my $i (0 .. $#fields) {
+ # $numbers[0] is the date
+ $data{$fields[$i]}{$numbers[0]} = $numbers[$i + 1];
+ }
+ }
+ }
+
+ close(IN);
+
+ foreach my $field (@fields) {
+ # Insert values into series_data: series_id, date, value
+ my %fielddata = %{$data{$field}};
+ foreach my $date (keys %fielddata) {
+ # We prepared this above
+ $seriesdatasth->execute($seriesids{$field},
+ $dbh->quote($date),
+ $fielddata{$date});
+ }
+ }
+ }
+}
+
# If you had to change the --TABLE-- definition in any way, then add your
# differential change code *** A B O V E *** this comment.
#
diff --git a/collectstats.pl b/collectstats.pl
index eedeaa35b..42f8e682e 100755
--- a/collectstats.pl
+++ b/collectstats.pl
@@ -32,7 +32,10 @@ use strict;
use IO::Handle;
use vars @::legal_product;
+use lib ".";
require "globals.pl";
+use Bugzilla::Search;
+use Bugzilla::User;
use Bugzilla;
@@ -79,6 +82,8 @@ my $tend = time;
&calculate_dupes();
+CollectSeriesData();
+
# Generate a static RDF file containing the default view of the duplicates data.
open(CGI, "GATEWAY_INTERFACE=cmdline REQUEST_METHOD=GET QUERY_STRING=ctype=rdf ./duplicates.cgi |")
|| die "can't fork duplicates.cgi: $!";
@@ -421,3 +426,71 @@ sub delta_time {
my $seconds = $delta - ($minutes * 60) - ($hours * 3600);
return sprintf("%02d:%02d:%02d" , $hours, $minutes, $seconds);
}
+
+sub CollectSeriesData {
+ # We need some way of randomising the distribution of series, such that
+ # all of the series which are to be run every 7 days don't run on the same
+ # day. This is because this might put the server under severe load if a
+ # particular frequency, such as once a week, is very common. We achieve
+ # this by only running queries when:
+ # (days_since_epoch + series_id) % frequency = 0. So they'll run every
+ # <frequency> days, but the start date depends on the series_id.
+ my $days_since_epoch = int(time() / (60 * 60 * 24));
+ my $today = today_dash();
+
+ CleanupChartTables() if ($days_since_epoch % 7 == 0);
+
+ my $dbh = Bugzilla->dbh;
+ my $serieses = $dbh->selectall_hashref("SELECT series_id, query " .
+ "FROM series " .
+ "WHERE frequency != 0 AND " .
+ "($days_since_epoch + series_id) % frequency = 0",
+ "series_id");
+
+ # We prepare the insertion into the data table, for efficiency.
+ my $sth = $dbh->prepare("INSERT INTO series_data " .
+ "(series_id, date, value) " .
+ "VALUES (?, " . $dbh->quote($today) . ", ?)");
+
+ foreach my $series_id (keys %$serieses) {
+ # We set up the user for Search.pm's permission checking - each series
+ # runs with the permissions of its creator.
+ $::vars->{'user'} =
+ new Bugzilla::User($serieses->{$series_id}->{'creator'});
+
+ my $cgi = new Bugzilla::CGI($serieses->{$series_id}->{'query'});
+ my $search = new Bugzilla::Search('params' => $cgi,
+ 'fields' => ["bugs.bug_id"]);
+ my $sql = $search->getSQL();
+
+ # We need to count the returned rows. Without subselects, we can't
+ # do this directly in the SQL for all queries. So we do it by hand.
+ my $data = $dbh->selectall_arrayref($sql);
+
+ my $count = scalar(@$data) || 0;
+
+ $sth->execute($series_id, $count);
+ }
+}
+
+sub CleanupChartTables {
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->do("LOCK TABLES series WRITE, user_series_map AS usm READ");
+
+ # Find all those that no-one subscribes to
+ my $series_data = $dbh->selectall_arrayref("SELECT series.series_id " .
+ "FROM series LEFT JOIN user_series_map AS usm " .
+ "ON series.series_id = usm.series_id " .
+ "WHERE usm.series_id IS NULL");
+
+ my $series_ids = join(",", map({ $_->[0] } @$series_data));
+
+ # Stop collecting data on all series which no-one is subscribed to.
+ if ($series_ids) {
+ $dbh->do("UPDATE series SET frequency = 0 " .
+ "WHERE series_id IN($series_ids)");
+ }
+
+ $dbh->do("UNLOCK TABLES");
+}
diff --git a/editcomponents.cgi b/editcomponents.cgi
index 74e0debe8..018c89cdf 100755
--- a/editcomponents.cgi
+++ b/editcomponents.cgi
@@ -31,6 +31,8 @@ use lib ".";
require "CGI.pl";
require "globals.pl";
+use Bugzilla::Series;
+
# Shut up misguided -w warnings about "used only once". For some reason,
# "use vars" chokes on me when I try it here.
@@ -352,6 +354,8 @@ if ($action eq 'add') {
print "</TR></TABLE>\n<HR>\n";
print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n";
print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n";
+ print "<INPUT TYPE=HIDDEN NAME='open_name' VALUE='All Open'>\n";
+ print "<INPUT TYPE=HIDDEN NAME='closed_name' VALUE='All Closed'>\n";
print "</FORM>";
my $other = $localtrailer;
@@ -440,6 +444,32 @@ if ($action eq 'new') {
SqlQuote($initialownerid) . "," .
SqlQuote($initialqacontactid) . ")");
+ # Insert default charting queries for this product.
+ # If they aren't using charting, this won't do any harm.
+ GetVersionTable();
+
+ my @series;
+ my $prodcomp = "&product=$product&component=$component";
+
+ # For localisation reasons, we get the title of the queries from the
+ # submitted form.
+ my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED");
+ my $statuses = join("&", map { "bug_status=$_" } @openedstatuses);
+ push(@series, [$::FORM{'open_name'}, $statuses . $prodcomp]);
+
+ my $resolved = "field0-0-0=resolution&type0-0-0=notequals&value0-0-0=---";
+ push(@series, [$::FORM{'closed_name'}, $resolved . $prodcomp]);
+
+ foreach my $sdata (@series) {
+ # We create the series with an nonsensical series_id, which is
+ # guaranteed not to exist. This is OK, because we immediately call
+ # createInDatabase().
+ my $series = new Bugzilla::Series(-1, $product, $component,
+ $sdata->[0], $::userid, 1,
+ $sdata->[1]);
+ $series->createInDatabase();
+ }
+
# Make versioncache flush
unlink "data/versioncache";
diff --git a/editproducts.cgi b/editproducts.cgi
index 423f028fe..55089d9ae 100755
--- a/editproducts.cgi
+++ b/editproducts.cgi
@@ -33,9 +33,11 @@ use vars qw ($template $vars);
use Bugzilla::Constants;
require "CGI.pl";
require "globals.pl";
+use Bugzilla::Series;
# Shut up misguided -w warnings about "used only once". "use vars" just
# doesn't work for me.
+use vars qw(@legal_bug_status @legal_resolution);
sub sillyness {
my $zz;
@@ -272,6 +274,8 @@ if ($action eq 'add') {
print "</TABLE>\n<HR>\n";
print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n";
print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n";
+ print "<INPUT TYPE=HIDDEN NAME='subcategory' VALUE='-All-'>\n";
+ print "<INPUT TYPE=HIDDEN NAME='open_name' VALUE='All Open'>\n";
print "</FORM>";
my $other = $localtrailer;
@@ -349,7 +353,7 @@ if ($action eq 'new') {
# If we're using bug groups, then we need to create a group for this
# product as well. -JMR, 2/16/00
- if(Param("makeproductgroups")) {
+ if (Param("makeproductgroups")) {
# Next we insert into the groups table
SendSQL("INSERT INTO groups " .
"(name, description, isbuggroup, last_changed) " .
@@ -390,8 +394,39 @@ if ($action eq 'new') {
PopGlobalSQLState();
}
}
+ }
-
+ # Insert default charting queries for this product.
+ # If they aren't using charting, this won't do any harm.
+ GetVersionTable();
+
+ my @series;
+
+ # We do every status, every resolution, and an "opened" one as well.
+ foreach my $bug_status (@::legal_bug_status) {
+ push(@series, [$bug_status, "bug_status=$bug_status"]);
+ }
+
+ foreach my $resolution (@::legal_resolution) {
+ next if !$resolution;
+ push(@series, [$resolution, "resolution=$resolution"]);
+ }
+
+ # For localisation reasons, we get the name of the "global" subcategory
+ # and the title of the "open" query from the submitted form.
+ my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED");
+ my $query = join("&", map { "bug_status=$_" } @openedstatuses);
+ push(@series, [$::FORM{'open_name'}, $query]);
+
+ foreach my $sdata (@series) {
+ # We create the series with an nonsensical series_id, which is
+ # guaranteed not to exist. This is OK, because we immediately call
+ # createInDatabase().
+ my $series = new Bugzilla::Series(-1, $product,
+ $::FORM{'subcategory'},
+ $sdata->[0], $::userid, 1,
+ $sdata->[1] . "&product=$product");
+ $series->createInDatabase();
}
# Make versioncache flush
diff --git a/query.cgi b/query.cgi
index 2a8051b6b..5e623437c 100755
--- a/query.cgi
+++ b/query.cgi
@@ -137,7 +137,9 @@ sub PrefillForm {
"status_whiteboard_type", "bug_id",
"bugidtype", "keywords", "keywords_type",
"x_axis_field", "y_axis_field", "z_axis_field",
- "chart_format", "cumulate", "x_labels_vertical")
+ "chart_format", "cumulate", "x_labels_vertical",
+ "category", "subcategory", "name", "newcategory",
+ "newsubcategory", "public", "frequency")
{
# This is a bit of a hack. The default, empty list has
# three entries to accommodate the needs of the email fields -
@@ -378,6 +380,11 @@ $vars->{'userdefaultquery'} = $userdefaultquery;
$vars->{'orders'} = \@orders;
$default{'querytype'} = $deforder || 'Importance';
+if (($::FORM{'query_format'} || $::FORM{'format'}) eq "create-series") {
+ require Bugzilla::Chart;
+ $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+}
+
# Add in the defaults.
$vars->{'default'} = \%default;
diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl
index d2abbdadb..6a7217d76 100644
--- a/template/en/default/filterexceptions.pl
+++ b/template/en/default/filterexceptions.pl
@@ -197,6 +197,36 @@
'bug.delta',
],
+'reports/chart.html.tmpl' => [
+ 'width',
+ 'height',
+ 'imageurl',
+ 'sizeurl',
+ 'height + 100',
+ 'height - 100',
+ 'width + 100',
+ 'width - 100',
+],
+
+'reports/series-common.html.tmpl' => [
+ 'sel.name',
+ 'sel.accesskey',
+ '"onchange=\'$sel.onchange\'" IF sel.onchange',
+],
+
+'reports/chart.csv.tmpl' => [
+ 'data.$j.$i',
+],
+
+'reports/create-chart.html.tmpl' => [
+ 'series.series_id',
+ 'newidx',
+],
+
+'reports/edit-series.html.tmpl' => [
+ 'default.series_id',
+],
+
'list/change-columns.html.tmpl' => [
'column',
'field_descs.${column} || column', #
@@ -293,6 +323,7 @@
'old_email', # email address
'new_email', # email address
'message_tag',
+ 'series.frequency * 2',
],
'global/select-menu.html.tmpl' => [
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index 68f046091..84a5e3259 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -132,6 +132,11 @@
[% title = "Invalid Dimensions" %]
The width or height specified is not a positive integer.
+ [% ELSIF error == "invalid_series_id" %]
+ [% title = "Invalid Series" %]
+ The series_id [% series_id FILTER html %] is not valid. It may be that
+ this series has been deleted.
+
[% ELSIF error == "mismatched_bug_ids_on_obsolete" %]
Attachment [% attach_id FILTER html %] ([% description FILTER html %])
is attached to bug [% attach_bug_id FILTER html %], but you tried to
@@ -178,6 +183,12 @@
[% ELSIF error == "missing_bug_id" %]
No bug ID was given.
+ [% ELSIF error == "missing_series_id" %]
+ Having inserted a series into the database, no series_id was returned for
+ it. Series: [% series.category FILTER html %] /
+ [%+ series.subcategory FILTER html %] /
+ [%+ series.name FILTER html %].
+
[% 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.
diff --git a/template/en/default/global/messages.html.tmpl b/template/en/default/global/messages.html.tmpl
index 13136d6cf..6b9612f54 100644
--- a/template/en/default/global/messages.html.tmpl
+++ b/template/en/default/global/messages.html.tmpl
@@ -131,6 +131,29 @@
<a href="editflagtypes.cgi">Back to flag types.</a>
</p>
+ [% ELSIF message_tag == "series_already_exists" %]
+ [% title = "Series Already Exists" %]
+ A series <em>[% series.category FILTER html %] /
+ [%+ series.subcategory FILTER html %] /
+ [%+ series.name FILTER html %]</em>
+ already exists. If you want to create this series, you will need to give
+ it a different name. @@@ subscribe?
+ <br><br>
+ Go back or
+ <a href="query.cgi?format=create-series">create another series</a>.
+
+ [% ELSIF message_tag == "series_created" %]
+ [% title = "Series Created" %]
+ The series <em>[% series.category FILTER html %] /
+ [%+ series.subcategory FILTER html %] /
+ [%+ series.name FILTER html %]</em>
+ has been created. Note that you may need to wait up to
+ [% series.frequency * 2 %] days before there will be enough data for a
+ chart of this series to be produced.
+ <br><br>
+ Go back or
+ <a href="query.cgi?format=create-series">create another series</a>.
+
[% ELSIF message_tag == "shutdown" %]
[% title = "Bugzilla is Down" %]
[% Param("shutdownhtml") %]
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index f626c640b..a057ef96b 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -255,7 +255,7 @@
You entered <tt>[% value FILTER html %]</tt>, which isn't.
[% ELSIF error == "illegal_date" %]
- [% title = "Your Query Makes No Sense" %]
+ [% title = "Illegal Date" %]
'<tt>[% date FILTER html %]</tt>' is not a legal date.
[% ELSIF error == "illegal_email_address" %]
@@ -266,6 +266,11 @@
It must also not contain any of these special characters:
<tt>\ ( ) &amp; &lt; &gt; , ; : &quot; [ ]</tt>, or any whitespace.
+ [% ELSIF error == "illegal_frequency" %]
+ [% title = "Too Frequent" %]
+ Unless you are an administrator, you may not create series which are
+ run more often than once every [% minimum FILTER html %] days.
+
[% ELSIF error == "illegal_group_control_combination" %]
[% title = "Your Group Control Combination Is Illegal" %]
Your group control combination for group &quot;
@@ -282,6 +287,18 @@
The name of your query cannot contain any of the following characters:
&lt;, &gt;, &amp;.
+ [% ELSIF error == "illegal_series_creation" %]
+ You are not authorised to create series.
+
+ [% ELSIF error == "illegal_series_edit" %]
+ You are not authorised to edit this series. To do this, you must either
+ be its creator, or an administrator.
+
+ [% ELSIF error == "insufficient_data" %]
+ [% title = "Insufficient Data" %]
+ None of the series you selected have any data associated with them, so a
+ chart cannot be plotted.
+
[% ELSIF error == "insufficient_data_points" %]
We don't have enough data points to make a graph (yet).
@@ -352,10 +369,19 @@
if you are going to accept it. Part of accepting
a bug is giving an estimate of when it will be fixed.
+ [% ELSIF error == "misarranged_dates" %]
+ [% title = "Misarranged Dates" %]
+ Your start date ([% datefrom FILTER html %]) is after
+ your end date ([% dateto FILTER html %]).
+
[% ELSIF error == "missing_attachment_description" %]
[% title = "Missing Attachment Description" %]
You must enter a description for the attachment.
+ [% ELSIF error == "missing_category" %]
+ [% title = "Missing Category" %]
+ You did not specify a category for this series.
+
[% ELSIF error == "missing_content_type" %]
[% title = "Missing Content-Type" %]
You asked Bugzilla to auto-detect the content type, but
@@ -383,14 +409,26 @@
You must specify one or more fields in which to search for
<tt>[% email FILTER html %]</tt>.
+ [% ELSIF error == "missing_frequency" %]
+ [% title = "Missing Frequency" %]
+ You did not specify a valid frequency for this series.
+
+ [% ELSIF error == "missing_name" %]
+ [% title = "Missing Name" %]
+ You did not specify a name for this series.
+
[% ELSIF error == "missing_query" %]
[% title = "Missing Query" %]
The query named <em>[% queryname FILTER html %]</em> does not
exist.
+ [% ELSIF error == "missing_subcategory" %]
+ [% title = "Missing Subcategory" %]
+ You did not specify a subcategory for this series.
+
[% ELSIF error == "need_component" %]
[% title = "Component Required" %]
- You must specify a component to help determine the new owner of these bugs.
+ You must specify a component to help determine the new owner of these bugs.
[% ELSIF error == "need_numeric_value" %]
[% title = "Numeric Value Required" %]
diff --git a/template/en/default/reports/chart.csv.tmpl b/template/en/default/reports/chart.csv.tmpl
new file mode 100644
index 000000000..83620bf08
--- /dev/null
+++ b/template/en/default/reports/chart.csv.tmpl
@@ -0,0 +1,40 @@
+[%# 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>
+ #%]
+
+[% data = chart.data %]
+Date\Series,
+[% FOREACH label = chart.labels %]
+ [% label FILTER csv %][% "," UNLESS loop.last %]
+[% END %]
+[%# The data, which is in the correct format for GD, is conceptually the wrong
+ # way round for CSV output. So, we need to invert it here, which is why
+ # these loops aren't just plain FOREACH.
+ #%]
+[% i = 0 %]
+[% WHILE i < data.0.size %]
+ [% j = 0 %]
+ [% WHILE j < data.size %]
+ [% data.$j.$i %][% "," UNLESS (j == data.size - 1) %]
+ [% j = j + 1 %]
+ [% END %]
+ [% i = i + 1 %]
+
+[% END %]
diff --git a/template/en/default/reports/chart.html.tmpl b/template/en/default/reports/chart.html.tmpl
new file mode 100644
index 000000000..95d52d725
--- /dev/null
+++ b/template/en/default/reports/chart.html.tmpl
@@ -0,0 +1,66 @@
+ <!-- 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:
+ #%]
+
+[% DEFAULT width = 600
+ height = 350
+%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Chart"
+ h3 = time2str("%Y-%m-%d %H:%M:%S", time)
+%]
+
+<div align="center">
+
+ [% imageurl = BLOCK %]chart.cgi?
+ [% imagebase FILTER html %]&amp;ctype=png&amp;action=plot&amp;width=
+ [% width %]&amp;height=[% height -%]
+ [% END %]
+
+ <img alt="Graphical report results" src="[% imageurl %]"
+ width="[% width %]" height="[% height %]">
+ <p>
+ [% sizeurl = BLOCK %]chart.cgi?
+ [% imagebase FILTER html %]&amp;action=wrap
+ [% END %]
+ <a href="[% sizeurl %]&amp;width=[% width %]&amp;height=
+ [% height + 100 %]">Taller</a><br>
+ <a href="[% sizeurl %]&amp;width=[% width - 100 %]&amp;height=
+ [% height %]">Thinner</a> *
+ <a href="[% sizeurl %]&amp;width=[% width + 100 %]&amp;height=
+ [% height %]">Fatter</a>&nbsp;&nbsp;&nbsp;&nbsp;<br>
+ <a href="[% sizeurl %]&amp;width=[% width %]&amp;height=
+ [% height - 100 %]">Shorter</a><br>
+ </p>
+
+ <p>
+ <a href="chart.cgi?
+ [% imagebase FILTER html %]&amp;ctype=csv&amp;action=plot">CSV</a> |
+ <a href="chart.cgi?[% imagebase FILTER html %]&amp;action=assemble">Edit
+ this chart</a>
+ </p>
+
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/reports/chart.png.tmpl b/template/en/default/reports/chart.png.tmpl
new file mode 100644
index 000000000..43d4e962d
--- /dev/null
+++ b/template/en/default/reports/chart.png.tmpl
@@ -0,0 +1,56 @@
+[%# 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>
+ #%]
+
+[% y_label = "Bugs" %]
+[% x_label = "Time" %]
+
+[% IF cumulate %]
+ [% USE graph = GD.Graph.area(width, height) %]
+ [% graph.set(cumulate => "true") %]
+[% ELSE %]
+ [% USE graph = GD.Graph.lines(width, height) %]
+[% END %]
+
+[% FILTER null;
+ x_label_skip = (30 * chart.data.0.size / width);
+
+ graph.set(x_label => x_label,
+ y_label => y_label,
+ y_tick_number => 8,
+ x_label_position => 0.5,
+ x_labels_vertical => 1,
+ x_label_skip => x_label_skip,
+ legend_placement => "RT",
+ line_width => 2);
+
+ # Workaround for the fact that set_legend won't take chart.labels directly,
+ # because chart.labels is an array reference rather than an array.
+ graph.set_legend(chart.labels.0, chart.labels.1, chart.labels.2,
+ chart.labels.3, chart.labels.4, chart.labels.5,
+ chart.labels.6, chart.labels.7, chart.labels.8,
+ chart.labels.9, chart.labels.10, chart.labels.11,
+ chart.labels.12, chart.labels.13, chart.labels.14,
+ chart.labels.15);
+
+ graph.plot(chart.data).png | stdout(1);
+ END;
+-%]
+
diff --git a/template/en/default/reports/create-chart.html.tmpl b/template/en/default/reports/create-chart.html.tmpl
new file mode 100644
index 000000000..fe0b4a76c
--- /dev/null
+++ b/template/en/default/reports/create-chart.html.tmpl
@@ -0,0 +1,281 @@
+<!-- 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:
+ # chart: Chart object representing the currently assembled chart.
+ # category: hash (keyed by category) of hashes (keyed by subcategory) of
+ # hashes (keyed by name), with value being the series_id of the
+ # series. Contains details of all series the user can see.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Create Chart"
+%]
+
+[% PROCESS "reports/series-common.html.tmpl"
+ donames = 1
+%]
+
+<script>
+[%# This function takes necessary action on selection of a subcategory %]
+function subcatSelected() {
+ var cat = document.chartform.category.value;
+ var subcat = document.chartform.subcategory.value;
+ var names = series[cat][subcat];
+
+ var namewidget = document.chartform.name;
+
+ namewidget.options.length = 0;
+ var i = 0;
+
+ for (x in names) {
+ namewidget.options[i] = new Option(x, names[x]);
+ i++;
+ }
+
+ namewidget.options[0].selected = true;
+
+ checkNewState();
+}
+</script>
+
+[% gttext = "Grand Total" %]
+
+<h3>Current Data Sets:</h3>
+
+<form method="get" action="chart.cgi" name="chartform">
+ [% IF chart.lines.size > 0 %]
+ <table border="0" cellspacing="2" cellpadding="2">
+ <tr>
+ <th>Select</th>
+ <th>As</th>
+ <th></th>
+ <th>Data Set</th>
+ <th>Subs</th>
+ <th></th>
+ </tr>
+
+ [%# The external loop has two counters; one which keeps track of where we
+ # are in the old labels array, and one which keeps track of the new
+ # indexes for the form elements. They are different if chart.lines has
+ # empty slots in it.
+ #%]
+ [% labelidx = 0 %]
+ [% newidx = 0 %]
+
+ [% FOREACH line = chart.lines %]
+ [% IF NOT line %]
+ [%# chart.lines has an empty slot, so chart.labels will too. We
+ # increment labelidx only to keep the labels in sync with the data.
+ #%]
+ [% labelidx = labelidx + 1 %]
+ [% NEXT %]
+ [% END %]
+
+ [% FOREACH series = line %]
+ <tr>
+ [% IF loop.first %]
+ <td align="center" rowspan="[% line.size %]">
+ <input type="checkbox" value="1" name="select[% newidx %]">
+ </td>
+ <td rowspan="[% line.size %]">
+ <input type="text" size="20" name="label[% newidx %]"
+ value="[% (chart.labels.$labelidx OR series.name)
+ FILTER html %]">
+ </td>
+ [% END %]
+
+ <td>
+ [% "{" IF line.size > 1 %]
+ </td>
+
+ <td>
+ <a href="buglist.cgi?cmdtype=dorem&amp;namedcmd=
+ [% series.category FILTER html %]-
+ [% series.subcategory FILTER html %]-
+ [% series.name FILTER html -%]&amp;series_id=
+ [% series.series_id %]&amp;remaction=runseries">
+ [% series.category FILTER html %] /
+ [%+ series.subcategory FILTER html %] /
+ [%+ series.name FILTER html %]
+ </a>
+ <input type="hidden" name="line[% newidx %]"
+ value="[% series.series_id %]">
+ </td>
+
+ <td>
+ [% IF series.creator != 0 %]
+ [% IF series.subscribed %]
+ <input type="submit" value="Unsubscribe" style="width: 12ex;"
+ name="action-unsubscribe[% series.series_id %]">
+ [% ELSE %]
+ <input type="submit" value="Subscribe" style="width: 12ex;"
+ name="action-subscribe[% series.series_id %]">
+ [% END %]
+ [% END %]
+ </td>
+
+ <td align="center">
+ [% IF user.userid == series.creator OR UserInGroup("admin") %]
+ <a href="chart.cgi?action=edit&series_id=
+ [% series.series_id %]">Edit</a>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% labelidx = labelidx + 1 %]
+ [% newidx = newidx + 1 %]
+ [% END %]
+
+ [% IF chart.gt %]
+ <tr>
+ <td align="center">
+ <input type="checkbox" value="1" name="select65536">
+ <input type="hidden" value="1" name="gt">
+ </td>
+ <td>
+ <input type="text" size="20" name="labelgt"
+ value="[% (chart.labelgt OR gttext) FILTER html %]">
+ </td>
+ <td></td>
+ <td>
+ <i>[% gttext FILTER html %]</i>
+ </td>
+ <td></td>
+ <td></td>
+ </tr>
+ [% END %]
+ <tr>
+ <td colspan="6">&nbsp;</td>
+ </tr>
+
+ <tr>
+ <td valign="bottom" style="text-align: center;">
+ <input type="submit" name="action-sum" value="Sum"
+ style="width: 5em;"><br>
+ <input type="submit" name="action-remove" value="Remove"
+ style="width: 5em;">
+ </td>
+
+ <td style="text-align: right; vertical-align: bottom;">
+ <b>Cumulate:</b>
+ <input type="checkbox" name="cumulate" value="1">
+ </td>
+
+ <td></td>
+ <td valign="bottom">
+ <b>Date Range:</b>
+ <input type="text" size="12" name="datefrom"
+ value="[% time2str("%Y-%m-%d", chart.datefrom) IF chart.datefrom%]">
+ <b>to</b>
+ <input type="text" size="12" name="dateto"
+ value="[% time2str("%Y-%m-%d", chart.dateto) IF chart.dateto %]">
+ </td>
+
+ <td valign="bottom">
+ </td>
+
+ <td style="text-align: right" valign="bottom">
+ <input type="submit" name="action-wrap" value="Chart"
+ style="width: 5em;">
+ </td>
+ </tr>
+ </table>
+ [% ELSE %]
+ <p><i>None</i></p>
+ [% END %]
+
+<h3>Select Data Sets:</h3>
+
+ <table cellpadding="2" cellspacing="2" border="0">
+ [% IF NOT category OR category.size == 0 %]
+ <tr>
+ <td>
+ <i>You do not have permissions to see any data sets, or none
+ exist.</i>
+ </td>
+ </tr>
+ [% ELSE %]
+ <tr>
+ <th>Category:</th>
+ <noscript><th></th></noscript>
+ <th>Sub-category:</th>
+ <noscript><th></th></noscript>
+ <th>Name:</th>
+ <th><br>
+ </th>
+ </tr>
+ <tr>
+
+ [% PROCESS series_select sel = { name => 'category',
+ size => 5,
+ onchange = "catSelected();
+ subcatSelected();" } %]
+
+ <noscript>
+ <td>
+ <input type="submit" name="action-assemble" value="Update -->">
+ </td>
+ </noscript>
+
+ [% PROCESS series_select sel = { name => 'subcategory',
+ size => 5,
+ onchange = "subcatSelected()" } %]
+
+ <noscript>
+ <td>
+ <input type="submit" name="action-assemble" value="Update -->">
+ </td>
+ </noscript>
+
+ <td align="left">
+ <label for="name" accesskey="N">
+ <select name="name" id="name" style="width: 15em"
+ size="5" multiple="multiple"
+ [% FOREACH x = name.keys.sort %]
+ <option value="[% name.$x FILTER html %]"
+ [%# " selected" IF lsearch(default.name, x) != -1 %]>
+ [% x FILTER html %]</option>
+ [% END %]
+ </select>
+ </label>
+ </td>
+
+ <td style="text-align: center; vertical-align: middle;">
+ <input type="submit" name="action-add" value="Add"
+ style="width: 3em;"><br>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ <script>
+ document.chartform.category[0].selected = true;
+ catSelected();
+ subcatSelected();
+ </script>
+</form>
+
+[% IF UserInGroup('editbugs') %]
+ <h3><a href="query.cgi?format=create-series">New Data Set</a></h3>
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/reports/edit-series.html.tmpl b/template/en/default/reports/edit-series.html.tmpl
new file mode 100644
index 000000000..352e5fade
--- /dev/null
+++ b/template/en/default/reports/edit-series.html.tmpl
@@ -0,0 +1,57 @@
+<!-- 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>
+ #%]
+
+[% title = "Edit Series" %]
+[% h2 = BLOCK %]
+ [% default.category FILTER html %] /
+ [%+ default.subcategory FILTER html %] /
+ [%+ default.name FILTER html %]
+[% END %]
+
+[% PROCESS global/header.html.tmpl %]
+
+<form method="get" action="chart.cgi" name="chartform">
+
+ [% button_name = "Change" %]
+
+ [% PROCESS reports/series.html.tmpl %]
+
+ [% IF default.series_id %]
+ <input type="hidden" name="series_id" value="[% default.series_id %]">
+ [% END %]
+</form>
+
+<p>
+ <b>Creator</b>: <a href="mailto:[% creator.email FILTER html %]">
+ [% creator.email FILTER html %]</a>
+</p>
+
+<p>
+ <a href="query.cgi?[% default.query FILTER html%]">View
+ series search parameters</a> |
+ <a href="buglist.cgi?cmdtype=dorem&amp;namedcmd=
+ [% default.category FILTER html %]-
+ [% default.subcategory FILTER html %]-
+ [% default.name FILTER html %]&amp;remaction=runseries&amp;series_id=
+ [% default.series_id %]">Run series search</a>
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/reports/menu.html.tmpl b/template/en/default/reports/menu.html.tmpl
index 4e21bf4d6..f28f1f697 100644
--- a/template/en/default/reports/menu.html.tmpl
+++ b/template/en/default/reports/menu.html.tmpl
@@ -58,10 +58,14 @@
<ul>
<li>
- <strong><a href="reports.cgi">Charts</a></strong> -
+ <strong><a href="reports.cgi">Old Charts</a></strong> -
plot the status and/or resolution of bugs against
time, for each product in your database.
</li>
+ <li>
+ <strong><a href="chart.cgi">New Charts</a></strong> -
+ plot any arbitrary search against time. Far more powerful.
+ </li>
</ul>
[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/reports/series-common.html.tmpl b/template/en/default/reports/series-common.html.tmpl
new file mode 100644
index 000000000..7fa34c6ec
--- /dev/null
+++ b/template/en/default/reports/series-common.html.tmpl
@@ -0,0 +1,117 @@
+<!-- 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:
+ # donames: boolean. True if we have a multi-select for names as well as
+ # categories and subcategories.
+ # category: hash (keyed by category) of hashes (keyed by subcategory) of
+ # hashes (keyed by name), with value being the series_id of the
+ # series. Contains details of all series the user can see.
+ #%]
+
+[% subcategory = category.${default.category} %]
+[% name = subcategory.${default.subcategory} %]
+
+<script>
+[%# This structure holds details of the series the user can select from. %]
+var series = {
+[% FOREACH c = category.keys.sort %]
+ "[%+ c FILTER js %]" : {
+ [% FOREACH s = category.$c.keys.sort %]
+ "[%+ s FILTER js %]" : {
+ [% IF donames %]
+ [% FOREACH n = category.$c.$s.keys.sort %]
+ "[% n FILTER js %]":
+ [% category.$c.$s.$n FILTER js %][% ", " UNLESS loop.last %]
+ [% END %]
+ [% END %]
+ }[% ", " UNLESS loop.last %]
+ [% END %]
+ }[% ", " UNLESS loop.last %]
+[% END %]
+};
+
+[%# Should attempt to preserve selection across invocations @@@ %]
+[%# This function takes necessary action on selection of a category %]
+function catSelected() {
+ var cat = document.chartform.category.value;
+ var subcats = series[cat];
+
+ var subcatwidget = document.chartform.subcategory;
+
+ subcatwidget.options.length = 0;
+ var i = 0;
+
+ for (x in subcats) {
+ subcatwidget.options[i] = new Option(x, x);
+ i++;
+ }
+
+ [% IF newtext %]
+ subcatwidget.options[i] = new Option("[% newtext FILTER js %]", "");
+ [% END %]
+
+ subcatwidget.options[0].selected = true;
+
+ if (document.chartform.action[1]) {
+ [%# On the query form, select the right radio button. %]
+ document.chartform.action[1].checked = true;
+ }
+
+ checkNewState();
+}
+
+[%# This function updates the disabled state of the two "new" textboxes %]
+function checkNewState() {
+ var fm = document.chartform;
+ if (fm.newcategory) {
+ fm.newcategory.disabled =
+ (fm.category.value != "" ||
+ fm.action[1] && fm.action[1].checked == false);
+ fm.newsubcategory.disabled =
+ (fm.subcategory.value != "" ||
+ fm.action[1] && fm.action[1].checked == false);
+ }
+}
+</script>
+
+[%###########################################################################%]
+[%# Block for SELECT fields - pinched from search/form.html.tmpl #%]
+[%###########################################################################%]
+
+[% BLOCK series_select %]
+ <td align="left">
+ <label for="[% sel.name %]" accesskey="[% sel.accesskey %]">
+ <select name="[% sel.name %]" id="[% sel.name %]"
+ size="[% sel.size %]" style="width: 15em"
+ [%+ "onchange='$sel.onchange'" IF sel.onchange %]>
+ [% FOREACH x = ${sel.name}.keys.sort %]
+ <option value="[% x FILTER html %]"
+ [% " selected" IF default.${sel.name} == x %]>
+ [% x FILTER html %]</option>
+ [% END %]
+ [% IF newtext %]
+ <option value="">[% newtext FILTER html %]</option>
+ [% END %]
+ </select>
+ </label>
+ </td>
+[% END %]
diff --git a/template/en/default/reports/series.html.tmpl b/template/en/default/reports/series.html.tmpl
new file mode 100644
index 000000000..a1474a1cf
--- /dev/null
+++ b/template/en/default/reports/series.html.tmpl
@@ -0,0 +1,96 @@
+<!-- 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:
+ # default: hash. Defaults for category, subcategory, name etc.
+ # button_name: string. What the button will say.
+ # category: hash (keyed by category) of hashes (keyed by subcategory) of
+ # hashes (keyed by name), with value being the series_id of the
+ # series. Contains details of all series the user can see.
+ #%]
+
+[% PROCESS "reports/series-common.html.tmpl"
+ newtext = "New (name below)"
+ %]
+
+<table cellpadding="2" cellspacing="2" border="0"
+ style="text-align: left; margin-left: 20px">
+ <tbody>
+ <tr>
+ <th>Category:</th>
+ <noscript><th></th></noscript>
+ <th>Sub-category:</th>
+ <th>Name:</th>
+ <td></td>
+ </tr>
+ <tr>
+ [% PROCESS series_select sel = { name => 'category',
+ size => 5,
+ onchange => "catSelected()" } %]
+ <noscript>
+ <td>
+ <input type="submit" name="action-edit" value="Update -->">
+ </td>
+ </noscript>
+
+ [% PROCESS series_select sel = { name => 'subcategory',
+ size => 5,
+ onchange => "checkNewState()" } %]
+
+ <td valign="top" name="name">
+ <input type="text" name="name" maxlength="64"
+ value="[% default.name.0 FILTER html %]" size="25">
+ </td>
+
+ <td valign="top">
+ <span style="font-weight: bold;">Run every</span> &nbsp;
+ <input type="text" size="2" name="frequency"
+ value="[% (default.frequency.0 OR 7) FILTER html %]">
+ <span style="font-weight: bold;">&nbsp;day(s)</span><br>
+ [% IF UserInGroup('admin') %]
+ <input type="checkbox" name="public"
+ [% "checked='checked'" IF default.public.0 %]>
+ <span style="font-weight: bold;">Visible to all</span>
+ [% END %]
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <input type="text" style="width: 100%" name="newcategory"
+ maxlength="64" value="[% default.newcategory.0 FILTER html %]">
+ </td>
+ <noscript><td></td></noscript>
+ <td>
+ <input type="text" style="width: 100%" name="newsubcategory"
+ maxlength="64"
+ value="[% default.newsubcategory.0 FILTER html %]">
+ </td>
+ <td></td>
+ <td>
+ <input type="submit" value="[% button_name FILTER html %]">
+ </td>
+ </tbody>
+</table>
+
+<script>
+ checkNewState();
+</script>
diff --git a/template/en/default/search/search-create-series.html.tmpl b/template/en/default/search/search-create-series.html.tmpl
new file mode 100644
index 000000000..9673a1838
--- /dev/null
+++ b/template/en/default/search/search-create-series.html.tmpl
@@ -0,0 +1,67 @@
+<!-- 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 search/form.html.tmpl, reports/series.html.tmpl and
+ # search/boolean-charts.html.tmpl.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Create New Data Set"
+ onload = "selectProduct(document.forms['chartform']);"
+%]
+
+[% button_name = "I'm Feeling Buggy" %]
+
+<form method="get" action="chart.cgi" name="chartform">
+
+[% PROCESS search/form.html.tmpl %]
+
+<table>
+ <tr>
+ <td>
+ <input type="radio" id="action-search"
+ name="action" value="search" checked="checked">
+ <label for="action-search">Run this search</label></td>
+ </tr>
+
+ <tr>
+ <td>
+ <input type="radio" id="action-create" name="action" value="create">
+ <label for="action-create">
+ Start recording bug count data for this search, as follows:
+ </label>
+ <br>
+
+ [% INCLUDE reports/series.html.tmpl %]
+
+ </td>
+ </tr>
+</table>
+
+<hr>
+
+[% PROCESS "search/boolean-charts.html.tmpl" %]
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]