~ubuntu-branches/ubuntu/oneiric/bugzilla/oneiric

« back to all changes in this revision

Viewing changes to Bugzilla/Chart.pm

  • Committer: Bazaar Package Importer
  • Author(s): Raphael Bossek
  • Date: 2008-06-27 22:34:34 UTC
  • mfrom: (1.1.7 upstream)
  • Revision ID: james.westby@ubuntu.com-20080627223434-0ib57vstn43bb4a3
Tags: 3.0.4.1-1
* Update of French, Russian and German translations. (closes: #488251)
* Added Bulgarian and Belarusian translations.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- Mode: perl; indent-tabs-mode: nil -*-
2
 
#
3
 
# The contents of this file are subject to the Mozilla Public
4
 
# License Version 1.1 (the "License"); you may not use this file
5
 
# except in compliance with the License. You may obtain a copy of
6
 
# the License at http://www.mozilla.org/MPL/
7
 
#
8
 
# Software distributed under the License is distributed on an "AS
9
 
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
10
 
# implied. See the License for the specific language governing
11
 
# rights and limitations under the License.
12
 
#
13
 
# The Original Code is the Bugzilla Bug Tracking System.
14
 
#
15
 
# The Initial Developer of the Original Code is Netscape Communications
16
 
# Corporation. Portions created by Netscape are
17
 
# Copyright (C) 1998 Netscape Communications Corporation. All
18
 
# Rights Reserved.
19
 
#
20
 
# Contributor(s): Gervase Markham <gerv@gerv.net>
21
 
#                 Albert Ting <altlst@sonic.net>
22
 
#                 A. Karl Kornel <karl@kornel.name>
23
 
 
24
 
use strict;
25
 
use lib ".";
26
 
 
27
 
# This module represents a chart.
28
 
#
29
 
# Note that it is perfectly legal for the 'lines' member variable of this
30
 
# class (which is an array of Bugzilla::Series objects) to have empty members
31
 
# in it. If this is true, the 'labels' array will also have empty members at
32
 
# the same points.
33
 
package Bugzilla::Chart;
34
 
 
35
 
use Bugzilla::Error;
36
 
use Bugzilla::Util;
37
 
use Bugzilla::Series;
38
 
 
39
 
use Date::Format;
40
 
use Date::Parse;
41
 
use List::Util qw(max);
42
 
 
43
 
sub new {
44
 
    my $invocant = shift;
45
 
    my $class = ref($invocant) || $invocant;
46
 
  
47
 
    # Create a ref to an empty hash and bless it
48
 
    my $self = {};
49
 
    bless($self, $class);
50
 
 
51
 
    if ($#_ == 0) {
52
 
        # Construct from a CGI object.
53
 
        $self->init($_[0]);
54
 
    } 
55
 
    else {
56
 
        die("CGI object not passed in - invalid number of args \($#_\)($_)");
57
 
    }
58
 
 
59
 
    return $self;
60
 
}
61
 
 
62
 
sub init {
63
 
    my $self = shift;
64
 
    my $cgi = shift;
65
 
 
66
 
    # The data structure is a list of lists (lines) of Series objects. 
67
 
    # There is a separate list for the labels.
68
 
    #
69
 
    # The URL encoding is:
70
 
    # line0=67&line0=73&line1=81&line2=67...
71
 
    # &label0=B+/+R+/+NEW&label1=...
72
 
    # &select0=1&select3=1...    
73
 
    # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html...
74
 
    # &gt=1&labelgt=Grand+Total    
75
 
    foreach my $param ($cgi->param()) {
76
 
        # Store all the lines
77
 
        if ($param =~ /^line(\d+)$/) {
78
 
            foreach my $series_id ($cgi->param($param)) {
79
 
                detaint_natural($series_id) 
80
 
                                     || ThrowCodeError("invalid_series_id");
81
 
                my $series = new Bugzilla::Series($series_id);
82
 
                push(@{$self->{'lines'}[$1]}, $series) if $series;
83
 
            }
84
 
        }
85
 
 
86
 
        # Store all the labels
87
 
        if ($param =~ /^label(\d+)$/) {
88
 
            $self->{'labels'}[$1] = $cgi->param($param);
89
 
        }        
90
 
    }
91
 
    
92
 
    # Store the miscellaneous metadata
93
 
    $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0;
94
 
    $self->{'gt'}       = $cgi->param('gt') ? 1 : 0;
95
 
    $self->{'labelgt'}  = $cgi->param('labelgt');
96
 
    $self->{'datefrom'} = $cgi->param('datefrom');
97
 
    $self->{'dateto'}   = $cgi->param('dateto');
98
 
    
99
 
    # If we are cumulating, a grand total makes no sense
100
 
    $self->{'gt'} = 0 if $self->{'cumulate'};
101
 
    
102
 
    # Make sure the dates are ones we are able to interpret
103
 
    foreach my $date ('datefrom', 'dateto') {
104
 
        if ($self->{$date}) {
105
 
            $self->{$date} = str2time($self->{$date}) 
106
 
              || ThrowUserError("illegal_date", { date => $self->{$date}});
107
 
        }
108
 
    }
109
 
 
110
 
    # datefrom can't be after dateto
111
 
    if ($self->{'datefrom'} && $self->{'dateto'} && 
112
 
        $self->{'datefrom'} > $self->{'dateto'}) 
113
 
    {
114
 
          ThrowUserError("misarranged_dates", 
115
 
                                         {'datefrom' => $cgi->param('datefrom'),
116
 
                                          'dateto' => $cgi->param('dateto')});
117
 
    }    
118
 
}
119
 
 
120
 
# Alter Chart so that the selected series are added to it.
121
 
sub add {
122
 
    my $self = shift;
123
 
    my @series_ids = @_;
124
 
 
125
 
    # Get the current size of the series; required for adding Grand Total later
126
 
    my $current_size = scalar($self->getSeriesIDs());
127
 
    
128
 
    # Count the number of added series
129
 
    my $added = 0;
130
 
    # Create new Series and push them on to the list of lines.
131
 
    # Note that new lines have no label; the display template is responsible
132
 
    # for inventing something sensible.
133
 
    foreach my $series_id (@series_ids) {
134
 
        my $series = new Bugzilla::Series($series_id);
135
 
        if ($series) {
136
 
            push(@{$self->{'lines'}}, [$series]);
137
 
            push(@{$self->{'labels'}}, "");
138
 
            $added++;
139
 
        }
140
 
    }
141
 
    
142
 
    # If we are going from < 2 to >= 2 series, add the Grand Total line.
143
 
    if (!$self->{'gt'}) {
144
 
        if ($current_size < 2 &&
145
 
            $current_size + $added >= 2) 
146
 
        {
147
 
            $self->{'gt'} = 1;
148
 
        }
149
 
    }
150
 
}
151
 
 
152
 
# Alter Chart so that the selections are removed from it.
153
 
sub remove {
154
 
    my $self = shift;
155
 
    my @line_ids = @_;
156
 
    
157
 
    foreach my $line_id (@line_ids) {
158
 
        if ($line_id == 65536) {
159
 
            # Magic value - delete Grand Total.
160
 
            $self->{'gt'} = 0;
161
 
        } 
162
 
        else {
163
 
            delete($self->{'lines'}->[$line_id]);
164
 
            delete($self->{'labels'}->[$line_id]);
165
 
        }
166
 
    }
167
 
}
168
 
 
169
 
# Alter Chart so that the selections are summed.
170
 
sub sum {
171
 
    my $self = shift;
172
 
    my @line_ids = @_;
173
 
    
174
 
    # We can't add the Grand Total to things.
175
 
    @line_ids = grep(!/^65536$/, @line_ids);
176
 
        
177
 
    # We can't add less than two things.
178
 
    return if scalar(@line_ids) < 2;
179
 
    
180
 
    my @series;
181
 
    my $label = "";
182
 
    my $biggestlength = 0;
183
 
    
184
 
    # We rescue the Series objects of all the series involved in the sum.
185
 
    foreach my $line_id (@line_ids) {
186
 
        my @line = @{$self->{'lines'}->[$line_id]};
187
 
        
188
 
        foreach my $series (@line) {
189
 
            push(@series, $series);
190
 
        }
191
 
        
192
 
        # We keep the label that labels the line with the most series.
193
 
        if (scalar(@line) > $biggestlength) {
194
 
            $biggestlength = scalar(@line);
195
 
            $label = $self->{'labels'}->[$line_id];
196
 
        }
197
 
    }
198
 
 
199
 
    $self->remove(@line_ids);
200
 
 
201
 
    push(@{$self->{'lines'}}, \@series);
202
 
    push(@{$self->{'labels'}}, $label);
203
 
}
204
 
 
205
 
sub data {
206
 
    my $self = shift;
207
 
    $self->{'_data'} ||= $self->readData();
208
 
    return $self->{'_data'};
209
 
}
210
 
 
211
 
# Convert the Chart's data into a plottable form in $self->{'_data'}.
212
 
sub readData {
213
 
    my $self = shift;
214
 
    my @data;
215
 
    my @maxvals;
216
 
 
217
 
    # Note: you get a bad image if getSeriesIDs returns nothing
218
 
    # We need to handle errors better.
219
 
    my $series_ids = join(",", $self->getSeriesIDs());
220
 
 
221
 
    return [] unless $series_ids;
222
 
 
223
 
    # Work out the date boundaries for our data.
224
 
    my $dbh = Bugzilla->dbh;
225
 
    
226
 
    # The date used is the one given if it's in a sensible range; otherwise,
227
 
    # it's the earliest or latest date in the database as appropriate.
228
 
    my $datefrom = $dbh->selectrow_array("SELECT MIN(series_date) " . 
229
 
                                         "FROM series_data " .
230
 
                                         "WHERE series_id IN ($series_ids)");
231
 
    $datefrom = str2time($datefrom);
232
 
 
233
 
    if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) {
234
 
        $datefrom = $self->{'datefrom'};
235
 
    }
236
 
 
237
 
    my $dateto = $dbh->selectrow_array("SELECT MAX(series_date) " . 
238
 
                                       "FROM series_data " .
239
 
                                       "WHERE series_id IN ($series_ids)");
240
 
    $dateto = str2time($dateto); 
241
 
 
242
 
    if ($self->{'dateto'} && $self->{'dateto'} < $dateto) {
243
 
        $dateto = $self->{'dateto'};
244
 
    }
245
 
 
246
 
    # Convert UNIX times back to a date format usable for SQL queries.
247
 
    my $sql_from = time2str('%Y-%m-%d', $datefrom);
248
 
    my $sql_to = time2str('%Y-%m-%d', $dateto);
249
 
 
250
 
    # Prepare the query which retrieves the data for each series
251
 
    my $query = "SELECT " . $dbh->sql_to_days('series_date') . " - " .
252
 
                            $dbh->sql_to_days('?') . ", series_value " .
253
 
                "FROM series_data " .
254
 
                "WHERE series_id = ? " .
255
 
                "AND series_date >= ?";
256
 
    if ($dateto) {
257
 
        $query .= " AND series_date <= ?";
258
 
    }
259
 
    
260
 
    my $sth = $dbh->prepare($query);
261
 
 
262
 
    my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef;
263
 
    my $line_index = 0;
264
 
 
265
 
    $maxvals[$gt_index] = 0 if $gt_index;
266
 
 
267
 
    my @datediff_total;
268
 
 
269
 
    foreach my $line (@{$self->{'lines'}}) {        
270
 
        # Even if we end up with no data, we need an empty arrayref to prevent
271
 
        # errors in the PNG-generating code
272
 
        $data[$line_index] = [];
273
 
        $maxvals[$line_index] = 0;
274
 
 
275
 
        foreach my $series (@$line) {
276
 
 
277
 
            # Get the data for this series and add it on
278
 
            if ($dateto) {
279
 
                $sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to);
280
 
            }
281
 
            else {
282
 
                $sth->execute($sql_from, $series->{'series_id'}, $sql_from);
283
 
            }
284
 
            my $points = $sth->fetchall_arrayref();
285
 
 
286
 
            foreach my $point (@$points) {
287
 
                my ($datediff, $value) = @$point;
288
 
                $data[$line_index][$datediff] ||= 0;
289
 
                $data[$line_index][$datediff] += $value;
290
 
                if ($data[$line_index][$datediff] > $maxvals[$line_index]) {
291
 
                    $maxvals[$line_index] = $data[$line_index][$datediff];
292
 
                }
293
 
 
294
 
                $datediff_total[$datediff] += $value;
295
 
 
296
 
                # Add to the grand total, if we are doing that
297
 
                if ($gt_index) {
298
 
                    $data[$gt_index][$datediff] ||= 0;
299
 
                    $data[$gt_index][$datediff] += $value;
300
 
                    if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) {
301
 
                        $maxvals[$gt_index] = $data[$gt_index][$datediff];
302
 
                    }
303
 
                }
304
 
            }
305
 
        }
