зеркало из https://github.com/mozilla/gecko-dev.git
Bug 16009 - generic charting. Patch by gerv; r,a=justdave.
This commit is contained in:
Родитель
33e4a9a297
Коммит
a5a1140ff7
|
@ -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...
|
||||
# >=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;
|
|
@ -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;
|
|
@ -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 } =
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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.
|
||||
#
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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' => [
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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") %]
|
||||
|
|
|
@ -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>\ ( ) & < > , ; : " [ ]</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 "
|
||||
|
@ -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
|
||||
<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" %]
|
||||
|
|
|
@ -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 %]
|
|
@ -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 %]&ctype=png&action=plot&width=
|
||||
[% width %]&height=[% height -%]
|
||||
[% END %]
|
||||
|
||||
<img alt="Graphical report results" src="[% imageurl %]"
|
||||
width="[% width %]" height="[% height %]">
|
||||
<p>
|
||||
[% sizeurl = BLOCK %]chart.cgi?
|
||||
[% imagebase FILTER html %]&action=wrap
|
||||
[% END %]
|
||||
<a href="[% sizeurl %]&width=[% width %]&height=
|
||||
[% height + 100 %]">Taller</a><br>
|
||||
<a href="[% sizeurl %]&width=[% width - 100 %]&height=
|
||||
[% height %]">Thinner</a> *
|
||||
<a href="[% sizeurl %]&width=[% width + 100 %]&height=
|
||||
[% height %]">Fatter</a> <br>
|
||||
<a href="[% sizeurl %]&width=[% width %]&height=
|
||||
[% height - 100 %]">Shorter</a><br>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="chart.cgi?
|
||||
[% imagebase FILTER html %]&ctype=csv&action=plot">CSV</a> |
|
||||
<a href="chart.cgi?[% imagebase FILTER html %]&action=assemble">Edit
|
||||
this chart</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
[% PROCESS global/footer.html.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;
|
||||
-%]
|
||||
|
|
@ -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&namedcmd=
|
||||
[% series.category FILTER html %]-
|
||||
[% series.subcategory FILTER html %]-
|
||||
[% series.name FILTER html -%]&series_id=
|
||||
[% series.series_id %]&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"> </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 %]
|
|
@ -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&namedcmd=
|
||||
[% default.category FILTER html %]-
|
||||
[% default.subcategory FILTER html %]-
|
||||
[% default.name FILTER html %]&remaction=runseries&series_id=
|
||||
[% default.series_id %]">Run series search</a>
|
||||
</p>
|
||||
|
||||
[% PROCESS global/footer.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 %]
|
||||
|
|
|
@ -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 %]
|
|
@ -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>
|
||||
<input type="text" size="2" name="frequency"
|
||||
value="[% (default.frequency.0 OR 7) FILTER html %]">
|
||||
<span style="font-weight: bold;"> 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>
|
|
@ -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 %]
|
Загрузка…
Ссылка в новой задаче