From 4f6b75a65628b0d86c760309dd81dd03f5c6d308 Mon Sep 17 00:00:00 2001 From: "gerv%gerv.net" <> Date: Thu, 26 Jun 2003 06:22:50 +0000 Subject: Bug 16009 - generic charting. Patch by gerv; r,a=justdave. --- Bugzilla/Chart.pm | 351 +++++++++++++++++++++ Bugzilla/Series.pm | 262 +++++++++++++++ Bugzilla/Template.pm | 7 + buglist.cgi | 18 ++ chart.cgi | 312 ++++++++++++++++++ checksetup.pl | 142 +++++++++ collectstats.pl | 73 +++++ editcomponents.cgi | 30 ++ editproducts.cgi | 39 ++- query.cgi | 9 +- template/en/default/filterexceptions.pl | 31 ++ template/en/default/global/code-error.html.tmpl | 11 + template/en/default/global/messages.html.tmpl | 23 ++ template/en/default/global/user-error.html.tmpl | 42 ++- template/en/default/reports/chart.csv.tmpl | 40 +++ template/en/default/reports/chart.html.tmpl | 66 ++++ template/en/default/reports/chart.png.tmpl | 56 ++++ template/en/default/reports/create-chart.html.tmpl | 281 +++++++++++++++++ template/en/default/reports/edit-series.html.tmpl | 57 ++++ template/en/default/reports/menu.html.tmpl | 6 +- .../en/default/reports/series-common.html.tmpl | 117 +++++++ template/en/default/reports/series.html.tmpl | 96 ++++++ .../default/search/search-create-series.html.tmpl | 67 ++++ 23 files changed, 2130 insertions(+), 6 deletions(-) create mode 100644 Bugzilla/Chart.pm create mode 100644 Bugzilla/Series.pm create mode 100755 chart.cgi create mode 100644 template/en/default/reports/chart.csv.tmpl create mode 100644 template/en/default/reports/chart.html.tmpl create mode 100644 template/en/default/reports/chart.png.tmpl create mode 100644 template/en/default/reports/create-chart.html.tmpl create mode 100644 template/en/default/reports/edit-series.html.tmpl create mode 100644 template/en/default/reports/series-common.html.tmpl create mode 100644 template/en/default/reports/series.html.tmpl create mode 100644 template/en/default/search/search-create-series.html.tmpl 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 + +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... + # >=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 "
Bugzilla::Chart object:\n";
+    print Data::Dumper::Dumper($self);
+    print "
"; +} + +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 + +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 + +# 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-". 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 # Bradley Baetz # Tobias Burnus +# Gervase Markham # # # 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 () { + 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 + # 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 "\n
\n"; print "\n"; print "\n"; + print "\n"; + print "\n"; print ""; 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 "\n
\n"; print "\n"; print "\n"; + print "\n"; + print "\n"; print ""; 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 @@ Back to flag types.

+ [% ELSIF message_tag == "series_already_exists" %] + [% title = "Series Already Exists" %] + A series [% series.category FILTER html %] / + [%+ series.subcategory FILTER html %] / + [%+ series.name FILTER html %] + already exists. If you want to create this series, you will need to give + it a different name. @@@ subscribe? +

+ Go back or + create another series. + + [% ELSIF message_tag == "series_created" %] + [% title = "Series Created" %] + The series [% series.category FILTER html %] / + [%+ series.subcategory FILTER html %] / + [%+ series.name FILTER html %] + 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. +

