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