зеркало из https://github.com/mozilla/pjs.git
352 строки
11 KiB
Perl
352 строки
11 KiB
Perl
# -*- 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;
|