306
 
 
307
 
        # We are done with the series making up this line, go to the next one
308
 
        $line_index++;
309
 
    }
310
 
 
311
 
    # calculate maximum y value
312
 
    if ($self->{'cumulate'}) {
313
 
        # Make sure we do not try to take the max of an array with undef values
314
 
        my @processed_datediff;
315
 
        while (@datediff_total) {
316
 
            my $datediff = shift @datediff_total;
317
 
            push @processed_datediff, $datediff if defined($datediff);
318
 
        }
319
 
        $self->{'y_max_value'} = max(@processed_datediff);
320
 
    }
321
 
    else {
322
 
        $self->{'y_max_value'} = max(@maxvals);
323
 
    }
324
 
    $self->{'y_max_value'} |= 1; # For log()
325
 
 
326
 
    # Align the max y value:
327
 
    #  For one- or two-digit numbers, increase y_max_value until divisible by 8
328
 
    #  For larger numbers, see the comments below to figure out what's going on
329
 
    if ($self->{'y_max_value'} < 100) {
330
 
        do {
331
 
            ++$self->{'y_max_value'};
332
 
        } while ($self->{'y_max_value'} % 8 != 0);
333
 
    }
334
 
    else {
335
 
        #  First, get the # of digits in the y_max_value
336
 
        my $num_digits = 1+int(log($self->{'y_max_value'})/log(10));
337
 
 
338
 
        # We want to zero out all but the top 2 digits
339
 
        my $mask_length = $num_digits - 2;
340
 
        $self->{'y_max_value'} /= 10**$mask_length;
341
 
        $self->{'y_max_value'} = int($self->{'y_max_value'});
342
 
        $self->{'y_max_value'} *= 10**$mask_length;
343
 
 
344
 
        # Add 10^$mask_length to the max value
345
 
        # Continue to increase until it's divisible by 8 * 10^($mask_length-1)
346
 
        # (Throwing in the -1 keeps at least the smallest digit at zero)
347
 
        do {
348
 
            $self->{'y_max_value'} += 10**$mask_length;
349
 
        } while ($self->{'y_max_value'} % (8*(10**($mask_length-1))) != 0);
350
 
    }
