Merge commit 'pianohacker-koha/prefs-submit' into master
[koha.git] / reports / guided_reports.pl
1 #!/usr/bin/perl
2
3 # Copyright 2007 Liblime ltd
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
18 # Suite 330, Boston, MA  02111-1307 USA
19
20 use strict;
21 # use warnings;  # FIXME
22 use CGI;
23 use Text::CSV;
24 use C4::Reports::Guided;
25 use C4::Auth;
26 use C4::Output;
27 use C4::Dates;
28 use C4::Debug;
29
30 =head1 NAME
31
32 guided_reports.pl
33
34 =head1 DESCRIPTION
35
36 Script to control the guided report creation
37
38 =over2
39
40 =cut
41
42 my $input = new CGI;
43
44 my $phase = $input->param('phase');
45 my $flagsrequired;
46 if ( $phase eq 'Build new' ) {
47     $flagsrequired = 'create_report';
48 }
49 elsif ( $phase eq 'Use saved' ) {
50     $flagsrequired = 'execute_report';
51 } else {
52     $flagsrequired = '*';
53 }
54
55 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
56     {
57         template_name   => "reports/guided_reports_start.tmpl",
58         query           => $input,
59         type            => "intranet",
60         authnotrequired => 0,
61         flagsrequired   => { reports => $flagsrequired },
62         debug           => 1,
63     }
64 );
65
66     my @errors = ();
67 if ( !$phase ) {
68     $template->param( 'start' => 1 );
69     # show welcome page
70 }
71 elsif ( $phase eq 'Build new' ) {
72     # build a new report
73     $template->param( 'build1' => 1 );
74     $template->param( 'areas' => get_report_areas() );
75 }
76 elsif ( $phase eq 'Use saved' ) {
77     # use a saved report
78     # get list of reports and display them
79     $template->param( 'saved1' => 1 );
80     $template->param( 'savedreports' => get_saved_reports() ); 
81 }
82
83 elsif ( $phase eq 'Delete Saved') {
84         
85         # delete a report from the saved reports list
86         my $id = $input->param('reports');
87         delete_report($id);
88     print $input->redirect("/cgi-bin/koha/reports/guided_reports.pl?phase=Use%20saved");
89         exit;
90 }               
91
92 elsif ( $phase eq 'Show SQL'){
93         
94         my $id = $input->param('reports');
95         my $sql = get_sql($id);
96         $template->param(
97                 'sql' => $sql,
98                 'showsql' => 1,
99     );
100 }
101
102 elsif ( $phase eq 'Edit SQL'){
103         
104     my $id = $input->param('reports');
105     my ($sql,$type,$reportname,$notes) = get_saved_report($id);
106     $template->param(
107             'sql'        => $sql,
108             'reportname' => $reportname,
109         'notes'      => $notes,
110         'id'         => $id,
111             'editsql'    => 1,
112     );
113 }
114
115 elsif ( $phase eq 'Update SQL'){
116     my $id         = $input->param('id');
117     my $sql        = $input->param('sql');
118     my $reportname = $input->param('reportname');
119     my $notes      = $input->param('notes');
120     my @errors;
121     if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
122         push @errors, {sqlerr => $1};
123     }
124     elsif ($sql !~ /^(SELECT)/i) {
125         push @errors, {queryerr => 1};
126     }
127     if (@errors) {
128         $template->param(
129             'errors'    => \@errors,
130             'sql'       => $sql,
131         );
132     }
133     else {
134         update_sql( $id, $sql, $reportname, $notes );
135         $template->param(
136             'save_successful'       => 1,
137         );
138     }
139     
140 }
141
142 elsif ($phase eq 'retrieve results') {
143         my $id = $input->param('id');
144         my ($results,$name,$notes) = format_results($id);
145         # do something
146         $template->param(
147                 'retresults' => 1,
148                 'results' => $results,
149                 'name' => $name,
150                 'notes' => $notes,
151     );
152 }
153
154 elsif ( $phase eq 'Report on this Area' ) {
155
156     # they have choosen a new report and the area to report on
157     $template->param(
158         'build2' => 1,
159         'area'   => $input->param('areas'),
160         'types'  => get_report_types(),
161     );
162 }
163
164 elsif ( $phase eq 'Choose this type' ) {
165
166     # they have chosen type and area
167     # get area and type and pass them to the template
168     my $area = $input->param('area');
169     my $type = $input->param('types');
170     $template->param(
171         'build3' => 1,
172         'area'   => $area,
173         'type'   => $type,
174         columns  => get_columns($area,$input),
175     );
176 }
177
178 elsif ( $phase eq 'Choose these columns' ) {
179
180     # we now know type, area, and columns
181     # next step is the constraints
182     my $area    = $input->param('area');
183     my $type    = $input->param('type');
184     my @columns = $input->param('columns');
185     my $column  = join( ',', @columns );
186     $template->param(
187         'build4' => 1,
188         'area'   => $area,
189         'type'   => $type,
190         'column' => $column,
191         definitions => get_from_dictionary($area),
192         criteria    => get_criteria($area,$input),
193     );
194 }
195
196 elsif ( $phase eq 'Choose these criteria' ) {
197     my $area     = $input->param('area');
198     my $type     = $input->param('type');
199     my $column   = $input->param('column');
200         my @definitions = $input->param('definition');
201         my $definition = join (',',@definitions);
202     my @criteria = $input->param('criteria_column');
203         my $query_criteria;
204     foreach my $crit (@criteria) {
205         my $value = $input->param( $crit . "_value" );
206         
207         # If value is not defined, then it may be range values
208         if (!defined $value) {
209
210             my $fromvalue = $input->param( "from_" . $crit . "_value" );
211             my $tovalue   = $input->param( "to_"   . $crit . "_value" );
212             
213             # If the range values are dates
214             if ($fromvalue =~ C4::Dates->regexp('syspref') && $tovalue =~ C4::Dates->regexp('syspref')) { 
215                 $fromvalue = C4::Dates->new($fromvalue)->output("iso");
216                 $tovalue = C4::Dates->new($tovalue)->output("iso");
217             }
218
219             if ($fromvalue && $tovalue) {
220                 $query_criteria .= " AND $crit >= '$fromvalue' AND $crit <= '$tovalue'";
221             }
222
223         } else {
224
225             # If value is a date
226             if ($value =~ C4::Dates->regexp('syspref')) { 
227                 $value = C4::Dates->new($value)->output("iso");
228             }
229             $query_criteria .= " AND $crit='$value'";
230         }
231         warn $query_criteria;
232     }
233
234     $template->param(
235         'build5'         => 1,
236         'area'           => $area,
237         'type'           => $type,
238         'column'         => $column,
239         'definition'     => $definition,
240         'criteriastring' => $query_criteria,
241     );
242
243     # get columns
244     my @columns = split( ',', $column );
245     my @total_by;
246
247     # build structue for use by tmpl_loop to choose columns to order by
248     # need to do something about the order of the order :)
249         # we also want to use the %columns hash to get the plain english names
250     foreach my $col (@columns) {
251         my %total = (name => $col);
252         my @selects = map {+{ value => $_ }} (qw(sum min max avg count));
253         $total{'select'} = \@selects;
254         push @total_by, \%total;
255     }
256
257     $template->param( 'total_by' => \@total_by );
258 }
259
260 elsif ( $phase eq 'Choose These Operations' ) {
261     my $area     = $input->param('area');
262     my $type     = $input->param('type');
263     my $column   = $input->param('column');
264     my $criteria = $input->param('criteria');
265         my $definition = $input->param('definition');
266     my @total_by = $input->param('total_by');
267     my $totals;
268     foreach my $total (@total_by) {
269         my $value = $input->param( $total . "_tvalue" );
270         $totals .= "$value($total),";
271     }
272
273     $template->param(
274         'build6'         => 1,
275         'area'           => $area,
276         'type'           => $type,
277         'column'         => $column,
278         'criteriastring' => $criteria,
279         'totals'         => $totals,
280         'definition'     => $definition,
281     );
282
283     # get columns
284     my @columns = split( ',', $column );
285     my @order_by;
286
287     # build structue for use by tmpl_loop to choose columns to order by
288     # need to do something about the order of the order :)
289     foreach my $col (@columns) {
290         my %order = (name => $col);
291         my @selects = map {+{ value => $_ }} (qw(asc desc));
292         $order{'select'} = \@selects;
293         push @order_by, \%order;
294     }
295
296     $template->param( 'order_by' => \@order_by );
297 }
298
299 elsif ( $phase eq 'Build Report' ) {
300
301     # now we have all the info we need and can build the sql
302     my $area     = $input->param('area');
303     my $type     = $input->param('type');
304     my $column   = $input->param('column');
305     my $crit     = $input->param('criteria');
306     my $totals   = $input->param('totals');
307         my $definition = $input->param('definition');
308 #    my @criteria = split( ',', $crit );
309     my $query_criteria=$crit;
310     # split the columns up by ,
311     my @columns = split( ',', $column );
312     my @order_by = $input->param('order_by');
313
314     my $query_orderby;
315     foreach my $order (@order_by) {
316         my $value = $input->param( $order . "_ovalue" );
317         if ($query_orderby) {
318             $query_orderby .= ",$order $value";
319         }
320         else {
321             $query_orderby = " ORDER BY $order $value";
322         }
323     }
324
325     # get the sql
326     my $sql =
327       build_query( \@columns, $query_criteria, $query_orderby, $area, $totals, $definition );
328     $template->param(
329         'showreport' => 1,
330         'sql'        => $sql,
331         'type'       => $type
332     );
333 }
334
335 elsif ( $phase eq 'Save' ) {
336         # Save the report that has just been built
337     my $sql  = $input->param('sql');
338     my $type = $input->param('type');
339     $template->param(
340         'save' => 1,
341         'sql'  => $sql,
342         'type' => $type
343     );
344 }
345
346 elsif ( $phase eq 'Save Report' ) {
347     # save the sql pasted in by a user 
348     my $sql  = $input->param('sql');
349     my $name = $input->param('reportname');
350     my $type = $input->param('types');
351     my $notes = $input->param('notes');
352     if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
353         push @errors, {sqlerr => $1};
354     }
355     elsif ($sql !~ /^(SELECT)/i) {
356         push @errors, {queryerr => 1};
357     }
358     if (@errors) {
359         $template->param(
360             'errors'    => \@errors,
361             'sql'       => $sql,
362             'reportname'=> $name,
363             'type'      => $type,
364             'notes'     => $notes,
365         );
366     }
367     else {
368         save_report( $borrowernumber, $sql, $name, $type, $notes );
369         $template->param(
370             'save_successful'       => 1,
371         );
372     }
373 }
374
375 elsif ($phase eq 'Run this report'){
376     # execute a saved report
377     my $limit  = 20;    # page size. # TODO: move to DB or syspref?
378     my $offset = 0;
379     my $report = $input->param('reports');
380     # offset algorithm
381     if ($input->param('page')) {
382         $offset = ($input->param('page') - 1) * $limit;
383     }
384     my ($sql,$type,$name,$notes) = get_saved_report($report);
385     unless ($sql) {
386         push @errors, {no_sql_for_id=>$report};   
387     } 
388     my @rows = ();
389     my ($sth, $errors) = execute_query($sql, $offset, $limit);
390     my $total = select_2_select_count_value($sql) || 0;
391     unless ($sth) {
392         die "execute_query failed to return sth for report $report: $sql";
393     } else {
394         my $headref = $sth->{NAME} || [];
395         my @headers = map { +{ cell => $_ } } @$headref;
396         $template->param(header_row => \@headers);
397         while (my $row = $sth->fetchrow_arrayref()) {
398             my @cells = map { +{ cell => $_ } } @$row;
399             push @rows, { cells => \@cells };
400         }
401     }
402
403     my $totpages = int($total/$limit) + (($total % $limit) > 0 ? 1 : 0);
404     my $url = "/cgi-bin/koha/reports/guided_reports.pl?reports=$report&phase=Run%20this%20report";
405     $template->param(
406         'results' => \@rows,
407         'sql'     => $sql,
408         'execute' => 1,
409         'name'    => $name,
410         'notes'   => $notes,
411         'errors'  => $errors,
412         'pagination_bar'  => pagination_bar($url, $totpages, $input->param('page')),
413         'unlimited_total' => $total,
414     );
415 }       
416
417 elsif ($phase eq 'Export'){
418     binmode STDOUT, ':utf8';
419
420         # export results to tab separated text or CSV
421         my $sql    = $input->param('sql');  # FIXME: use sql from saved report ID#, not new user-supplied SQL!
422     my $format = $input->param('format');
423         my ($sth, $q_errors) = execute_query($sql);
424     unless ($q_errors and @$q_errors) {
425         print $input->header(       -type => 'application/octet-stream',
426                                     -attachment=>"reportresults.$format"
427                             );
428         if ($format eq 'tab') {
429             print join("\t", header_cell_values($sth)), "\n";
430             while (my $row = $sth->fetchrow_arrayref()) {
431                 print join("\t", @$row), "\n";
432             }
433         } else {
434             my $csv = Text::CSV->new({binary => 1});
435             $csv or die "Text::CSV->new({binary => 1}) FAILED: " . Text::CSV->error_diag();
436             if ($csv->combine(header_cell_values($sth))) {
437                 print $csv->string(), "\n";
438             } else {
439                 push @$q_errors, { combine => 'HEADER ROW: ' . $csv->error_diag() } ;
440             }
441             while (my $row = $sth->fetchrow_arrayref()) {
442                 if ($csv->combine(@$row)) {
443                     print $csv->string(), "\n"; 
444                 } else {
445                     push @$q_errors, { combine => $csv->error_diag() } ;
446                 }
447             }
448         }
449         foreach my $err (@$q_errors, @errors) {
450             print "# ERROR: " . (map {$_ . ": " . $err->{$_}} keys %$err) . "\n";
451         }   # here we print all the non-fatal errors at the end.  Not super smooth, but better than nothing.
452         exit;
453     }
454     $template->param(
455         'sql'           => $sql,
456         'execute'       => 1,
457         'name'          => 'Error exporting report!',
458         'notes'         => '',
459         'errors'        => $q_errors,
460     );
461 }
462
463 elsif ($phase eq 'Create report from SQL') {
464         # allow the user to paste in sql
465     if ($input->param('sql')) {
466         $template->param(
467             'sql'           => $input->param('sql'),
468             'reportname'    => $input->param('reportname'),
469             'notes'         => $input->param('notes'),
470         );
471     }
472         $template->param('create' => 1);
473 }
474
475 elsif ($phase eq 'Create Compound Report'){
476         $template->param( 'savedreports' => get_saved_reports(),
477                 'compound' => 1,
478         );
479 }
480
481 elsif ($phase eq 'Save Compound'){
482     my $master    = $input->param('master');
483         my $subreport = $input->param('subreport');
484         my ($mastertables,$subtables) = create_compound($master,$subreport);
485         $template->param( 'save_compound' => 1,
486                 master=>$mastertables,
487                 subsql=>$subtables
488         );
489 }
490
491 # pass $sth, get back an array of names for the column headers
492 sub header_cell_values {
493     my $sth = shift or return ();
494     return @{$sth->{NAME}};
495 }
496
497 # pass $sth, get back a TMPL_LOOP-able set of names for the column headers
498 sub header_cell_loop {
499     my @headers = map { +{ cell => $_ } } header_cell_values (shift);
500     return \@headers;
501 }
502
503 # pass $sth, get back an array of names for the column headers
504 sub header_cell_values {
505     my $sth = shift or return ();
506     return @{$sth->{NAME}};
507 }
508
509 # pass $sth, get back a TMPL_LOOP-able set of names for the column headers
510 sub header_cell_loop {
511     my @headers = map { +{ cell => $_ } } header_cell_values (shift);
512     return \@headers;
513 }
514
515 foreach (1..6) {
516     $template->param('build' . $_) and $template->param(buildx => $_) and last;
517 }
518 $template->param(   'referer' => $input->referer(),
519                     'DHTMLcalendar_dateformat' => C4::Dates->DHTMLcalendar(),
520                 );
521
522 output_html_with_http_headers $input, $cookie, $template->output;