]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/reporter/clark-kent.pl
pivot bug fix (I think)
[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::Big;
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         # get the groups-to-be
290         my @temp_groupby = @{$settings->{groupby}};
291         splice(@temp_groupby, $pivot_groupby, 1);
292
293         @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
294
295         my %p_header;
296         for my $row (@$data) {
297                 $p_header{ $$row[$settings->{pivot}] } = [] unless exists($p_header{ $$row[$settings->{pivot}] });
298                 
299                 # add the header from this row's pivot
300                 push @{ $p_header{ $$row[$settings->{pivot}] } }, [ $$row[$val_col], join('',@$row[@temp_groups]) ];
301                 
302                 splice(@$row,$_,1) for (@remove_me);
303         }
304
305         push @{ $settings->{columns} }, sort keys %p_header;
306
307         # remove from headings;
308         splice(@{$settings->{columns}},$_,1) for (@remove_me);
309
310         # remove pivot from groupby
311         splice(@{$settings->{groupby}}, $pivot_groupby, 1);
312         @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
313
314         $count = scalar(keys %p_header);
315         my %seenit;
316         my @new_data;
317         {       no warnings;
318                 for my $row (@$data) {
319
320                         my $fingerprint = join('',@$row[@groups]);
321                         next if $seenit{$fingerprint};
322
323                         $seenit{$fingerprint}++;
324
325                         for my $h ( sort keys %p_header ) {
326                                 push @$row,  join('',@$row[@temp_groups]) eq $p_header{$h}[0][1] ?
327                                         shift(@{ $p_header{$h} }[0]) :
328                                         0;
329                         }
330
331                         push @new_data, [@$row];
332                 }
333         }
334
335         #replace old data with new
336         $r->{data} = \@new_data;
337
338 }
339
340 sub build_csv {
341         my $file = shift;
342         my $r = shift;
343
344         my $csv = Text::CSV_XS->new({ always_quote => 1, eol => "\015\012" });
345
346         return unless ($csv);
347         
348         my $f = new FileHandle (">$file");
349
350         $csv->print($f, $r->{sql}->{columns});
351         $csv->print($f, $_) for (@{$r->{data}});
352
353         $f->close;
354 }
355 sub build_excel {
356         my $file = shift;
357         my $r = shift;
358         my $p = JSON->JSON2perl( $r->{stage3}->{params} );
359
360         my $xls = Spreadsheet::WriteExcel::Big->new($file);
361
362         my $sheetname = substr($p->{reportname},1,31);
363         $sheetname =~ s/\W/_/gos;
364         
365         my $sheet = $xls->add_worksheet($sheetname);
366
367         $sheet->write_row('A1', $r->{sql}->{columns});
368
369         $sheet->write_col('A2', $r->{data});
370
371         $xls->close;
372 }
373
374 sub build_html {
375         my $file = shift;
376         my $r = shift;
377         my $p = JSON->JSON2perl( $r->{stage3}->{params} );
378
379         my $index = new FileHandle (">$file");
380         my $raw = new FileHandle (">$file.raw.html");
381         
382         # index header
383         print $index <<"        HEADER";
384 <html>
385         <head>
386                 <title>$$p{reportname}</title>
387                 <style>
388                         table { border-collapse: collapse; }
389                         th { background-color: lightgray; }
390                         td,th { border: solid black 1px; }
391                         * { font-family: sans-serif; font-size: 10px; }
392                 </style>
393         </head>
394         <body>
395                 <h2><u>$$p{reportname}</u></h2>
396         HEADER
397
398         
399         # add a link to the raw output html
400         print $index "<a href='report-data.html.raw.html'>Raw output data</a><br/><br/><br/><br/>";
401
402         # create the raw output html file
403         print $raw "<html><head><title>$$p{reportname}</title>";
404
405         print $raw <<'  CSS';
406                 <style>
407                         table { border-collapse: collapse; }
408                         th { background-color: lightgray; }
409                         td,th { border: solid black 1px; }
410                         * { font-family: sans-serif; font-size: 10px; }
411                 </style>
412         CSS
413
414         print $raw "</head><body><table>";
415
416         {       no warnings;
417                 print $raw "<tr><th>".join('</th><th>',@{$r->{sql}->{columns}}).'</th></tr>';
418                 print $raw "<tr><td>".join('</td><td>',@$_                    ).'</td></tr>' for (@{$r->{data}});
419         }
420
421         print $raw '</table></body></html>';
422         
423         $raw->close;
424
425         # get the graph types
426         my @graphs;
427         if (ref $$p{html_graph_type}) {
428                 @graphs = @{ $$p{html_graph_type} };
429         } else {
430                 @graphs = ( $$p{html_graph_type} );
431         }
432
433         if ($graphs[0]) {
434                 # Time for a pie chart
435                 if (grep {$_ eq 'pie'} @graphs) {
436                         my $pics = draw_pie($r, $p, $file);
437                         for my $pic (@$pics) {
438                                 print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
439                         }
440                 }
441
442                 print $index '<br/><br/><br/><br/>';
443                 # Time for a bar chart
444                 if (grep {$_ eq 'bar'} @graphs) {
445                         my $pics = draw_bars($r, $p, $file);
446                         for my $pic (@$pics) {
447                                 print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
448                         }
449                 }
450
451                 print $index '<br/><br/><br/><br/>';
452                 # Time for a bar chart
453                 if (grep {$_ eq 'line'} @graphs) {
454                         my $pics = draw_lines($r, $p, $file);
455                         for my $pic (@$pics) {
456                                 print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/><br/><br/><br/><br/>";
457                         }
458                 }
459         }
460
461         # and that's it!
462         print $index '</body></html>';
463         
464         $index->close;
465 }
466
467 sub draw_pie {
468         my $r = shift;
469         my $p = shift;
470         my $file = shift;
471         my $data = $r->{data};
472         my $settings = $r->{sql};
473
474         my @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
475         
476         my @values = (0 .. (scalar(@{$settings->{columns}}) - 1));
477         delete @values[@groups];
478
479         my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
480         
481         my @pics;
482         for my $vcol (@values) {
483                 next unless (defined $vcol);
484
485                 my @pic_data = ([],[]);
486                 for my $row (@$data) {
487                         next if (!defined($$row[$vcol]) || $$row[$vcol] == 0);
488                         my $val = $$row[$vcol];
489                         push @{$pic_data[0]}, join(" -- ", @$row[@groups])." ($val)";
490                         push @{$pic_data[1]}, $val;
491                 }
492
493                 next unless (@{$pic_data[0]});
494
495                 my $size = 300;
496                 my $split = int(scalar(@{$pic_data[0]}) / $size);
497                 my $last = scalar(@{$pic_data[0]}) % $size;
498
499                 for my $sub_graph (0 .. $split) {
500                         
501                         if ($sub_graph == $split) {
502                                 $size = $last;
503                         }
504
505                         my @sub_data;
506                         for my $set (@pic_data) {
507                                 push @sub_data, [ splice(@$set,0,$size) ];
508                         }
509
510                         my $pic = new GD::Graph::pie;
511
512                         $pic->set(
513                                 label                   => $settings->{columns}->[$vcol],
514                                 start_angle             => 180,
515                                 legend_placement        => 'R',
516                                 logo                    => $logo,
517                                 logo_position           => 'TL',
518                                 logo_resize             => 0.5,
519                                 show_values             => 1,
520                         );
521
522                         my $format = $pic->export_format;
523
524                         open(IMG, ">$file.pie.$vcol.$sub_graph.$format");
525                         binmode IMG;
526
527                         my $forgetit = 0;
528                         try {
529                                 $pic->plot(\@sub_data) or die $pic->error;
530                                 print IMG $pic->gd->$format;
531                         } otherwise {
532                                 my $e = shift;
533                                 warn "Couldn't draw $file.pie.$vcol.$sub_graph.$format : $e";
534                                 $forgetit = 1;
535                         };
536
537                         close IMG;
538
539
540                         push @pics,
541                                 { file => "pie.$vcol.$sub_graph.$format",
542                                   name => $settings->{columns}->[$vcol].' (Pie)',
543                                 } unless ($forgetit);
544
545                         last if ($sub_graph == $split);
546                 }
547
548         }
549         
550         return \@pics;
551 }
552
553 sub draw_bars {
554         my $r = shift;
555         my $p = shift;
556         my $file = shift;
557         my $data = $r->{data};
558         my $settings = $r->{sql};
559
560         my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
561
562         my @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
563
564         
565         my @values = (0 .. (scalar(@{$settings->{columns}}) - 1));
566         splice(@values,$_,1) for (reverse @groups);
567
568         my @pic_data;
569         {       no warnings;
570                 for my $row (@$data) {
571                         push @{$pic_data[0]}, join(' -- ', @$row[@groups]);
572                 }
573         }
574
575         my @leg;
576         my $set = 1;
577
578         my %trim_candidates;
579
580         my $max_y = 0;
581         for my $vcol (@values) {
582                 next unless (defined $vcol);
583
584
585                 my $pos = 0;
586                 for my $row (@$data) {
587                         my $val = $$row[$vcol] ? $$row[$vcol] : 0;
588                         push @{$pic_data[$set]}, $val;
589                         $max_y = $val if ($val > $max_y);
590                         $trim_candidates{$pos}++ if ($val == 0);
591                         $pos++;
592                 }
593
594                 $set++;
595         }
596         my $set_count = scalar(@pic_data) - 1;
597         my @trim_cols = grep { $trim_candidates{$_} == $set_count } keys %trim_candidates;
598
599         my @new_data;
600         my @use_me;
601         my @no_use;
602         my $set_index = 0;
603         for my $dataset (@pic_data) {
604                 splice(@$dataset,$_,1) for (sort { $b <=> $a } @trim_cols);
605
606                 if (grep { $_ } @$dataset) {
607                         push @new_data, $dataset;
608                         push @use_me, $set_index;
609                 } else {
610                         push @no_use, $set_index;
611                 }
612                 $set_index++;
613                 
614         }
615
616         return [] unless ($new_data[0] && @{$new_data[0]});
617
618         for my $col (@use_me) {
619                 push @leg, $settings->{columns}->[$col + @groups - 1] if (map { 1 } grep { $col == $_ } @values);
620         }
621
622         my $w = 100 + 10 * scalar(@{$new_data[0]});
623         $w = 400 if ($w < 400);
624
625         my $h = 10 * (scalar(@new_data) / 2);
626
627         $h = 0 if ($h < 0);
628
629         my $pic = new GD::Graph::bars3d ($w + 250, $h + 500);
630
631         $pic->set(
632                 title                   => $p->{reportname},
633                 x_labels_vertical       => 1,
634                 shading                 => 1,
635                 bar_depth               => 5,
636                 bar_spacing             => 2,
637                 y_max_value             => $max_y,
638                 legend_placement        => 'TR',
639                 boxclr                  => 'lgray',
640                 logo                    => $logo,
641                 logo_position           => 'R',
642                 logo_resize             => 0.5,
643                 show_values             => 1,
644                 overwrite               => 1,
645         );
646         $pic->set_legend(@leg);
647
648         my $format = $pic->export_format;
649
650         open(IMG, ">$file.bar.$format");
651         binmode IMG;
652
653         try {
654                 $pic->plot(\@new_data) or die $pic->error;
655                 print IMG $pic->gd->$format;
656         } otherwise {
657                 my $e = shift;
658                 warn "Couldn't draw $file.bar.$format : $e";
659         };
660
661         close IMG;
662
663         return [{ file => "bar.$format",
664                   name => $p->{reportname}.' (Bar)',
665                 }];
666
667 }
668
669 sub draw_lines {
670         my $r = shift;
671         my $p = shift;
672         my $file = shift;
673         my $data = $r->{data};
674         my $settings = $r->{sql};
675
676         my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
677
678         my @groups = (map { ($_ - 1) } @{ $settings->{groupby} });
679
680         
681         my @values = (0 .. (scalar(@{$settings->{columns}}) - 1));
682         splice(@values,$_,1) for (reverse @groups);
683
684         my @pic_data;
685         {       no warnings;
686                 for my $row (@$data) {
687                         push @{$pic_data[0]}, join(' -- ', @$row[@groups]);
688                 }
689         }
690
691         my @leg;
692         my $set = 1;
693
694         my $max_y = 0;
695         for my $vcol (@values) {
696                 next unless (defined $vcol);
697
698
699                 for my $row (@$data) {
700                         my $val = $$row[$vcol] ? $$row[$vcol] : 0;
701                         push @{$pic_data[$set]}, $val;
702                         $max_y = $val if ($val > $max_y);
703                 }
704
705                 $set++;
706         }
707         my $set_count = scalar(@pic_data) - 1;
708
709         my @new_data;
710         my @use_me;
711         my @no_use;
712         my $set_index = 0;
713         for my $dataset (@pic_data) {
714
715                 if (grep { $_ } @$dataset) {
716                         push @new_data, $dataset;
717                         push @use_me, $set_index;
718                 } else {
719                         push @no_use, $set_index;
720                 }
721                 $set_index++;
722                 
723         }
724
725         for my $col (@use_me) {
726                 push @leg, $settings->{columns}->[$col + @groups - 1] if (map { 1 } grep { $col == $_ } @values);
727         }
728
729         my $w = 100 + 10 * scalar(@{$new_data[0]});
730         $w = 400 if ($w < 400);
731
732         my $h = 10 * (scalar(@new_data) / 2);
733
734         $h = 0 if ($h < 0);
735
736         my $pic = new GD::Graph::lines3d ($w + 250, $h + 500);
737
738         $pic->set(
739                 title                   => $p->{reportname},
740                 x_labels_vertical       => 1,
741                 shading                 => 1,
742                 line_depth              => 5,
743                 y_max_value             => $max_y,
744                 legend_placement        => 'TR',
745                 boxclr                  => 'lgray',
746                 logo                    => $logo,
747                 logo_position           => 'R',
748                 logo_resize             => 0.5,
749                 show_values             => 1,
750                 overwrite               => 1,
751         );
752         $pic->set_legend(@leg);
753
754         my $format = $pic->export_format;
755
756         open(IMG, ">$file.line.$format");
757         binmode IMG;
758
759         try {
760                 $pic->plot(\@new_data) or die $pic->error;
761                 print IMG $pic->gd->$format;
762         } otherwise {
763                 my $e = shift;
764                 warn "Couldn't draw $file.line.$format : $e";
765         };
766
767         close IMG;
768
769         return [{ file => "line.$format",
770                   name => $p->{reportname}.' (Bar)',
771                 }];
772
773 }
774
775 sub table_by_id {
776         my $id = shift;
777         my ($node) = $doc->findnodes("//*[\@id='$id']");
778         if ($node && $node->findvalue('@table')) {
779                 ($node) = $doc->findnodes("//*[\@id='".$node->getAttribute('table')."']");
780         }
781         return $node;
782 }
783
784 sub generate_query {
785         my $r = shift;
786
787         my $p = JSON->JSON2perl( $r->{stage3}->{params} );
788
789         my @group_by = ();
790         my @aggs = ();
791         my $core = $r->{stage3}->{stage2}->{stage1};
792         my @dims = ();
793
794         for my $t (keys %{$$p{filter}}) {
795                 if ($t ne $core) {
796                         push @dims, $t;
797                 }
798         }
799
800         for my $t (keys %{$$p{output}}) {
801                 if ($t ne $core && !(grep { $t eq $_ } @dims) ) {
802                         push @dims, $t;
803                 }
804         }
805
806         my @dim_select = ();
807         my @dim_from = ();
808         for my $d (@dims) {
809                 my $t = table_by_id($d);
810                 my $t_name = $t->findvalue('tablename');
811                 push @dim_from, "$t_name AS \"$d\""
812                         unless ( grep {$_ eq "$t_name AS \"$d\""} @dim_from );
813
814                 my $k = $doc->findvalue("//*[\@id='$d']/\@key");
815                 push @dim_select, "\"$d\".\"$k\" AS \"${d}_${k}\""
816                         unless ( grep {$_ eq "\"$d\".\"$k\" AS \"${d}_${k}\""} @dim_select );
817
818                 for my $c ( keys %{$$p{output}{$d}} ) {
819                         push @dim_select, "\"$d\".\"$c\" AS \"${d}_${c}\""
820                                 unless ( grep {$_ eq "\"$d\".\"$c\" AS \"${d}_${c}\""} @dim_select );
821                 }
822
823                 for my $c ( keys %{$$p{filter}{$d}} ) {
824                         next if (exists $$p{output}{$d}{$c});
825                         push @dim_select, "\"$d\".\"$c\" AS \"${d}_${c}\""
826                                 unless ( grep {$_ eq "\"$d\".\"$c\" AS \"${d}_${c}\""} @dim_select );
827                 }
828         }
829
830         my $d_select =
831                 '(SELECT ' . join(',', @dim_select) .
832                 '  FROM ' . join(',', @dim_from) . ') AS dims';
833         
834         my @opord = ();
835         if (ref $$p{output_order}) {
836                 @opord = @{ $$p{output_order} };
837         } else {
838                 @opord = ( $$p{output_order} );
839         }
840         my @output_order = map { { (split ':')[1] => (split ':')[2] } } @opord;
841         my @p_col = split(':',$p->{pivot_col}) if $p->{pivot_col};
842         my $pivot = undef;
843
844         my $col = 1;
845         my @groupby = ();
846         my @output = ();
847         my @dim_col_names = ();
848         my @columns = ();
849         my @join = ();
850         my @join_base = ();
851         for my $pair (@output_order) {
852                 my ($t_name) = keys %$pair;
853                 my $t = $t_name;
854
855                 $t_name = "dims" if ($t ne $core);
856
857                 my $t_node = table_by_id($t);
858
859                 for my $c ( values %$pair ) {
860                         my $label = $t_node->findvalue("fields/field[\@name='$c']/label");
861
862                         my $full_col = $c;
863                         $full_col = "${t}_${c}" if ($t ne $t_name);
864                         $full_col = "\"$t_name\".\"$full_col\"";
865
866                         if (my $xform_type = $$p{xform}{type}{$t}{$c}) {
867                                 my $xform = $$OpenILS::WWW::Reporter::dtype_xforms{$xform_type};
868                                 if ($xform->{group}) {
869                                         push @groupby, $col;
870                                 }
871                                 $label = "$$xform{label} -- $label";
872
873                                 my $tmp = $xform->{'select'};
874                                 $tmp =~ s/\?COLNAME\?/$full_col/gs;
875                                 $tmp =~ s/\?PARAM\?/$$p{xform}{param}{$t}{$c}/gs;
876                                 $full_col = $tmp;
877                         } else {
878                                 push @groupby, $col;
879                         }
880
881                         push @output, "$full_col AS \"$label\"";
882                         push @columns, $label;
883                         $pivot = scalar(@columns) - 1 if (@p_col && $t eq $p_col[1] && $c eq $p_col[2]);
884                         $col++;
885                 }
886
887                 if ($t ne $t_name && (!@join_base || !grep{$t eq $_}@join_base)) {
888                         my $k = $doc->findvalue("//*[\@id='$t']/\@key");
889                         my $f = $doc->findvalue("//*[\@id='$t']/\@field");
890                         push @join, "dims.\"${t}_${k}\" = \"$core\".\"$f\"";
891                         push @join_base, $t;
892                 }
893         }
894
895         my @where = ();
896         my @bind = ();
897         for my $t ( keys %{$$p{filter}} ) {
898                 my $t_name = $t;
899                 $t_name = "dims" if ($t ne $core);
900
901                 my $t_node = table_by_id($t);
902
903                 for my $c ( keys %{$$p{filter}{$t}} ) {
904                         my $label = $t_node->findvalue("fields/field[\@name='$c']/label");
905
906                         my $full_col = $c;
907                         $full_col = "${t}_${c}" if ($t ne $t_name);
908                         $full_col = "\"$t_name\".\"$full_col\"";
909
910                         my ($fam) = keys %{ $$p{filter}{$t}{$c} };
911                         my ($w) = keys %{ $$p{filter}{$t}{$c}{$fam} };
912                         my $val = $$p{filter}{$t}{$c}{$fam}{$w};
913
914                         my $filter_code_xpath = "/reporter/widgets/widget-family[\@name='$fam']/widget[\@name='$w']/filter-code[\@type='perl']";
915                         if (my $widget_code = $doc->findvalue($filter_code_xpath)) { # widget supplys it's own filter code
916                                 my ($where_clause, $bind_list) = ('',[]);
917
918                                 eval $widget_code;
919
920                                 die "$@\n\n$widget_code" if ($@);
921
922                                 push @where, $where_clause;
923                                 push @bind, @$bind_list;
924
925                         } elsif (ref $val) {
926                                 push @where, "$full_col IN (".join(",",map {'?'}@$val).")";
927                                 push @bind, @$val;
928                         } else {
929                                 push @where, "$full_col = ?";
930                                 push @bind, $val;
931                         }
932                 }
933
934                 if ($t ne $t_name && (!@join_base || !grep{$t eq $_}@join_base)) {
935                         my $k = $doc->findvalue("//*[\@id='$t']/\@key");
936                         my $f = $doc->findvalue("//*[\@id='$t']/\@field");
937                         push @join, "dims.\"${t}_${k}\" = \"$core\".\"$f\"";
938                         push @join_base, $t;
939                 }
940         }
941
942         my $t = table_by_id($core)->findvalue('tablename');
943
944         my $from = " FROM $t AS \"$core\" ";
945         $from .= "RIGHT JOIN $d_select ON (". join(' AND ', @join).")" if ( @join );
946
947         my $select =
948                 "SELECT ".join(',', @output). $from;
949
950         $select .= ' WHERE '.join(' AND ', @where) if (@where);
951         $select .= ' GROUP BY '.join(',',@groupby) if (@groupby);
952
953         $r->{sql}->{'pivot'}    = $pivot;
954         $r->{sql}->{'select'}   = $select;
955         $r->{sql}->{'bind'}     = \@bind;
956         $r->{sql}->{columns}    = \@columns;
957         $r->{sql}->{groupby}    = \@groupby;
958         
959 }
960
961
962
963
964
965