]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/reporter/clark-kent.pl
fb55b1169611c229522f1911a80647ce2be61233
[Evergreen.git] / Open-ILS / src / reporter / clark-kent.pl
1 #!/usr/bin/perl -w
2
3 use strict;
4 use diagnostics;
5 use DBI;
6 use FileHandle;
7 use XML::LibXML;
8 use Getopt::Long;
9 use DateTime;
10 use DateTime::Format::ISO8601;
11 use JSON;
12 use Data::Dumper;
13 use OpenILS::WWW::Reporter::transforms;
14 use Text::CSV_XS;
15 use Spreadsheet::WriteExcel;
16 use OpenSRF::EX qw/:try/;
17 use OpenSRF::Utils qw/:daemon/;
18 use OpenSRF::Utils::Logger qw/:level/;
19 use POSIX;
20 use GD::Graph::pie;
21 use GD::Graph::bars3d;
22 use GD::Graph::lines3d;
23
24 use open ':utf8';
25
26
27 my ($base_xml, $count, $daemon) = ('/openils/conf/reporter.xml', 1);
28
29 GetOptions(
30         "file=s"        => \$base_xml,
31         "daemon"        => \$daemon,
32         "concurrency=i" => \$count,
33 );
34
35 my $parser = XML::LibXML->new;
36 $parser->expand_xinclude(1);
37
38 my $doc = $parser->parse_file($base_xml);
39
40 my $db_driver = $doc->findvalue('/reporter/setup/database/driver');
41 my $db_host = $doc->findvalue('/reporter/setup/database/host');
42 my $db_port = $doc->findvalue('/reporter/setup/database/port') || '5432';
43 my $db_name = $doc->findvalue('/reporter/setup/database/name');
44 my $db_user = $doc->findvalue('/reporter/setup/database/user');
45 my $db_pw = $doc->findvalue('/reporter/setup/database/password');
46
47 my $dsn = "dbi:" . $db_driver . ":dbname=" . $db_name .';host=' . $db_host . ';port=' . $db_port;
48
49 my ($dbh,$running,$sth,@reports,$run, $current_time);
50
51 daemonize("Clark Kent, waiting for trouble") if ($daemon);
52
53 DAEMON:
54
55 $dbh = DBI->connect($dsn,$db_user,$db_pw, {pg_enable_utf8 => 1, RaiseError => 1});
56
57 $current_time = DateTime->from_epoch( epoch => time() )->strftime('%FT%T%z');
58 # Move new reports into the run queue
59 $dbh->do(<<'SQL', {}, $current_time);
60 INSERT INTO reporter.output ( stage3, state ) 
61         SELECT  id, 'wait'
62           FROM  reporter.stage3 
63           WHERE runtime <= $1
64                 AND NOT disable
65                 AND (   (       recurrence = '0 seconds'::INTERVAL
66                                 AND (
67                                         id NOT IN ( SELECT stage3 FROM reporter.output )
68                                         OR rerun IS TRUE
69                                 )
70                         )
71                         OR (    recurrence > '0 seconds'::INTERVAL
72                                 AND id NOT IN (
73                                         SELECT  stage3
74                                           FROM  reporter.output
75                                           WHERE state <> 'complete')
76                         )
77                 )
78           ORDER BY runtime;
79 SQL
80
81 # make sure we're not already running $count reports
82 ($running) = $dbh->selectrow_array(<<SQL);
83 SELECT  count(*)
84   FROM  reporter.output
85   WHERE state = 'running';
86 SQL
87
88 if ($count <= $running) {
89         if ($daemon) {
90                 $dbh->disconnect;
91                 sleep 1;
92                 POSIX::waitpid( -1, POSIX::WNOHANG );
93                 sleep 60;
94                 goto DAEMON;
95         }
96         print "Already running maximum ($running) concurrent reports\n";
97         exit 1;
98 }
99
100 # if we have some open slots then generate the sql
101 $run = $count - $running;
102
103 $sth = $dbh->prepare(<<SQL);
104 SELECT  *
105   FROM  reporter.output
106   WHERE state = 'wait'
107   ORDER BY queue_time
108   LIMIT $run;
109 SQL
110
111 $sth->execute;
112
113 @reports = ();
114 while (my $r = $sth->fetchrow_hashref) {
115         my $s3 = $dbh->selectrow_hashref(<<"    SQL", {}, $r->{stage3});
116                 SELECT * FROM reporter.stage3 WHERE id = ?;
117         SQL
118
119         my $s2 = $dbh->selectrow_hashref(<<"    SQL", {}, $s3->{stage2});
120                 SELECT * FROM reporter.stage2 WHERE id = ?;
121         SQL
122
123         $s3->{stage2} = $s2;
124         $r->{stage3} = $s3;
125
126         generate_query( $r );
127         push @reports, $r;
128 }
129
130 $sth->finish;
131
132 $dbh->disconnect;
133
134 # Now we spaun the report runners
135
136 for my $r ( @reports ) {
137         next if (safe_fork());
138
139         # This is the child (runner) process;
140         my $p = JSON->JSON2perl( $r->{stage3}->{params} );
141         daemonize("Clark Kent reporting: $p->{reportname}");
142
143         $dbh = DBI->connect($dsn,$db_user,$db_pw, {pg_enable_utf8 => 1, RaiseError => 1});
144
145         try {
146                 $dbh->do(<<'            SQL',{}, $r->{sql}->{'select'}, $$, $r->{id});
147                         UPDATE  reporter.output
148                           SET   state = 'running',
149                                 run_time = 'now',
150                                 query = ?,
151                                 run_pid = ?
152                           WHERE id = ?;
153                 SQL
154
155                 my ($runtime) = $dbh->selectrow_array("SELECT run_time FROM reporter.output WHERE id = ?",{},$r->{id});
156                 $r->{run_time} = $runtime;
157
158                 $sth = $dbh->prepare($r->{sql}->{'select'});
159
160                 $sth->execute(@{ $r->{sql}->{'bind'} });
161                 $r->{data} = $sth->fetchall_arrayref;
162
163                 pivot_data($r);
164
165                 my $base = $doc->findvalue('/reporter/setup/files/output_base');
166                 my $s1 = $r->{stage3}->{stage2}->{stage1};
167                 my $s2 = $r->{stage3}->{stage2}->{id};
168                 my $s3 = $r->{stage3}->{id};
169                 my $output = $r->{id};
170
171                 mkdir($base);
172                 mkdir("$base/$s1");
173                 mkdir("$base/$s1/$s2");
174                 mkdir("$base/$s1/$s2/$s3");
175                 mkdir("$base/$s1/$s2/$s3/$output");
176         
177                 my @formats;
178                 if (ref $p->{output_format}) {
179                         @formats = @{ $p->{output_format} };
180                 } else {
181                         @formats = ( $p->{output_format} );
182                 }
183         
184                 if ( grep { $_ eq 'csv' } @formats ) {
185                         build_csv("$base/$s1/$s2/$s3/$output/report-data.csv", $r);
186                 }
187                 
188                 if ( grep { $_ eq 'excel' } @formats ) {
189                         build_excel("$base/$s1/$s2/$s3/$output/report-data.xls", $r);
190                 }
191                 
192                 if ( grep { $_ eq 'html' } @formats ) {
193                         mkdir("$base/$s1/$s2/$s3/$output/html");
194                         build_html("$base/$s1/$s2/$s3/$output/report-data.html", $r);
195                 }
196
197
198                 $dbh->begin_work;
199                 #$dbh->do(<<'           SQL',{}, $r->{run_time}, $r->{stage3}->{id});
200                 #       UPDATE  reporter.stage3
201                 #         SET   runtime = CAST(? AS TIMESTAMP WITH TIME ZONE) + recurrence
202                 #         WHERE id = ? AND recurrence > '0 seconds'::INTERVAL;
203                 #SQL
204                 $dbh->do(<<'            SQL',{}, $r->{stage3}->{id});
205                         UPDATE  reporter.stage3
206                           SET   runtime = runtime + recurrence
207                           WHERE id = ? AND recurrence > '0 seconds'::INTERVAL;
208                 SQL
209                 $dbh->do(<<'            SQL',{}, $r->{stage3}->{id});
210                         UPDATE  reporter.stage3
211                           SET   rerun = FALSE
212                           WHERE id = ? AND rerun = TRUE;
213                 SQL
214                 $dbh->do(<<'            SQL',{}, $r->{id});
215                         UPDATE  reporter.output
216                           SET   state = 'complete',
217                                 complete_time = 'now'
218                           WHERE id = ?;
219                 SQL
220                 $dbh->commit;
221
222
223         } otherwise {
224                 my $e = shift;
225                 $dbh->rollback;
226                 $dbh->do(<<'            SQL',{}, $e, $r->{id});
227                         UPDATE  reporter.output
228                           SET   state = 'error',
229                                 error_time = 'now',
230                                 error = ?,
231                                 run_pid = NULL
232                           WHERE id = ?;
233                 SQL
234         };
235
236         $dbh->disconnect;
237
238         exit; # leave the child
239 }
240
241 if ($daemon) {
242         sleep 1;
243         POSIX::waitpid( -1, POSIX::WNOHANG );
244         sleep 60;
245         goto DAEMON;
246 }
247
248 #-------------------------------------------------------------------
249
250 sub pivot_data {
251         my $r = shift;
252         my $p = JSON->JSON2perl( $r->{stage3}->{params} );
253         my $settings = $r->{sql};
254         my $data = $r->{data};
255
256         return unless (defined($settings->{pivot}));
257
258         my @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
259         my @values = (0 .. (scalar(@{$settings->{columns}}) - 1));
260         splice(@values,$_,1) for (reverse @groups);
261
262         # remove pivot from group-by
263         my $count = 0;
264         my $pivot_groupby;
265         while ($count < scalar(@{$settings->{groupby}})) {
266                 if (defined $pivot_groupby) {
267                         $settings->{groupby}->[$count] -= 1;
268                         if ($settings->{groupby}->[$count] >= $values[0] + 1) {
269                                 $settings->{groupby}->[$count] -= 1;
270                         }
271                 } elsif ($settings->{groupby}->[$count] == $settings->{pivot} + 1) {
272                         $pivot_groupby = $count;
273                 }
274                 $count++;
275         }
276
277
278         # grab positions of non-group-bys
279         @values = (0 .. (scalar(@{$settings->{columns}}) - 1));
280         splice(@values,$_,1) for (reverse @groups);
281         
282         # we're only doing one "value" for now, so grab that and remove from headings
283         my ($val_col) = @values;
284
285         my @remove_me = sort
286                 { $b <=> $a }
287                 ($val_col, $settings->{groupby}->[$pivot_groupby] - 1);
288
289         my %p_header;
290         for my $row (@$data) {
291                 $p_header{ $$row[$settings->{pivot}] } = [] unless exists($p_header{ $$row[$settings->{pivot}] });
292                 
293                 push @{ $p_header{ $$row[$settings->{pivot}] } }, $$row[$val_col];
294                 
295                 splice(@$row,$_,1) for (@remove_me);
296         }
297
298         push @{ $settings->{columns} }, sort keys %p_header;
299
300         # remove from headings;
301         splice(@{$settings->{columns}},$_,1) for (@remove_me);
302
303         # remove pivot from groupby
304         splice(@{$settings->{groupby}}, $pivot_groupby, 1);
305         @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
306
307         $count = scalar(keys %p_header);
308         my %seenit;
309         my @new_data;
310         {       no warnings;
311                 for my $row (@$data) {
312
313                         my $fingerprint = join('',@$row[@groups]);
314                         next if $seenit{$fingerprint};
315
316                         $seenit{$fingerprint}++;
317
318                         for my $h ( sort keys %p_header ) {
319                                 push @$row, shift(@{ $p_header{$h} });
320                         }
321
322                         push @new_data, [@$row];
323                 }
324         }
325
326         #replace old data with new
327         $r->{data} = \@new_data;
328
329 }
330
331 sub build_csv {
332         my $file = shift;
333         my $r = shift;
334
335         my $csv = Text::CSV_XS->new({ always_quote => 1, eol => "\015\012" });
336
337         return unless ($csv);
338         
339         my $f = new FileHandle (">$file");
340
341         $csv->print($f, $r->{sql}->{columns});
342         $csv->print($f, $_) for (@{$r->{data}});
343
344         $f->close;
345 }
346 sub build_excel {
347         my $file = shift;
348         my $r = shift;
349         my $p = JSON->JSON2perl( $r->{stage3}->{params} );
350
351         my $xls = Spreadsheet::WriteExcel->new($file);
352
353         my $sheetname = substr($p->{reportname},1,31);
354         $sheetname =~ s/\W/_/gos;
355         
356         my $sheet = $xls->add_worksheet($sheetname);
357
358         $sheet->write_row('A1', $r->{sql}->{columns});
359
360         $sheet->write_col('A2', $r->{data});
361
362         $xls->close;
363 }
364
365 sub build_html {
366         my $file = shift;
367         my $r = shift;
368         my $p = JSON->JSON2perl( $r->{stage3}->{params} );
369
370         my $index = new FileHandle (">$file");
371         my $raw = new FileHandle (">$file.raw.html");
372         
373         # index header
374         print $index <<"        HEADER";
375 <html>
376         <head>
377                 <title>$$p{reportname}</title>
378                 <style>
379                         table { border-collapse: collapse; }
380                         th { background-color: lightgray; }
381                         td,th { border: solid black 1px; }
382                         * { font-family: sans-serif; font-size: 10px; }
383                 </style>
384         </head>
385         <body>
386                 <h2><u>$$p{reportname}</u></h2>
387         HEADER
388
389         
390         # add a link to the raw output html
391         print $index "<a href='report-data.html.raw.html'>Raw output data</a><br/><br/><br/><br/>";
392
393         # create the raw output html file
394         print $raw "<html><head><title>$$p{reportname}</title>";
395
396         print $raw <<'  CSS';
397                 <style>
398                         table { border-collapse: collapse; }
399                         th { background-color: lightgray; }
400                         td,th { border: solid black 1px; }
401                         * { font-family: sans-serif; font-size: 10px; }
402                 </style>
403         CSS
404
405         print $raw "</head><body><table>";
406
407         {       no warnings;
408                 print $raw "<tr><th>".join('</th><th>',@{$r->{sql}->{columns}}).'</th></tr>';
409                 print $raw "<tr><td>".join('</td><td>',@$_                    ).'</td></tr>' for (@{$r->{data}});
410         }
411
412         print $raw '</table></body></html>';
413         
414         $raw->close;
415
416         # get the graph types
417         my @graphs;
418         if (ref $$p{html_graph_type}) {
419                 @graphs = @{ $$p{html_graph_type} };
420         } else {
421                 @graphs = ( $$p{html_graph_type} );
422         }
423
424         # Time for a pie chart
425         if (grep {$_ eq 'pie'} @graphs) {
426                 my $pics = draw_pie($r, $p, $file);
427                 for my $pic (@$pics) {
428                         print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
429                 }
430         }
431
432         print $index '<br/><br/><br/><br/>';
433         # Time for a bar chart
434         if (grep {$_ eq 'bar'} @graphs) {
435                 my $pics = draw_bars($r, $p, $file);
436                 for my $pic (@$pics) {
437                         print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
438                 }
439         }
440
441         print $index '<br/><br/><br/><br/>';
442         # Time for a bar chart
443         if (grep {$_ eq 'line'} @graphs) {
444                 my $pics = draw_lines($r, $p, $file);
445                 for my $pic (@$pics) {
446                         print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
447                 }
448         }
449
450
451         # and that's it!
452         print $index '</body></html>';
453         
454         $index->close;
455 }
456
457 sub draw_pie {
458         my $r = shift;
459         my $p = shift;
460         my $file = shift;
461         my $data = $r->{data};
462         my $settings = $r->{sql};
463
464         my @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
465         
466         my @values = (0 .. (scalar(@{$settings->{columns}}) - 1));
467         delete @values[@groups];
468
469         my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
470         
471         my @pics;
472         for my $vcol (@values) {
473                 next unless (defined $vcol);
474
475                 my @pic_data = ([],[]);
476                 for my $row (@$data) {
477                         next if (!defined($$row[$vcol]) || $$row[$vcol] == 0);
478                         my $val = $$row[$vcol];
479                         push @{$pic_data[0]}, join(" -- ", @$row[@groups])." ($val)";
480                         push @{$pic_data[1]}, $val;
481                 }
482
483                 next unless (@{$pic_data[0]});
484
485                 my $size = 300;
486                 my $split = int(scalar(@{$pic_data[0]}) / $size);
487                 my $last = scalar(@{$pic_data[0]}) % $size;
488
489                 for my $sub_graph (0 .. $split) {
490                         
491                         if ($sub_graph == $split) {
492                                 $size = $last;
493                         }
494
495                         my @sub_data;
496                         for my $set (@pic_data) {
497                                 push @sub_data, [ splice(@$set,0,$size) ];
498                         }
499
500                         my $pic = new GD::Graph::pie;
501
502                         $pic->set(
503                                 label                   => $settings->{columns}->[$vcol],
504                                 start_angle             => 180,
505                                 legend_placement        => 'R',
506                                 logo                    => $logo,
507                                 logo_position           => 'TL',
508                                 logo_resize             => 0.5,
509                                 show_values             => 1,
510                         );
511
512                         my $format = $pic->export_format;
513
514                         open(IMG, ">$file.pie.$vcol.$sub_graph.$format");
515                         binmode IMG;
516
517                         my $forgetit = 0;
518                         try {
519                                 $pic->plot(\@sub_data) or die $pic->error;
520                                 print IMG $pic->gd->$format;
521                         } otherwise {
522                                 my $e = shift;
523                                 warn "Couldn't draw $file.pie.$vcol.$sub_graph.$format : $e";
524                                 $forgetit = 1;
525                         };
526
527                         close IMG;
528
529
530                         push @pics,
531                                 { file => "pie.$vcol.$sub_graph.$format",
532                                   name => $settings->{columns}->[$vcol].' (Pie)',
533                                 } unless ($forgetit);
534
535                         last if ($sub_graph == $split);
536                 }
537
538         }
539         
540         return \@pics;
541 }
542
543 sub draw_bars {
544         my $r = shift;
545         my $p = shift;
546         my $file = shift;
547         my $data = $r->{data};
548         my $settings = $r->{sql};
549
550         my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
551
552         my @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
553
554         
555         my @values = (0 .. (scalar(@{$settings->{columns}}) - 1));
556         splice(@values,$_,1) for (reverse @groups);
557
558         my @pic_data;
559         {       no warnings;
560                 for my $row (@$data) {
561                         push @{$pic_data[0]}, join(' -- ', @$row[@groups]);
562                 }
563         }
564
565         my @leg;
566         my $set = 1;
567
568         my %trim_candidates;
569
570         my $max_y = 0;
571         for my $vcol (@values) {
572                 next unless (defined $vcol);
573
574
575                 my $pos = 0;
576                 for my $row (@$data) {
577                         my $val = $$row[$vcol] ? $$row[$vcol] : 0;
578                         push @{$pic_data[$set]}, $val;
579                         $max_y = $val if ($val > $max_y);
580                         $trim_candidates{$pos}++ if ($val == 0);
581                         $pos++;
582                 }
583
584                 $set++;
585         }
586         my $set_count = scalar(@pic_data) - 1;
587         my @trim_cols = grep { $trim_candidates{$_} == $set_count } keys %trim_candidates;
588
589         my @new_data;
590         my @use_me;
591         my @no_use;
592         my $set_index = 0;
593         for my $dataset (@pic_data) {
594                 splice(@$dataset,$_,1) for (sort { $b <=> $a } @trim_cols);
595
596                 if (grep { $_ } @$dataset) {
597                         push @new_data, $dataset;
598                         push @use_me, $set_index;
599                 } else {
600                         push @no_use, $set_index;
601                 }
602                 $set_index++;
603                 
604         }
605
606         return [] unless ($new_data[0] && @{$new_data[0]});
607
608         for my $col (@use_me) {
609                 push @leg, $settings->{columns}->[$col + @groups - 1] if (map { 1 } grep { $col == $_ } @values);
610         }
611
612         my $w = 100 + 10 * scalar(@{$new_data[0]});
613         $w = 400 if ($w < 400);
614
615         my $h = 10 * (scalar(@new_data) / 2);
616
617         $h = 0 if ($h < 0);
618
619         my $pic = new GD::Graph::bars3d ($w + 250, $h + 500);
620
621         $pic->set(
622                 title                   => $p->{reportname},
623                 x_labels_vertical       => 1,
624                 shading                 => 1,
625                 bar_depth               => 5,
626                 bar_spacing             => 2,
627                 y_max_value             => $max_y,
628                 legend_placement        => 'TR',
629                 boxclr                  => 'lgray',
630                 logo                    => $logo,
631                 logo_position           => 'R',
632                 logo_resize             => 0.5,
633                 show_values             => 1,
634                 overwrite               => 1,
635         );
636         $pic->set_legend(@leg);
637
638         my $format = $pic->export_format;
639
640         open(IMG, ">$file.bar.$format");
641         binmode IMG;
642
643         try {
644                 $pic->plot(\@new_data) or die $pic->error;
645                 print IMG $pic->gd->$format;
646         } otherwise {
647                 my $e = shift;
648                 warn "Couldn't draw $file.bar.$format : $e";
649         };
650
651         close IMG;
652
653         return [{ file => "bar.$format",
654                   name => $p->{reportname}.' (Bar)',
655                 }];
656
657 }
658
659 sub draw_lines {
660         my $r = shift;
661         my $p = shift;
662         my $file = shift;
663         my $data = $r->{data};
664         my $settings = $r->{sql};
665
666         my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
667
668         my @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
669
670         
671         my @values = (0 .. (scalar(@{$settings->{columns}}) - 1));
672         splice(@values,$_,1) for (reverse @groups);
673
674         my @pic_data;
675         {       no warnings;
676                 for my $row (@$data) {
677                         push @{$pic_data[0]}, join(' -- ', @$row[@groups]);
678                 }
679         }
680
681         my @leg;
682         my $set = 1;
683
684         my $max_y = 0;
685         for my $vcol (@values) {
686                 next unless (defined $vcol);
687
688
689                 for my $row (@$data) {
690                         my $val = $$row[$vcol] ? $$row[$vcol] : 0;
691                         push @{$pic_data[$set]}, $val;
692                         $max_y = $val if ($val > $max_y);
693                 }
694
695                 $set++;
696         }
697         my $set_count = scalar(@pic_data) - 1;
698
699         my @new_data;
700         my @use_me;
701         my @no_use;
702         my $set_index = 0;
703         for my $dataset (@pic_data) {
704
705                 if (grep { $_ } @$dataset) {
706                         push @new_data, $dataset;
707                         push @use_me, $set_index;
708                 } else {
709                         push @no_use, $set_index;
710                 }
711                 $set_index++;
712                 
713         }
714
715         for my $col (@use_me) {
716                 push @leg, $settings->{columns}->[$col + @groups - 1] if (map { 1 } grep { $col == $_ } @values);
717         }
718
719         my $w = 100 + 10 * scalar(@{$new_data[0]});
720         $w = 400 if ($w < 400);
721
722         my $h = 10 * (scalar(@new_data) / 2);
723
724         $h = 0 if ($h < 0);
725
726         my $pic = new GD::Graph::lines3d ($w + 250, $h + 500);
727
728         $pic->set(
729                 title                   => $p->{reportname},
730                 x_labels_vertical       => 1,
731                 shading                 => 1,
732                 line_depth              => 5,
733                 y_max_value             => $max_y,
734                 legend_placement        => 'TR',
735                 boxclr                  => 'lgray',
736                 logo                    => $logo,
737                 logo_position           => 'R',
738                 logo_resize             => 0.5,
739                 show_values             => 1,
740                 overwrite               => 1,
741         );
742         $pic->set_legend(@leg);
743
744         my $format = $pic->export_format;
745
746         open(IMG, ">$file.line.$format");
747         binmode IMG;
748
749         try {
750                 $pic->plot(\@new_data) or die $pic->error;
751                 print IMG $pic->gd->$format;
752         } otherwise {
753                 my $e = shift;
754                 warn "Couldn't draw $file.line.$format : $e";
755         };
756
757         close IMG;
758
759         return [{ file => "line.$format",
760                   name => $p->{reportname}.' (Bar)',
761                 }];
762
763 }
764
765 sub table_by_id {
766         my $id = shift;
767         my ($node) = $doc->findnodes("//*[\@id='$id']");
768         if ($node && $node->findvalue('@table')) {
769                 ($node) = $doc->findnodes("//*[\@id='".$node->getAttribute('table')."']");
770         }
771         return $node;
772 }
773
774 sub generate_query {
775         my $r = shift;
776
777         my $p = JSON->JSON2perl( $r->{stage3}->{params} );
778
779         my @group_by = ();
780         my @aggs = ();
781         my $core = $r->{stage3}->{stage2}->{stage1};
782         my @dims = ();
783
784         for my $t (keys %{$$p{filter}}) {
785                 if ($t ne $core) {
786                         push @dims, $t;
787                 }
788         }
789
790         for my $t (keys %{$$p{output}}) {
791                 if ($t ne $core && !(grep { $t eq $_ } @dims) ) {
792                         push @dims, $t;
793                 }
794         }
795
796         my @dim_select = ();
797         my @dim_from = ();
798         for my $d (@dims) {
799                 my $t = table_by_id($d);
800                 my $t_name = $t->findvalue('tablename');
801                 push @dim_from, "$t_name AS \"$d\""
802                         unless ( grep {$_ eq "$t_name AS \"$d\""} @dim_from );
803
804                 my $k = $doc->findvalue("//*[\@id='$d']/\@key");
805                 push @dim_select, "\"$d\".\"$k\" AS \"${d}_${k}\""
806                         unless ( grep {$_ eq "\"$d\".\"$k\" AS \"${d}_${k}\""} @dim_select );
807
808                 for my $c ( keys %{$$p{output}{$d}} ) {
809                         push @dim_select, "\"$d\".\"$c\" AS \"${d}_${c}\""
810                                 unless ( grep {$_ eq "\"$d\".\"$c\" AS \"${d}_${c}\""} @dim_select );
811                 }
812
813                 for my $c ( keys %{$$p{filter}{$d}} ) {
814                         next if (exists $$p{output}{$d}{$c});
815                         push @dim_select, "\"$d\".\"$c\" AS \"${d}_${c}\""
816                                 unless ( grep {$_ eq "\"$d\".\"$c\" AS \"${d}_${c}\""} @dim_select );
817                 }
818         }
819
820         my $d_select =
821                 '(SELECT ' . join(',', @dim_select) .
822                 '  FROM ' . join(',', @dim_from) . ') AS dims';
823         
824         my @opord = ();
825         if (ref $$p{output_order}) {
826                 @opord = @{ $$p{output_order} };
827         } else {
828                 @opord = ( $$p{output_order} );
829         }
830         my @output_order = map { { (split ':')[1] => (split ':')[2] } } @opord;
831         my @p_col = split(':',$p->{pivot_col}) if $p->{pivot_col};
832         my $pivot = undef;
833
834         my $col = 1;
835         my @groupby = ();
836         my @output = ();
837         my @dim_col_names = ();
838         my @columns = ();
839         my @join = ();
840         my @join_base = ();
841         for my $pair (@output_order) {
842                 my ($t_name) = keys %$pair;
843                 my $t = $t_name;
844
845                 $t_name = "dims" if ($t ne $core);
846
847                 my $t_node = table_by_id($t);
848
849                 for my $c ( values %$pair ) {
850                         my $label = $t_node->findvalue("fields/field[\@name='$c']/label");
851
852                         my $full_col = $c;
853                         $full_col = "${t}_${c}" if ($t ne $t_name);
854                         $full_col = "\"$t_name\".\"$full_col\"";
855
856                         if (my $xform_type = $$p{xform}{type}{$t}{$c}) {
857                                 my $xform = $$OpenILS::WWW::Reporter::dtype_xforms{$xform_type};
858                                 if ($xform->{group}) {
859                                         push @groupby, $col;
860                                 }
861                                 $label = "$$xform{label} -- $label";
862
863                                 my $tmp = $xform->{'select'};
864                                 $tmp =~ s/\?COLNAME\?/$full_col/gs;
865                                 $tmp =~ s/\?PARAM\?/$$p{xform}{param}{$t}{$c}/gs;
866                                 $full_col = $tmp;
867                         } else {
868                                 push @groupby, $col;
869                         }
870
871                         push @output, "$full_col AS \"$label\"";
872                         push @columns, $label;
873                         $pivot = scalar(@columns) - 1 if (@p_col && $t eq $p_col[1] && $c eq $p_col[2]);
874                         $col++;
875                 }
876
877                 if ($t ne $t_name && (!@join_base || !grep{$t eq $_}@join_base)) {
878                         my $k = $doc->findvalue("//*[\@id='$t']/\@key");
879                         my $f = $doc->findvalue("//*[\@id='$t']/\@field");
880                         push @join, "dims.\"${t}_${k}\" = \"$core\".\"$f\"";
881                         push @join_base, $t;
882                 }
883         }
884
885         my @where = ();
886         my @bind = ();
887         for my $t ( keys %{$$p{filter}} ) {
888                 my $t_name = $t;
889                 $t_name = "dims" if ($t ne $core);
890
891                 my $t_node = table_by_id($t);
892
893                 for my $c ( keys %{$$p{filter}{$t}} ) {
894                         my $label = $t_node->findvalue("fields/field[\@name='$c']/label");
895
896                         my $full_col = $c;
897                         $full_col = "${t}_${c}" if ($t ne $t_name);
898                         $full_col = "\"$t_name\".\"$full_col\"";
899
900                         my ($fam) = keys %{ $$p{filter}{$t}{$c} };
901                         my ($w) = keys %{ $$p{filter}{$t}{$c}{$fam} };
902                         my $val = $$p{filter}{$t}{$c}{$fam}{$w};
903
904                         my $filter_code_xpath = "/reporter/widgets/widget-family[\@name='$fam']/widget[\@name='$w']/filter-code[\@type='perl']";
905                         if (my $widget_code = $doc->findvalue($filter_code_xpath)) { # widget supplys it's own filter code
906                                 my ($where_clause, $bind_list) = ('',[]);
907
908                                 eval $widget_code;
909
910                                 die "$@\n\n$widget_code" if ($@);
911
912                                 push @where, $where_clause;
913                                 push @bind, @$bind_list;
914
915                         } elsif (ref $val) {
916                                 push @where, "$full_col IN (".join(",",map {'?'}@$val).")";
917                                 push @bind, @$val;
918                         } else {
919                                 push @where, "$full_col = ?";
920                                 push @bind, $val;
921                         }
922                 }
923
924                 if ($t ne $t_name && (!@join_base || !grep{$t eq $_}@join_base)) {
925                         my $k = $doc->findvalue("//*[\@id='$t']/\@key");
926                         my $f = $doc->findvalue("//*[\@id='$t']/\@field");
927                         push @join, "dims.\"${t}_${k}\" = \"$core\".\"$f\"";
928                         push @join_base, $t;
929                 }
930         }
931
932         my $t = table_by_id($core)->findvalue('tablename');
933
934         my $from = " FROM $t AS \"$core\" ";
935         $from .= "RIGHT JOIN $d_select ON (". join(' AND ', @join).")" if ( @join );
936
937         my $select =
938                 "SELECT ".join(',', @output). $from;
939
940         $select .= ' WHERE '.join(' AND ', @where) if (@where);
941         $select .= ' GROUP BY '.join(',',@groupby) if (@groupby);
942
943         $r->{sql}->{'pivot'}    = $pivot;
944         $r->{sql}->{'select'}   = $select;
945         $r->{sql}->{'bind'}     = \@bind;
946         $r->{sql}->{columns}    = \@columns;
947         $r->{sql}->{groupby}    = \@groupby;
948         
949 }
950
951
952
953
954
955