351
 
 
352
 
        
353
 
    # Add the x-axis labels into the data structure
354
 
    my $date_progression = generateDateProgression($datefrom, $dateto);
355
 
    unshift(@data, $date_progression);
356
 
 
357
 
    if ($self->{'gt'}) {
358
 
        # Add Grand Total to label list
359
 
        push(@{$self->{'labels'}}, $self->{'labelgt'});
360
 
 
361
 
        $data[$gt_index] ||= [];
362
 
    }
363
 
 
364
 
    return \@data;
365
 
}
366
 
 
367
 
# Flatten the data structure into a list of series_ids
368
 
sub getSeriesIDs {
369
 
    my $self = shift;
370
 
    my @series_ids;
371
 
 
372
 
    foreach my $line (@{$self->{'lines'}}) {
373
 
        foreach my $series (@$line) {
374
 
            push(@series_ids, $series->{'series_id'});
375
 
        }
376
 
    }
377
 
 
378
 
    return @series_ids;
379
 
}
380
 
 
381
 
# Class method to get the data necessary to populate the "select series"
382
 
# widgets on various pages.
383
 
sub getVisibleSeries {
384
 
    my %cats;
385
 
 
386
 
    # List of groups the user is in; use -1 to make sure it's not empty.
387
 
    my $grouplist = join(", ", (-1, values(%{Bugzilla->user->groups})));
388
 
    
389
 
    # Get all visible series
390
 
    my $dbh = Bugzilla->dbh;
391
 
    my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " .
392
 
                        "series.name, series.series_id " .
393
 
                        "FROM series " .
394
 
                        "INNER JOIN series_categories AS cc1 " .
395
 
                        "    ON series.category = cc1.id " .
396
 
                        "INNER JOIN series_categories AS cc2 " .
397
 
                        "    ON series.subcategory = cc2.id " .
398
 
                        "LEFT JOIN category_group_map AS cgm " .
399
 
                        "    ON series.category = cgm.category_id " .
400
 
                        "    AND cgm.group_id NOT IN($grouplist) " .
401
 
                        "WHERE creator = " . Bugzilla->user->id . " OR " .
402
 
                        "      cgm.category_id IS NULL " . 
403
 
                   $dbh->sql_group_by('series.series_id', 'cc1.name, cc2.name, ' .
404
 
                                      'series.name'));