+ Go back or + create another series. + [% 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 [% value FILTER html %], which isn't. [% ELSIF error == "illegal_date" %] - [% title = "Your Query Makes No Sense" %] + [% title = "Illegal Date" %] '[% date FILTER html %]' is not a legal date. [% ELSIF error == "illegal_email_address" %] @@ -266,6 +266,11 @@ It must also not contain any of these special characters: \ ( ) & < > , ; : " [ ], 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 " @@ -282,6 +287,18 @@ The name of your query cannot contain any of the following characters: <, >, &. + [% 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 [% email FILTER html %]. + [% 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 [% queryname FILTER html %] 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 + #%] + +[% 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 @@ + +[%# 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 + #%] + +[%# INTERFACE: + #%] + +[% DEFAULT width = 600 + height = 350 +%] + +[% PROCESS global/header.html.tmpl + title = "Chart" + h3 = time2str("%Y-%m-%d %H:%M:%S", time) +%] + +
+ + [% imageurl = BLOCK %]chart.cgi? + [% imagebase FILTER html %]&ctype=png&action=plot&width= + [% width %]&height=[% height -%] + [% END %] + + Graphical report results +

+ [% sizeurl = BLOCK %]chart.cgi? + [% imagebase FILTER html %]&action=wrap + [% END %] + Taller
+ Thinner * + Fatter    
+ Shorter
+

+ +

+ CSV | + Edit + this chart +

+ +
+ +[% 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 + #%] + +[% 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 @@ + +[%# 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 + #%] + +[%# 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 +%] + + + +[% gttext = "Grand Total" %] + +

Current Data Sets:

+ +
+ [% IF chart.lines.size > 0 %] + + + + + + + + + + + [%# 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 %] + + [% IF loop.first %] + + + [% END %] + + + + + + + + + + [% END %] + [% labelidx = labelidx + 1 %] + [% newidx = newidx + 1 %] + [% END %] + + [% IF chart.gt %] + + + + + + + + + [% END %] + + + + + + + + + + + + + + + + +
SelectAsData SetSubs
+ + + + + [% "{" IF line.size > 1 %] + + + [% series.category FILTER html %] / + [%+ series.subcategory FILTER html %] / + [%+ series.name FILTER html %] + + + + [% IF series.creator != 0 %] + [% IF series.subscribed %] + + [% ELSE %] + + [% END %] + [% END %] + + [% IF user.userid == series.creator OR UserInGroup("admin") %] + Edit + [% END %] +
+ + + + + + [% gttext FILTER html %] +
 
+
+ +
+ Cumulate: + + + Date Range: + + to + + + + +
+ [% ELSE %] +

None

+ [% END %] + +

Select Data Sets:

+ + + [% IF NOT category OR category.size == 0 %] + + + + [% ELSE %] + + + + + + + + + + + [% PROCESS series_select sel = { name => 'category', + size => 5, + onchange = "catSelected(); + subcatSelected();" } %] + + + + + [% PROCESS series_select sel = { name => 'subcategory', + size => 5, + onchange = "subcatSelected()" } %] + + + + + + + + + [% END %] +
+ You do not have permissions to see any data sets, or none + exist. +
Category:Sub-category:Name:
+
+ + + + + + +
+
+ + +
+ +[% IF UserInGroup('editbugs') %] +

New Data Set

+[% 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 @@ + +[%# 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 + #%] + +[% title = "Edit Series" %] +[% h2 = BLOCK %] + [% default.category FILTER html %] / + [%+ default.subcategory FILTER html %] / + [%+ default.name FILTER html %] +[% END %] + +[% PROCESS global/header.html.tmpl %] + +
+ + [% button_name = "Change" %] + + [% PROCESS reports/series.html.tmpl %] + + [% IF default.series_id %] + + [% END %] +
+ +

+ Creator: + [% creator.email FILTER html %] +

+ +

+ View + series search parameters | + Run series search +

+ +[% 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 @@
  • - Charts - + Old Charts - plot the status and/or resolution of bugs against time, for each product in your database.
  • +
  • + New Charts - + plot any arbitrary search against time. Far more powerful. +
[% 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 @@ + +[%# 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 + #%] + +[%# 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} %] + + + +[%###########################################################################%] +[%# Block for SELECT fields - pinched from search/form.html.tmpl #%] +[%###########################################################################%] + +[% BLOCK series_select %] + + + +[% 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 @@ + +[%# 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 + #%] + +[%# 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)" + %] + + + + + + + + + + + + [% PROCESS series_select sel = { name => 'category', + size => 5, + onchange => "catSelected()" } %] + + + + [% PROCESS series_select sel = { name => 'subcategory', + size => 5, + onchange => "checkNewState()" } %] + + + + + + + + + + + + + +
Category:Sub-category:Name:
+ + + + + Run every   + +  day(s)
+ [% IF UserInGroup('admin') %] + + Visible to all + [% END %] +
+ + + + + +
+ + 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 @@ + +[%# 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 + #%] + +[%# 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" %] + +
+ +[% PROCESS search/form.html.tmpl %] + + + + + + + + + +
+ +
+ + +
+ + [% INCLUDE reports/series.html.tmpl %] + +
+ +
+ +[% PROCESS "search/boolean-charts.html.tmpl" %] + +
+ +[% PROCESS global/footer.html.tmpl %] -- cgit v1.2.3-24-g4f1b