]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/reporter/clark-kent.pl
LP#1516867 Make HTML report tables sortable
[working/Evergreen.git] / Open-ILS / src / reporter / clark-kent.pl
1 #!/usr/bin/perl
2 # vim:ts=4:noet:
3
4 use strict;
5 use DBI;
6 use FileHandle;
7 use XML::LibXML;
8 use Getopt::Long;
9 use DateTime;
10 use DateTime::Format::ISO8601;
11 use Data::Dumper;
12 use Text::CSV_XS;
13 use Excel::Writer::XLSX;
14 use OpenSRF::EX qw/:try/;
15 use OpenSRF::Utils qw/:daemon/;
16 use OpenSRF::Utils::JSON;
17 use OpenSRF::Utils::Logger qw/$logger/;
18 use OpenSRF::System;
19 use OpenSRF::AppSession;
20 use OpenSRF::Utils::SettingsClient;
21 use OpenILS::Reporter::SQLBuilder;
22 use POSIX;
23 use GD::Graph::pie;
24 use GD::Graph::bars3d;
25 use GD::Graph::lines3d;
26 use Tie::IxHash;
27 use Email::Send;
28
29 use open ':utf8';
30
31
32 my ($config, $sleep_interval, $lockfile, $daemon) = ('SYSCONFDIR/opensrf_core.xml', 10, '/tmp/reporter-LOCK');
33
34 my $opt_count;
35 my $opt_max_rows_for_charts;
36 my $opt_statement_timeout;
37 my $opt_resultset_limit;
38
39 GetOptions(
40         "daemon"        => \$daemon,
41         "sleep=i"       => \$sleep_interval,
42         "concurrency=i" => \$opt_count,
43         "max-rows-for-charts=i" => \$opt_max_rows_for_charts,
44         "resultset-limit=i" => \$opt_resultset_limit,
45         "statement-timeout=i" => \$opt_statement_timeout,
46         "bootstrap|boostrap=s"  => \$config,
47         "lockfile=s"    => \$lockfile,
48 );
49
50 if (-e $lockfile) {
51         die "I seem to be running already. If not, remove $lockfile and try again\n";
52 }
53
54 OpenSRF::System->bootstrap_client( config_file => $config );
55
56 my (%data_db, %state_db);
57
58 my $sc = OpenSRF::Utils::SettingsClient->new;
59
60 $data_db{db_driver} = $sc->config_value( reporter => setup => database => 'driver' );
61 $data_db{db_host}   = $sc->config_value( reporter => setup => database => 'host' );
62 $data_db{db_port}   = $sc->config_value( reporter => setup => database => 'port' );
63 $data_db{db_name}   = $sc->config_value( reporter => setup => database => 'db' );
64 if (!$data_db{db_name}) {
65     $data_db{db_name} = $sc->config_value( reporter => setup => database => 'name' );
66     print STDERR "WARN: <database><name> is a deprecated setting for database name. For future compatibility, you should use <database><db> instead." if $data_db{db_name}; 
67 }
68 $data_db{db_user}   = $sc->config_value( reporter => setup => database => 'user' );
69 $data_db{db_pw}     = $sc->config_value( reporter => setup => database => 'pw' );
70
71
72
73 # Fetch the optional state database connection info
74 $state_db{db_driver} = $sc->config_value( reporter => setup => state_store => 'driver' ) || $data_db{db_driver};
75 $state_db{db_host}   = $sc->config_value( reporter => setup => state_store => 'host'   ) || $data_db{db_host};
76 $state_db{db_port}   = $sc->config_value( reporter => setup => state_store => 'port'   ) || $data_db{db_port};
77 $state_db{db_name}   = $sc->config_value( reporter => setup => state_store => 'db'     );
78 if (!$state_db{db_name}) {
79     $state_db{db_name} = $sc->config_value( reporter => setup => state_store => 'name' ) || $data_db{db_name};
80 }
81 $state_db{db_user}   = $sc->config_value( reporter => setup => state_store => 'user'   ) || $data_db{db_user};
82 $state_db{db_pw}     = $sc->config_value( reporter => setup => state_store => 'pw'     ) || $data_db{db_pw};
83
84
85 die "Unable to retrieve database connection information from the settings server"
86     unless ($state_db{db_driver} && $state_db{db_host} && $state_db{db_port} && $state_db{db_name} && $state_db{db_user} &&
87         $data_db{db_driver} && $data_db{db_host} && $data_db{db_port} && $data_db{db_name} && $data_db{db_user});
88
89 my $email_server     = $sc->config_value( email_notify => 'smtp_server' );
90 my $email_sender     = $sc->config_value( email_notify => 'sender_address' );
91 my $success_template = $sc->config_value( reporter => setup => files => 'success_template' );
92 my $fail_template    = $sc->config_value( reporter => setup => files => 'fail_template' );
93 my $output_base      = $sc->config_value( reporter => setup => files => 'output_base' );
94 my $base_uri         = $sc->config_value( reporter => setup => 'base_uri' );
95
96 my $state_dsn = "dbi:" . $state_db{db_driver} . ":dbname=" . $state_db{db_name} .';host=' . $state_db{db_host} . ';port=' . $state_db{db_port};
97 my $data_dsn  = "dbi:" .  $data_db{db_driver} . ":dbname=" .  $data_db{db_name} .';host=' .  $data_db{db_host} . ';port=' .  $data_db{db_port};
98
99 my $count               = $opt_count //
100                           $sc->config_value( reporter => setup => 'parallel' ) //
101                           1;
102 $count = 1 unless $count =~ /^\d+$/ && $count > 0;
103 my $statement_timeout   = $opt_statement_timeout //
104                           $sc->config_value( reporter => setup => 'statement_timeout' ) //
105                           60;
106 $statement_timeout = 60 unless $statement_timeout =~ /^\d+$/;
107 my $max_rows_for_charts = $opt_max_rows_for_charts //
108                           $sc->config_value( reporter => setup => 'max_rows_for_charts' ) //
109                           1000;
110 $max_rows_for_charts = 1000 unless $max_rows_for_charts =~ /^\d+$/;
111 my $resultset_limit     = $opt_resultset_limit //
112                           $sc->config_value( reporter => setup => 'resultset_limit' ) //
113                           0;
114 $resultset_limit = 0 unless $resultset_limit =~ /^\d+$/; # 0 means no limit
115
116 my ($dbh,$running,$sth,@reports,$run, $current_time);
117
118 if ($daemon) {
119         daemonize("Clark Kent, waiting for trouble");
120         open(F, ">$lockfile") or die "Cannot write lockfile '$lockfile'";
121         print F $$;
122         close F;
123 }
124
125
126 DAEMON:
127
128 $dbh = DBI->connect(
129         $state_dsn,
130         $state_db{db_user},
131         $state_db{db_pw},
132         { AutoCommit => 1,
133           pg_expand_array => 0,
134           pg_enable_utf8 => 1,
135           RaiseError => 1
136         }
137 );
138
139 $current_time = DateTime->from_epoch( epoch => time() )->strftime('%FT%T%z');
140
141 # make sure we're not already running $count reports
142 ($running) = $dbh->selectrow_array(<<SQL);
143 SELECT  count(*)
144   FROM  reporter.schedule
145   WHERE start_time IS NOT NULL AND complete_time IS NULL;
146 SQL
147
148 if ($count <= $running) {
149         if ($daemon) {
150                 $dbh->disconnect;
151                 sleep 1;
152                 POSIX::waitpid( -1, POSIX::WNOHANG );
153                 sleep $sleep_interval;
154                 goto DAEMON;
155         }
156         print "Already running maximum ($running) concurrent reports\n";
157         exit 1;
158 }
159
160 # if we have some open slots then generate the sql
161 $run = $count - $running;
162
163 $sth = $dbh->prepare(<<SQL);
164 SELECT  *
165   FROM  reporter.schedule
166   WHERE start_time IS NULL AND run_time < NOW()
167   ORDER BY run_time
168   LIMIT $run;
169 SQL
170
171 $sth->execute;
172
173 @reports = ();
174 while (my $r = $sth->fetchrow_hashref) {
175         my $s3 = $dbh->selectrow_hashref(<<"    SQL", {}, $r->{report});
176                 SELECT * FROM reporter.report WHERE id = ?;
177         SQL
178
179         my $s2 = $dbh->selectrow_hashref(<<"    SQL", {}, $s3->{template});
180                 SELECT * FROM reporter.template WHERE id = ?;
181         SQL
182
183         $s3->{template} = $s2;
184         $r->{report} = $s3;
185
186         my $b = OpenILS::Reporter::SQLBuilder->new;
187         my $report_data = OpenSRF::Utils::JSON->JSON2perl( $r->{report}->{data} );
188         $b->register_params( $report_data );
189
190         $r->{resultset} = $b->parse_report( OpenSRF::Utils::JSON->JSON2perl( $r->{report}->{template}->{data} ) );
191         $r->{resultset}->set_pivot_data($report_data->{__pivot_data}) if $report_data->{__pivot_data};
192         $r->{resultset}->set_pivot_label($report_data->{__pivot_label}) if $report_data->{__pivot_label};
193         $r->{resultset}->set_pivot_default($report_data->{__pivot_default}) if $report_data->{__pivot_default};
194         $r->{resultset}->relative_time($r->{run_time});
195         $r->{resultset}->resultset_limit($resultset_limit) if $resultset_limit;
196         push @reports, $r;
197 }
198
199 $sth->finish;
200
201 $dbh->disconnect;
202
203 # Now we spawn the report runners
204
205 for my $r ( @reports ) {
206         next if (safe_fork());
207
208         # This is the child (runner) process;
209         daemonize("Clark Kent reporting: $r->{report}->{name}");
210
211         my $state_dbh = DBI->connect(
212                 $state_dsn,
213                 $state_db{db_user},
214                 $state_db{db_pw},
215                 { AutoCommit => 1,
216                   pg_expand_array => 0,
217                   pg_enable_utf8 => 1,
218                   RaiseError => 1
219                 }
220         );
221
222         my $data_dbh = DBI->connect(
223                 $data_dsn,
224                 $data_db{db_user},
225                 $data_db{db_pw},
226                 { AutoCommit => 1,
227                   pg_expand_array => 0,
228                   pg_enable_utf8 => 1,
229                   RaiseError => 1
230                 }
231         );
232         $data_dbh->do('SET statement_timeout = ?', {}, ($statement_timeout * 60 * 1000));
233
234         try {
235                 $state_dbh->do(<<'              SQL',{}, $r->{id});
236                         UPDATE  reporter.schedule
237                           SET   start_time = now()
238                           WHERE id = ?;
239                 SQL
240
241             $logger->debug('Report SQL: ' . $r->{resultset}->toSQL);
242                 $sth = $data_dbh->prepare($r->{resultset}->toSQL);
243
244                 $sth->execute;
245                 $r->{data} = $sth->fetchall_arrayref;
246
247                 $r->{column_labels} = [$r->{resultset}->column_label_list];
248
249                 if ($r->{resultset}->pivot_data && $r->{resultset}->pivot_label) {
250                         my @labels = $r->{resultset}->column_label_list;
251                         my $newdata = pivot_data(
252                                 { columns => $r->{column_labels}, data => $r->{data}},
253                                 $r->{resultset}->pivot_label,
254                                 $r->{resultset}->pivot_data,
255                                 $r->{resultset}->pivot_default
256                         );
257
258                         $r->{column_labels} = $newdata->{columns};
259                         $r->{data} = $newdata->{data};
260                         $r->{group_by_list} = $newdata->{group_by_list};
261                 } else {
262                         $r->{group_by_list} = [$r->{resultset}->group_by_list(0)];
263                 }
264
265                 my $s2 = $r->{report}->{template}->{id};
266                 my $s3 = $r->{report}->{id};
267                 my $output = $r->{id};
268
269                 mkdir($output_base);
270                 mkdir("$output_base/$s2");
271                 mkdir("$output_base/$s2/$s3");
272                 mkdir("$output_base/$s2/$s3/$output");
273         
274                 my $output_dir = "$output_base/$s2/$s3/$output";
275
276                 if ( $r->{csv_format} ) {
277                         build_csv("$output_dir/report-data.csv", $r);
278                 }
279
280                 if ( $r->{excel_format} ) {
281                         build_excel("$output_dir/report-data.xlsx", $r);
282                 }
283
284                 build_html("$output_dir/report-data.html", $r);
285
286                 $state_dbh->begin_work;
287
288                 if ($r->{report}->{recur} ) {
289                         my $sql = <<'                   SQL';
290                                 INSERT INTO reporter.schedule (
291                                                 report,
292                                                 folder,
293                                                 runner,
294                                                 run_time,
295                                                 email,
296                                                 csv_format,
297                                                 excel_format,
298                                                 html_format,
299                                                 chart_pie,
300                                                 chart_bar,
301                                                 chart_line )
302                                         VALUES ( ?, ?, ?, ?::TIMESTAMPTZ + ?, ?, ?, ?, ?, ?, ?, ? );
303                         SQL
304
305                         $state_dbh->do(
306                                 $sql,
307                                 {},
308                                 $r->{report}->{id},
309                                 $r->{folder},
310                                 $r->{runner},
311                                 $r->{run_time},
312                                 $r->{report}->{recurrence},
313                                 $r->{email},
314                                 $r->{csv_format},
315                                 $r->{excel_format},
316                                 $r->{html_format},
317                                 $r->{chart_pie},
318                                 $r->{chart_bar},
319                                 $r->{chart_line},
320                         );
321                 }
322
323                 $state_dbh->do(<<'              SQL',{}, $r->{id});
324                         UPDATE  reporter.schedule
325                           SET   complete_time = now()
326                           WHERE id = ?;
327                 SQL
328
329                 $state_dbh->commit;
330
331                 my $new_r = $state_dbh->selectrow_hashref(<<"           SQL", {}, $r->{id});
332                         SELECT * FROM reporter.schedule WHERE id = ?;
333                 SQL
334
335                 $r->{start_time}    = $new_r->{start_time};
336                 $r->{complete_time} = $new_r->{complete_time};
337
338                 if ($r->{email}) {
339                         send_success($r);
340                 }
341
342         } otherwise {
343                 my $e = shift;
344                 $r->{error_text} = ''.$e;
345                 if (!$state_dbh->{AutoCommit}) {
346                         $state_dbh->rollback;
347                 }
348                 $state_dbh->do(<<'              SQL',{}, $e, $r->{id});
349                         UPDATE  reporter.schedule
350                           SET   error_text = ?,
351                                 complete_time = now(),
352                                 error_code = 1
353                           WHERE id = ?;
354                 SQL
355
356                 my $new_r = $state_dbh->selectrow_hashref(<<"           SQL", {}, $r->{id});
357                         SELECT * FROM reporter.schedule WHERE id = ?;
358                 SQL
359
360                 $r->{error_text}    = $new_r->{error_text};
361                 $r->{complete_time} = $new_r->{complete_time};
362
363                 if ($r->{email}) {
364                         send_fail($r);
365                 }
366
367         };
368
369         $state_dbh->disconnect;
370         $data_dbh->disconnect;
371
372         exit; # leave the child
373 }
374
375 if ($daemon) {
376         sleep 1;
377         POSIX::waitpid( -1, POSIX::WNOHANG );
378         sleep $sleep_interval;
379         goto DAEMON;
380 }
381
382 #-------------------------------------------------------------------
383
384 sub send_success {
385         my $r = shift;
386         open F, $success_template or die "Cannot read '$success_template'";
387         my $tmpl = join('',<F>);
388         close F;
389
390         my $url = $base_uri . '/' .
391                 $r->{report}->{template}->{id} . '/' .
392                 $r->{report}->{id} . '/' .
393                 $r->{id} . '/report-data.html';
394
395         $tmpl =~ s/{TO}/$r->{email}/smog;
396         $tmpl =~ s/{FROM}/$email_sender/smog;
397         $tmpl =~ s/{REPLY_TO}/$email_sender/smog;
398         $tmpl =~ s/{REPORT_NAME}/$r->{report}->{name} -- $r->{report}->{template}->{name}/smog;
399         $tmpl =~ s/{RUN_TIME}/$r->{run_time}/smog;
400         $tmpl =~ s/{COMPLETE_TIME}/$r->{complete_time}/smog;
401         $tmpl =~ s/{OUTPUT_URL}/$url/smog;
402
403         my $tdata = OpenSRF::Utils::JSON->JSON2perl( $r->{report}->{template}->{data} );
404         if ($$tdata{version} >= 4) {
405                 $tmpl =~ s/{EXTERNAL_URL}/$$tdata{doc_url}/smog;
406         }
407
408         my $sender = Email::Send->new({mailer => 'SMTP'});
409         $sender->mailer_args([Host => $email_server]);
410         $sender->send($tmpl);
411 }
412
413 sub send_fail {
414         my $r = shift;
415         open F, $fail_template or die "Cannot read '$fail_template'";
416         my $tmpl = join('',<F>);
417         close F;
418
419         my $sql = $r->{resultset}->toSQL;
420
421         $tmpl =~ s/{TO}/$r->{email}/smog;
422         $tmpl =~ s/{FROM}/$email_sender/smog;
423         $tmpl =~ s/{REPLY_TO}/$email_sender/smog;
424         $tmpl =~ s/{REPORT_NAME}/$r->{report}->{name} -- $r->{report}->{template}->{name}/smog;
425         $tmpl =~ s/{RUN_TIME}/$r->{run_time}/smog;
426         $tmpl =~ s/{ERROR_TEXT}/$r->{error_text}/smog;
427         $tmpl =~ s/{SQL}/$sql/smog;
428
429         my $tdata = OpenSRF::Utils::JSON->JSON2perl( $r->{report}->{template}->{data} );
430         if ($$tdata{version} >= 4) {
431                 $tmpl =~ s/{EXTERNAL_URL}/$$tdata{doc_url}/smog;
432         }
433
434         my $sender = Email::Send->new({mailer => 'SMTP'});
435         $sender->mailer_args([Host => $email_server]);
436         $sender->send($tmpl);
437 }
438
439 sub build_csv {
440         my $file = shift;
441         my $r = shift;
442
443         my $csv = Text::CSV_XS->new({ always_quote => 1, eol => "\015\012" });
444
445         return unless ($csv);
446         
447         my $f = new FileHandle (">$file") or die "Cannot write to '$file'";
448
449         $csv->print($f, $r->{column_labels});
450         $csv->print($f, $_) for (@{$r->{data}});
451
452         $f->close;
453 }
454 sub build_excel {
455         my $file = shift;
456         my $r = shift;
457         my $xls = Excel::Writer::XLSX->new($file);
458
459         my $sheetname = substr($r->{report}->{name},0,30);
460         $sheetname =~ s/\W/_/gos;
461         
462         my $sheet = $xls->add_worksheet($sheetname);
463         # don't try to write formulas, just write anything that starts with = as a text cell
464         $sheet->add_write_handler(qr/^=/, sub { return shift->write_string(@_); } );
465
466         $sheet->write_row('A1', $r->{column_labels});
467
468         $sheet->write_col('A2', $r->{data});
469
470         $xls->close;
471 }
472
473 sub build_html {
474         my $file = shift;
475         my $r = shift;
476
477         my $index = new FileHandle (">$file") or die "Cannot write to '$file'";
478
479         my $tdata = OpenSRF::Utils::JSON->JSON2perl( $r->{report}->{template}->{data} );
480         
481         # index header
482         print $index <<"        HEADER";
483 <html>
484         <head>
485                 <meta charset='utf-8'>
486                 <title>$$r{report}{name}</title>
487                 <style>
488                         table { border-collapse: collapse; }
489                         th { background-color: lightgray; }
490                         td,th { border: solid black 1px; }
491                         * { font-family: sans-serif; font-size: 10px; }
492                         .dim { color: lightblue; }
493                 </style>
494         </head>
495         <body>
496                 <center>
497                 <h2><u>$$r{report}{name}</u></h2>
498                 $$r{report}{description}<br/>
499         HEADER
500
501         if ($$tdata{version} >= 4 and $$tdata{doc_url}) {
502                 print $index "<a target='_blank' href='$$tdata{doc_url}'>External template documentation</a><br/>";
503         }
504
505         print $index "<br/><br/>";
506
507         my @links;
508
509     my $br4 = '<br/>' x 4;
510         # add a link to the raw output html
511         push @links, "<a href='report-data.html.raw.html'>Tabular Output</a>" if ($r->{html_format});
512
513         # add a link to the CSV output
514         push @links, "<a href='report-data.xlsx'>Excel Output</a>" if ($r->{excel_format});
515
516         # add a link to the CSV output
517         push @links, "<a href='report-data.csv'>CSV Output</a>" if ($r->{csv_format});
518
519         # debugging output
520         push @links, "<a class='dim' href='report-data.html.debug.html'>Debugging Info</a>";
521
522         my $debug = new FileHandle (">$file.debug.html") or die "Cannot write to '$file.debug.html'";
523         print $debug "<html><head><meta charset='utf-8'><title>DEBUG: $$r{report}{name}</title></head><body>";
524
525         {       no warnings;
526                 if ($$tdata{version} >= 4 and $$tdata{doc_url}) {
527                         print $debug "<b><a target='_blank' href='$$tdata{doc_url}'>External template documentation</a></b><br/><a href='report-data.html'>Back to output index</a><hr/>";
528                 }
529
530                 print $debug '<h1>Generated SQL</h1><pre>' . $r->{resultset}->toSQL() . "</pre><a href='report-data.html'>Back to output index</a><hr/>";
531                 print $debug '<h1>Template</h1><pre>' . Dumper( $r->{report}->{template} ) . "</pre><a href='report-data.html'>Back to output index</a><hr/>";
532                 print $debug '<h1>Template Data</h1><pre>' . Dumper( $tdata ) . "</pre><a href='report-data.html'>Back to output index</a><hr/>";
533                 print $debug '<h1>Report Parameter</h1><pre>' . Dumper( $r->{report} ) . "</pre><a href='report-data.html'>Back to output index</a><hr/>";
534                 print $debug '<h1>Report Parameter Data</h1><pre>' . Dumper( $tdata ) . "</pre><a href='report-data.html'>Back to output index</a><hr/>";
535                 print $debug '<h1>Report Run Time</h1><pre>' . $r->{resultset}->relative_time . "</pre><a href='report-data.html'>Back to output index</a><hr/>";
536                 print $debug '<h1>OpenILS::Reporter::SQLBuilder::ResultSet Object</h1><pre>' . Dumper( $r->{resultset} ) . "</pre><a href='report-data.html'>Back to output index</a>";
537         }
538
539         print $debug '</body></html>';
540
541         $debug->close;
542
543         print $index join(' -- ', @links);
544         print $index "$br4</center>";
545
546         if ($r->{html_format}) {
547                 # create the raw output html file
548                 my $raw = new FileHandle (">$file.raw.html") or die "Cannot write to '$file.raw.html'";
549                 print $raw "<html><head><meta charset='utf-8'><title>$$r{report}{name}</title>";
550
551                 print $raw <<'          CSS';
552                         <style>
553                                 table { border-collapse: collapse; }
554                                 th { background-color: lightgray; }
555                                 td,th { border: solid black 1px; }
556                                 * { font-family: sans-serif; }
557                         </style>
558                         <script src="/js/sortable/sortable.min.js"></script>
559                         <link rel="stylesheet" href="/js/sortable/sortable-theme-minimal.css" />
560                 CSS
561
562                 print $raw "</head><body><table class='sortable-theme-minimal' data-sortable>";
563
564                 {       no warnings;
565                         print $raw "<thead><tr><th>".join('</th><th>', @{$r->{column_labels}})."</th></tr></thead>\n<tbody>";
566                         print $raw "<tr><td>".join('</td><td>', @$_)."</td></tr>\n" for (@{$r->{data}});
567                 }
568
569                 print $raw '</tbody></table></body></html>';
570         
571                 $raw->close;
572         }
573
574         # Time for a pie chart
575         if ($r->{chart_pie}) {
576                 if (scalar(@{$r->{data}}) > $max_rows_for_charts) {
577                         print $index "<strong>Report output has too many rows to make a pie chart</strong>$br4";
578                 } else {
579                         my $pics = draw_pie($r, $file);
580                         for my $pic (@$pics) {
581                                 print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/>$br4";
582                         }
583                 }
584         }
585
586         print $index $br4;
587         # Time for a bar chart
588         if ($r->{chart_bar}) {
589                 if (scalar(@{$r->{data}}) > $max_rows_for_charts) {
590                         print $index "<strong>Report output has too many rows to make a bar chart</strong>$br4";
591                 } else {
592                         my $pics = draw_bars($r, $file);
593                         for my $pic (@$pics) {
594                                 print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/>$br4";
595                         }
596                 }
597         }
598
599         print $index $br4;
600         # Time for a bar chart
601         if ($r->{chart_line}) {
602                 if (scalar(@{$r->{data}}) > $max_rows_for_charts) {
603                         print $index "<strong>Report output has too many rows to make a line chart</strong>$br4";
604                 } else {
605                         my $pics = draw_lines($r, $file);
606                         for my $pic (@$pics) {
607                                 print $index "<img src='report-data.html.$pic->{file}' alt='$pic->{name}'/>$br4";
608                         }
609             }
610         }
611
612         # and that's it!
613         print $index '</body></html>';
614         
615         $index->close;
616 }
617
618 sub draw_pie {
619         my $r = shift;
620         my $file = shift;
621
622         my $data = $r->{data};
623
624         my @groups = @{ $r->{group_by_list} };
625         
626         my @values = (0 .. (scalar(@{$r->{column_labels}}) - 1));
627         delete @values[@groups];
628
629         #my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
630         
631         my @pics;
632         for my $vcol (@values) {
633                 next unless (defined $vcol);
634
635                 my @pic_data = ([],[]);
636                 for my $row (@$data) {
637                         next if (!defined($$row[$vcol]) || $$row[$vcol] == 0);
638                         my $val = $$row[$vcol];
639                         push @{$pic_data[0]}, join(" -- ", @$row[@groups])." ($val)";
640                         push @{$pic_data[1]}, $val;
641                 }
642
643                 next unless (@{$pic_data[0]});
644
645                 my $size = 300;
646                 my $split = int(scalar(@{$pic_data[0]}) / $size);
647                 my $last = scalar(@{$pic_data[0]}) % $size;
648
649                 for my $sub_graph (0 .. $split) {
650                         
651                         if ($sub_graph == $split) {
652                                 $size = $last;
653                         }
654
655                         my @sub_data;
656                         for my $set (@pic_data) {
657                                 push @sub_data, [ splice(@$set,0,$size) ];
658                         }
659
660                         my $pic = new GD::Graph::pie;
661
662                         $pic->set(
663                                 label                   => $r->{column_labels}->[$vcol],
664                                 start_angle             => 180,
665                                 legend_placement        => 'R',
666                                 #logo                   => $logo,
667                                 #logo_position          => 'TL',
668                                 #logo_resize            => 0.5,
669                                 show_values             => 1,
670                         );
671
672                         my $format = $pic->export_format;
673
674                         open(IMG, ">$file.pie.$vcol.$sub_graph.$format") or die "Cannot write '$file.pie.$vcol.$sub_graph.$format'";
675                         binmode IMG;
676
677                         my $forgetit = 0;
678                         try {
679                                 $pic->plot(\@sub_data) or die $pic->error;
680                                 print IMG $pic->gd->$format;
681                         } otherwise {
682                                 my $e = shift;
683                                 warn "Couldn't draw $file.pie.$vcol.$sub_graph.$format : $e";
684                                 $forgetit = 1;
685                         };
686
687                         close IMG;
688
689
690                         push @pics,
691                                 { file => "pie.$vcol.$sub_graph.$format",
692                                   name => $r->{column_labels}->[$vcol].' (Pie)',
693                                 } unless ($forgetit);
694
695                         last if ($sub_graph == $split);
696                 }
697
698         }
699         
700         return \@pics;
701 }
702
703 sub draw_bars {
704         my $r = shift;
705         my $file = shift;
706         my $data = $r->{data};
707
708         #my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
709
710         my @groups = @{ $r->{group_by_list} };
711
712         
713         my @values = (0 .. (scalar(@{$r->{column_labels}}) - 1));
714         splice(@values,$_,1) for (reverse @groups);
715
716         my @pic_data;
717         {       no warnings;
718                 for my $row (@$data) {
719                         push @{$pic_data[0]}, join(' -- ', @$row[@groups]);
720                 }
721         }
722
723         my @leg;
724         my $set = 1;
725
726         my %trim_candidates;
727
728         my $max_y = 0;
729         for my $vcol (@values) {
730                 next unless (defined $vcol);
731                 $pic_data[$set] ||= [];
732
733                 my $pos = 0;
734                 for my $row (@$data) {
735                         my $val = $$row[$vcol] ? $$row[$vcol] : 0;
736                         push @{$pic_data[$set]}, $val;
737                         $max_y = $val if ($val > $max_y);
738                         $trim_candidates{$pos}++ if ($val == 0);
739                         $pos++;
740                 }
741
742                 $set++;
743         }
744         my $set_count = scalar(@pic_data) - 1;
745         my @trim_cols = grep { $trim_candidates{$_} == $set_count } keys %trim_candidates;
746
747         my @new_data;
748         my @use_me;
749         my @no_use;
750         my $set_index = 0;
751         for my $dataset (@pic_data) {
752                 splice(@$dataset,$_,1) for (reverse sort @trim_cols);
753
754                 if (grep { $_ } @$dataset) {
755                         push @new_data, $dataset;
756                         push @use_me, $set_index if ($set_index > 0);
757                 } else {
758                         push @no_use, $set_index;
759                 }
760                 $set_index++;
761                 
762         }
763
764         return [] unless ($new_data[0] && @{$new_data[0]});
765
766         for my $col (@use_me) {
767                 push @leg, $r->{column_labels}->[$values[$col - 1]];
768         }
769
770         my $w = 100 + 10 * scalar(@{$new_data[0]});
771         $w = 400 if ($w < 400);
772
773         my $h = 10 * (scalar(@new_data) / 2);
774
775         $h = 0 if ($h < 0);
776
777         my $pic = new GD::Graph::bars3d ($w + 250, $h + 500);
778
779         $pic->set(
780                 title                   => $r->{report}{name},
781                 x_labels_vertical       => 1,
782                 shading                 => 1,
783                 bar_depth               => 5,
784                 bar_spacing             => 2,
785                 y_max_value             => $max_y,
786                 legend_placement        => 'TR',
787                 boxclr                  => 'lgray',
788                 #logo                   => $logo,
789                 #logo_position          => 'R',
790                 #logo_resize            => 0.5,
791                 show_values             => 1,
792                 overwrite               => 1,
793         );
794         $pic->set_legend(@leg);
795
796         my $format = $pic->export_format;
797
798         open(IMG, ">$file.bar.$format") or die "Cannot write '$file.bar.$format'";
799         binmode IMG;
800
801         try {
802                 $pic->plot(\@new_data) or die $pic->error;
803                 print IMG $pic->gd->$format;
804         } otherwise {
805                 my $e = shift;
806                 warn "Couldn't draw $file.bar.$format : $e";
807         };
808
809         close IMG;
810
811         return [{ file => "bar.$format",
812                   name => $r->{report}{name}.' (Bar)',
813                 }];
814
815 }
816
817 sub draw_lines {
818         my $r    = shift;
819         my $file = shift;
820         my $data = $r->{data};
821
822         #my $logo = $doc->findvalue('/reporter/setup/files/chart_logo');
823
824         my @groups = @{ $r->{group_by_list} };
825         
826         my @values = (0 .. (scalar(@{$r->{column_labels}}) - 1));
827         splice(@values,$_,1) for (reverse @groups);
828
829         my @pic_data;
830         {       no warnings;
831                 for my $row (@$data) {
832                         push @{$pic_data[0]}, join(' -- ', @$row[@groups]);
833                 }
834         }
835
836         my @leg;
837         my $set = 1;
838
839         my $max_y = 0;
840         for my $vcol (@values) {
841                 next unless (defined $vcol);
842                 $pic_data[$set] ||= [];
843
844
845                 for my $row (@$data) {
846                         my $val = $$row[$vcol] ? $$row[$vcol] : 0;
847                         push @{$pic_data[$set]}, $val;
848                         $max_y = $val if ($val > $max_y);
849                 }
850
851                 $set++;
852         }
853         my $set_count = scalar(@pic_data) - 1;
854
855         my @new_data;
856         my @use_me;
857         my @no_use;
858         my $set_index = 0;
859         for my $dataset (@pic_data) {
860
861                 if (grep { $_ } @$dataset) {
862                         push @new_data, $dataset;
863                         push @use_me, $set_index if ($set_index > 0);
864                 } else {
865                         push @no_use, $set_index;
866                 }
867                 $set_index++;
868                 
869         }
870
871         return [] unless ($new_data[0] && @{$new_data[0]});
872
873         for my $col (@use_me) {
874                 push @leg, $r->{column_labels}->[$values[$col - 1]];
875         }
876
877         my $w = 100 + 10 * scalar(@{$new_data[0]});
878         $w = 400 if ($w < 400);
879
880         my $h = 10 * (scalar(@new_data) / 2);
881
882         $h = 0 if ($h < 0);
883
884         my $pic = new GD::Graph::lines3d ($w + 250, $h + 500);
885
886         $pic->set(
887                 title                   => $r->{report}{name},
888                 x_labels_vertical       => 1,
889                 shading                 => 1,
890                 line_depth              => 5,
891                 y_max_value             => $max_y,
892                 legend_placement        => 'TR',
893                 boxclr                  => 'lgray',
894                 #logo                   => $logo,
895                 #logo_position          => 'R',
896                 #logo_resize            => 0.5,
897                 show_values             => 1,
898                 overwrite               => 1,
899         );
900         $pic->set_legend(@leg);
901
902         my $format = $pic->export_format;
903
904         open(IMG, ">$file.line.$format") or die "Cannot write '$file.line.$format'";
905         binmode IMG;
906
907         try {
908                 $pic->plot(\@new_data) or die $pic->error;
909                 print IMG $pic->gd->$format;
910         } otherwise {
911                 my $e = shift;
912                 warn "Couldn't draw $file.line.$format : $e";
913         };
914
915         close IMG;
916
917         return [{ file => "line.$format",
918                   name => $r->{report}{name}.' (Bar)',
919                 }];
920
921 }
922
923
924 sub pivot_data {
925         my $blob        = shift;
926         my $pivot_label = shift;
927         my $pivot_data  = shift;
928         my $default     = shift;
929         $default = 0 unless (defined $default);
930
931         my $data = $$blob{data};
932         my $cols = $$blob{columns};
933
934         my @keep_labels =  @$cols;
935         splice(@keep_labels, $_ - 1, 1) for (reverse sort ($pivot_label, $pivot_data));
936
937         my @keep_cols = (0 .. @$cols - 1);
938         splice(@keep_cols, $_ - 1, 1) for (reverse sort ($pivot_label, $pivot_data));
939
940         my @gb = ( 0 .. @keep_cols - 1);
941
942         #first, find the unique list of pivot values
943         my %tmp;
944         for my $row (@$data) {
945                 $tmp{ $$row[$pivot_label - 1] } = 1;
946         }
947         my @new_cols = sort keys %tmp;
948
949         tie my %split_data, 'Tie::IxHash';
950         for my $row (@$data) {
951
952                 my $row_fp = ''. join('', map { defined($$row[$_]) ? $$row[$_] : '' } @keep_cols);
953                 $split_data{$row_fp} ||= [];
954
955                 push @{ $split_data{$row_fp} }, $row;
956         }
957
958
959         #now loop over the data, building a new result set
960         tie my %new_data, 'Tie::IxHash';
961
962         for my $fp ( keys %split_data ) {
963
964                 $new_data{$fp} = [];
965
966                 for my $col (@keep_cols) {
967                         push @{ $new_data{$fp} }, $split_data{$fp}[0][$col];
968                 }
969
970                 for my $col (@new_cols) {
971
972                         my ($datum) = map { $_->[$pivot_data - 1] } grep { $_->[$pivot_label - 1] eq $col } @{ $split_data{$fp} };
973                         $datum ||= $default;
974                         push @{ $new_data{$fp} }, $datum;
975                 }
976         }
977
978         push @keep_labels, @new_cols;
979
980         return { columns => \@keep_labels, data => [ values %new_data ], group_by_list => \@gb };
981 }
982
983