405
 
    foreach my $series (@$serieses) {
406
 
        my ($cat, $subcat, $name, $series_id) = @$series;
407
 
        $cats{$cat}{$subcat}{$name} = $series_id;
408
 
    }
409
 
 
410
 
    return \%cats;
411
 
}
412
 
 
413
 
sub generateDateProgression {
414
 
    my ($datefrom, $dateto) = @_;
415
 
    my @progression;
416
 
 
417
 
    $dateto = $dateto || time();
418
 
    my $oneday = 60 * 60 * 24;
419
 
 
420
 
    # When the from and to dates are converted by str2time(), you end up with
421
 
    # a time figure representing midnight at the beginning of that day. We
422
 
    # adjust the times by 1/3 and 2/3 of a day respectively to prevent
423
 
    # edge conditions in time2str().
424
 
    $datefrom += $oneday / 3;
425
 
    $dateto += (2 * $oneday) / 3;
426
 
 
427
 
    while ($datefrom < $dateto) {
428
 
        push (@progression, time2str("%Y-%m-%d", $datefrom));
429
 
        $datefrom += $oneday;
430
 
    }
431
 
 
432
 
    return \@progression;
433
 
}
434
 
 
435
 
sub dump {
436
 
    my $self = shift;
437
 
 
438
 
    # Make sure we've read in our data
439
 
    my $data = $self->data;
440
 
    
441
 
    require Data::Dumper;
442
 
    print "<pre>Bugzilla::Chart object:\n";
443
 
    print Data::Dumper::Dumper($self);
444
 
    print "</pre>";
445
 
}
446
 
 
447
 
1;