Translation updates for Koha 3.18.0-beta release
[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
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 use Modern::Perl;
21 use CGI qw/-utf8/;
22 use Text::CSV::Encoded;
23 use URI::Escape;
24 use File::Temp;
25 use File::Basename qw( dirname );
26 use C4::Reports::Guided;
27 use C4::Auth qw/:DEFAULT get_session/;
28 use C4::Output;
29 use C4::Dates qw/format_date/;
30 use C4::Debug;
31 use C4::Branch; # XXX subfield_is_koha_internal_p
32 use C4::Koha qw/IsAuthorisedValueCategory GetFrameworksLoop/;
33
34 =head1 NAME
35
36 guided_reports.pl
37
38 =head1 DESCRIPTION
39
40 Script to control the guided report creation
41
42 =cut
43
44 my $input = new CGI;
45 my $usecache = C4::Context->ismemcached;
46
47 my $phase = $input->param('phase');
48 my $flagsrequired;
49 if ( $phase eq 'Build new' or $phase eq 'Delete Saved' ) {
50     $flagsrequired = 'create_reports';
51 }
52 elsif ( $phase eq 'Use saved' ) {
53     $flagsrequired = 'execute_reports';
54 } else {
55     $flagsrequired = '*';
56 }
57
58 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
59     {
60         template_name   => "reports/guided_reports_start.tt",
61         query           => $input,
62         type            => "intranet",
63         authnotrequired => 0,
64         flagsrequired   => { reports => $flagsrequired },
65         debug           => 1,
66     }
67 );
68 my $session = $cookie ? get_session($cookie->value) : undef;
69
70 my $filter;
71 if ( $input->param("filter_set") ) {
72     $filter = {};
73     $filter->{$_} = $input->param("filter_$_") foreach qw/date author keyword group subgroup/;
74     $session->param('report_filter', $filter) if $session;
75     $template->param( 'filter_set' => 1 );
76 }
77 elsif ($session) {
78     $filter = $session->param('report_filter');
79 }
80
81
82 my @errors = ();
83 if ( !$phase ) {
84     $template->param( 'start' => 1 );
85     # show welcome page
86 }
87 elsif ( $phase eq 'Build new' ) {
88     # build a new report
89     $template->param( 'build1' => 1 );
90     $template->param(
91         'areas'        => get_report_areas(),
92         'usecache'     => $usecache,
93         'cache_expiry' => 300,
94         'public'       => '0',
95     );
96 } elsif ( $phase eq 'Use saved' ) {
97
98     # use a saved report
99     # get list of reports and display them
100     my $group = $input->param('group');
101     my $subgroup = $input->param('subgroup');
102     $filter->{group} = $group;
103     $filter->{subgroup} = $subgroup;
104     $template->param(
105         'saved1' => 1,
106         'savedreports' => get_saved_reports($filter),
107         'usecache' => $usecache,
108         'groups_with_subgroups'=> groups_with_subgroups($group, $subgroup),
109     );
110 }
111
112 elsif ( $phase eq 'Delete Multiple') {
113     my @ids = $input->param('ids');
114     delete_report( @ids );
115     print $input->redirect("/cgi-bin/koha/reports/guided_reports.pl?phase=Use%20saved");
116     exit;
117 }
118
119 elsif ( $phase eq 'Delete Saved') {
120         
121         # delete a report from the saved reports list
122     my $ids = $input->param('reports');
123     delete_report($ids);
124     print $input->redirect("/cgi-bin/koha/reports/guided_reports.pl?phase=Use%20saved");
125         exit;
126 }               
127
128 elsif ( $phase eq 'Show SQL'){
129         
130     my $id = $input->param('reports');
131     my $report = get_saved_report($id);
132     $template->param(
133         'id'      => $id,
134         'reportname' => $report->{report_name},
135         'notes'      => $report->{notes},
136         'sql'     => $report->{savedsql},
137         'showsql' => 1,
138     );
139 }
140
141 elsif ( $phase eq 'Edit SQL'){
142     my $id = $input->param('reports');
143     my $report = get_saved_report($id);
144     my $group = $report->{report_group};
145     my $subgroup  = $report->{report_subgroup};
146     $template->param(
147         'sql'        => $report->{savedsql},
148         'reportname' => $report->{report_name},
149         'groups_with_subgroups' => groups_with_subgroups($group, $subgroup),
150         'notes'      => $report->{notes},
151         'id'         => $id,
152         'cache_expiry' => $report->{cache_expiry},
153         'public' => $report->{public},
154         'usecache' => $usecache,
155         'editsql'    => 1,
156     );
157 }
158
159 elsif ( $phase eq 'Update SQL'){
160     my $id         = $input->param('id');
161     my $sql        = $input->param('sql');
162     my $reportname = $input->param('reportname');
163     my $group      = $input->param('group');
164     my $subgroup   = $input->param('subgroup');
165     my $notes      = $input->param('notes');
166     my $cache_expiry = $input->param('cache_expiry');
167     my $cache_expiry_units = $input->param('cache_expiry_units');
168     my $public = $input->param('public');
169     my $save_anyway = $input->param('save_anyway');
170
171     my @errors;
172
173     # if we have the units, then we came from creating a report from SQL and thus need to handle converting units
174     if( $cache_expiry_units ){
175       if( $cache_expiry_units eq "minutes" ){
176         $cache_expiry *= 60;
177       } elsif( $cache_expiry_units eq "hours" ){
178         $cache_expiry *= 3600; # 60 * 60
179       } elsif( $cache_expiry_units eq "days" ){
180         $cache_expiry *= 86400; # 60 * 60 * 24
181       }
182     }
183     # check $cache_expiry isnt too large, Memcached::set requires it to be less than 30 days or it will be treated as if it were an absolute time stamp
184     if( $cache_expiry >= 2592000 ){
185       push @errors, {cache_expiry => $cache_expiry};
186     }
187
188     create_non_existing_group_and_subgroup($input, $group, $subgroup);
189
190     if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
191         push @errors, {sqlerr => $1};
192     }
193     elsif ($sql !~ /^(SELECT)/i) {
194         push @errors, {queryerr => 1};
195     }
196
197     if (@errors) {
198         $template->param(
199             'errors'    => \@errors,
200             'sql'       => $sql,
201         );
202     } else {
203
204         # Check defined SQL parameters for authorised value validity
205         my $problematic_authvals = ValidateSQLParameters($sql);
206
207         if ( scalar @$problematic_authvals > 0 && not $save_anyway ) {
208             # There's at least one problematic parameter, report to the
209             # GUI and provide all user input for further actions
210             $template->param(
211                 'id' => $id,
212                 'sql' => $sql,
213                 'reportname' => $reportname,
214                 'group' => $group,
215                 'subgroup' => $subgroup,
216                 'notes' => $notes,
217                 'cache_expiry' => $cache_expiry,
218                 'cache_expiry_units' => $cache_expiry_units,
219                 'public' => $public,
220                 'problematic_authvals' => $problematic_authvals,
221                 'warn_authval_problem' => 1,
222                 'phase_update' => 1
223             );
224
225         } else {
226             # No params problem found or asked to save anyway
227             update_sql( $id, {
228                     sql => $sql,
229                     name => $reportname,
230                     group => $group,
231                     subgroup => $subgroup,
232                     notes => $notes,
233                     cache_expiry => $cache_expiry,
234                     public => $public,
235                 } );
236             $template->param(
237                 'save_successful'       => 1,
238                 'reportname'            => $reportname,
239                 'id'                    => $id,
240             );
241         }
242     }
243 }
244
245 elsif ($phase eq 'retrieve results') {
246         my $id = $input->param('id');
247         my ($results,$name,$notes) = format_results($id);
248         # do something
249         $template->param(
250                 'retresults' => 1,
251                 'results' => $results,
252                 'name' => $name,
253                 'notes' => $notes,
254     );
255 }
256
257 elsif ( $phase eq 'Report on this Area' ) {
258     my $cache_expiry_units = $input->param('cache_expiry_units'),
259     my $cache_expiry = $input->param('cache_expiry');
260
261     # we need to handle converting units
262     if( $cache_expiry_units eq "minutes" ){
263       $cache_expiry *= 60;
264     } elsif( $cache_expiry_units eq "hours" ){
265       $cache_expiry *= 3600; # 60 * 60
266     } elsif( $cache_expiry_units eq "days" ){
267       $cache_expiry *= 86400; # 60 * 60 * 24
268     }
269     # check $cache_expiry isnt too large, Memcached::set requires it to be less than 30 days or it will be treated as if it were an absolute time stamp
270     if( $cache_expiry >= 2592000 ){ # oops, over the limit of 30 days
271       # report error to user
272       $template->param(
273         'cache_error' => 1,
274         'build1' => 1,
275         'areas'   => get_report_areas(),
276         'cache_expiry' => $cache_expiry,
277         'usecache' => $usecache,
278         'public' => $input->param('public'),
279       );
280     } else {
281       # they have choosen a new report and the area to report on
282       $template->param(
283           'build2' => 1,
284           'area'   => $input->param('area'),
285           'types'  => get_report_types(),
286           'cache_expiry' => $cache_expiry,
287           'public' => $input->param('public'),
288       );
289     }
290 }
291
292 elsif ( $phase eq 'Choose this type' ) {
293     # they have chosen type and area
294     # get area and type and pass them to the template
295     my $area = $input->param('area');
296     my $type = $input->param('types');
297     $template->param(
298         'build3' => 1,
299         'area'   => $area,
300         'type'   => $type,
301         columns  => get_columns($area,$input),
302         'cache_expiry' => $input->param('cache_expiry'),
303         'public' => $input->param('public'),
304     );
305 }
306
307 elsif ( $phase eq 'Choose these columns' ) {
308     # we now know type, area, and columns
309     # next step is the constraints
310     my $area    = $input->param('area');
311     my $type    = $input->param('type');
312     my @columns = $input->param('columns');
313     my $column  = join( ',', @columns );
314     $template->param(
315         'build4' => 1,
316         'area'   => $area,
317         'type'   => $type,
318         'column' => $column,
319         definitions => get_from_dictionary($area),
320         criteria    => get_criteria($area,$input),
321         'cache_expiry' => $input->param('cache_expiry'),
322         'cache_expiry_units' => $input->param('cache_expiry_units'),
323         'public' => $input->param('public'),
324     );
325 }
326
327 elsif ( $phase eq 'Choose these criteria' ) {
328     my $area     = $input->param('area');
329     my $type     = $input->param('type');
330     my $column   = $input->param('column');
331     my @definitions = $input->param('definition');
332     my $definition = join (',',@definitions);
333     my @criteria = $input->param('criteria_column');
334     my $query_criteria;
335     foreach my $crit (@criteria) {
336         my $value = $input->param( $crit . "_value" );
337
338         # If value is not defined, then it may be range values
339         if (!defined $value) {
340
341             my $fromvalue = $input->param( "from_" . $crit . "_value" );
342             my $tovalue   = $input->param( "to_"   . $crit . "_value" );
343
344             # If the range values are dates
345             if ($fromvalue =~ C4::Dates->regexp('syspref') && $tovalue =~ C4::Dates->regexp('syspref')) {
346                 $fromvalue = C4::Dates->new($fromvalue)->output("iso");
347                 $tovalue = C4::Dates->new($tovalue)->output("iso");
348             }
349
350             if ($fromvalue && $tovalue) {
351                 $query_criteria .= " AND $crit >= '$fromvalue' AND $crit <= '$tovalue'";
352             }
353
354         } else {
355
356             # If value is a date
357             if ($value =~ C4::Dates->regexp('syspref')) {
358                 $value = C4::Dates->new($value)->output("iso");
359             }
360             # don't escape runtime parameters, they'll be at runtime
361             if ($value =~ /<<.*>>/) {
362                 $query_criteria .= " AND $crit=$value";
363             } else {
364                 $query_criteria .= " AND $crit='$value'";
365             }
366         }
367     }
368     $template->param(
369         'build5'         => 1,
370         'area'           => $area,
371         'type'           => $type,
372         'column'         => $column,
373         'definition'     => $definition,
374         'criteriastring' => $query_criteria,
375         'cache_expiry' => $input->param('cache_expiry'),
376         'cache_expiry_units' => $input->param('cache_expiry_units'),
377         'public' => $input->param('public'),
378     );
379
380     # get columns
381     my @columns = split( ',', $column );
382     my @total_by;
383
384     # build structue for use by tmpl_loop to choose columns to order by
385     # need to do something about the order of the order :)
386         # we also want to use the %columns hash to get the plain english names
387     foreach my $col (@columns) {
388         my %total = (name => $col);
389         my @selects = map {+{ value => $_ }} (qw(sum min max avg count));
390         $total{'select'} = \@selects;
391         push @total_by, \%total;
392     }
393
394     $template->param( 'total_by' => \@total_by );
395 }
396
397 elsif ( $phase eq 'Choose these operations' ) {
398     my $area     = $input->param('area');
399     my $type     = $input->param('type');
400     my $column   = $input->param('column');
401     my $criteria = $input->param('criteria');
402         my $definition = $input->param('definition');
403     my @total_by = $input->param('total_by');
404     my $totals;
405     foreach my $total (@total_by) {
406         my $value = $input->param( $total . "_tvalue" );
407         $totals .= "$value($total),";
408     }
409
410     $template->param(
411         'build6'         => 1,
412         'area'           => $area,
413         'type'           => $type,
414         'column'         => $column,
415         'criteriastring' => $criteria,
416         'totals'         => $totals,
417         'definition'     => $definition,
418         'cache_expiry' => $input->param('cache_expiry'),
419         'public' => $input->param('public'),
420     );
421
422     # get columns
423     my @columns = split( ',', $column );
424     my @order_by;
425
426     # build structue for use by tmpl_loop to choose columns to order by
427     # need to do something about the order of the order :)
428     foreach my $col (@columns) {
429         my %order = (name => $col);
430         my @selects = map {+{ value => $_ }} (qw(asc desc));
431         $order{'select'} = \@selects;
432         push @order_by, \%order;
433     }
434
435     $template->param( 'order_by' => \@order_by );
436 }
437
438 elsif ( $phase eq 'Build report' ) {
439
440     # now we have all the info we need and can build the sql
441     my $area     = $input->param('area');
442     my $type     = $input->param('type');
443     my $column   = $input->param('column');
444     my $crit     = $input->param('criteria');
445     my $totals   = $input->param('totals');
446     my $definition = $input->param('definition');
447     my $query_criteria=$crit;
448     # split the columns up by ,
449     my @columns = split( ',', $column );
450     my @order_by = $input->param('order_by');
451
452     my $query_orderby;
453     foreach my $order (@order_by) {
454         my $value = $input->param( $order . "_ovalue" );
455         if ($query_orderby) {
456             $query_orderby .= ",$order $value";
457         }
458         else {
459             $query_orderby = " ORDER BY $order $value";
460         }
461     }
462
463     # get the sql
464     my $sql =
465       build_query( \@columns, $query_criteria, $query_orderby, $area, $totals, $definition );
466     $template->param(
467         'showreport' => 1,
468         'area'       => $area,
469         'sql'        => $sql,
470         'type'       => $type,
471         'cache_expiry' => $input->param('cache_expiry'),
472         'public' => $input->param('public'),
473     );
474 }
475
476 elsif ( $phase eq 'Save' ) {
477     # Save the report that has just been built
478     my $area           = $input->param('area');
479     my $sql  = $input->param('sql');
480     my $type = $input->param('type');
481     $template->param(
482         'save' => 1,
483         'area'  => $area,
484         'sql'  => $sql,
485         'type' => $type,
486         'cache_expiry' => $input->param('cache_expiry'),
487         'public' => $input->param('public'),
488         'groups_with_subgroups' => groups_with_subgroups($area), # in case we have a report group that matches area
489     );
490 }
491
492 elsif ( $phase eq 'Save Report' ) {
493     # save the sql pasted in by a user
494     my $area  = $input->param('area');
495     my $group = $input->param('group');
496     my $subgroup = $input->param('subgroup');
497     my $sql   = $input->param('sql');
498     my $name  = $input->param('reportname');
499     my $type  = $input->param('types');
500     my $notes = $input->param('notes');
501     my $cache_expiry = $input->param('cache_expiry');
502     my $cache_expiry_units = $input->param('cache_expiry_units');
503     my $public = $input->param('public');
504     my $save_anyway = $input->param('save_anyway');
505
506
507     # if we have the units, then we came from creating a report from SQL and thus need to handle converting units
508     if( $cache_expiry_units ){
509       if( $cache_expiry_units eq "minutes" ){
510         $cache_expiry *= 60;
511       } elsif( $cache_expiry_units eq "hours" ){
512         $cache_expiry *= 3600; # 60 * 60
513       } elsif( $cache_expiry_units eq "days" ){
514         $cache_expiry *= 86400; # 60 * 60 * 24
515       }
516     }
517     # check $cache_expiry isnt too large, Memcached::set requires it to be less than 30 days or it will be treated as if it were an absolute time stamp
518     if( $cache_expiry && $cache_expiry >= 2592000 ){
519       push @errors, {cache_expiry => $cache_expiry};
520     }
521
522     create_non_existing_group_and_subgroup($input, $group, $subgroup);
523
524     ## FIXME this is AFTER entering a name to save the report under
525     if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
526         push @errors, {sqlerr => $1};
527     }
528     elsif ($sql !~ /^(SELECT)/i) {
529         push @errors, {queryerr => "No SELECT"};
530     }
531
532     if (@errors) {
533         $template->param(
534             'errors'    => \@errors,
535             'sql'       => $sql,
536             'reportname'=> $name,
537             'type'      => $type,
538             'notes'     => $notes,
539             'cache_expiry' => $cache_expiry,
540             'public'    => $public,
541         );
542     } else {
543         # Check defined SQL parameters for authorised value validity
544         my $problematic_authvals = ValidateSQLParameters($sql);
545
546         if ( scalar @$problematic_authvals > 0 && not $save_anyway ) {
547             # There's at least one problematic parameter, report to the
548             # GUI and provide all user input for further actions
549             $template->param(
550                 'area' => $area,
551                 'group' =>  $group,
552                 'subgroup' => $subgroup,
553                 'sql' => $sql,
554                 'reportname' => $name,
555                 'type' => $type,
556                 'notes' => $notes,
557                 'cache_expiry' => $cache_expiry,
558                 'cache_expiry_units' => $cache_expiry_units,
559                 'public' => $public,
560                 'problematic_authvals' => $problematic_authvals,
561                 'warn_authval_problem' => 1,
562                 'phase_save' => 1
563             );
564         } else {
565             # No params problem found or asked to save anyway
566             my $id = save_report( {
567                     borrowernumber => $borrowernumber,
568                     sql            => $sql,
569                     name           => $name,
570                     area           => $area,
571                     group          => $group,
572                     subgroup       => $subgroup,
573                     type           => $type,
574                     notes          => $notes,
575                     cache_expiry   => $cache_expiry,
576                     public         => $public,
577                 } );
578             $template->param(
579                 'save_successful' => 1,
580                 'reportname'      => $name,
581                 'id'              => $id,
582             );
583         }
584     }
585 }
586
587 elsif ($phase eq 'Run this report'){
588     # execute a saved report
589     my $limit      = $input->param('limit') || 20;
590     my $offset     = 0;
591     my $report_id  = $input->param('reports');
592     my @sql_params = $input->param('sql_params');
593     # offset algorithm
594     if ($input->param('page')) {
595         $offset = ($input->param('page') - 1) * $limit;
596     }
597
598     $template->param(
599         'limit'   => $limit,
600         'report_id' => $report_id,
601     );
602
603     my ( $sql, $type, $name, $notes );
604     if (my $report = get_saved_report($report_id)) {
605         $sql   = $report->{savedsql};
606         $name  = $report->{report_name};
607         $notes = $report->{notes};
608
609         my @rows = ();
610         # if we have at least 1 parameter, and it's not filled, then don't execute but ask for parameters
611         if ($sql =~ /<</ && !@sql_params) {
612             # split on ??. Each odd (2,4,6,...) entry should be a parameter to fill
613             my @split = split /<<|>>/,$sql;
614             my @tmpl_parameters;
615             my @authval_errors;
616             for(my $i=0;$i<($#split/2);$i++) {
617                 my ($text,$authorised_value) = split /\|/,$split[$i*2+1];
618                 my $input;
619                 my $labelid;
620                 if ( not defined $authorised_value ) {
621                     # no authorised value input, provide a text box
622                     $input = "text";
623                 } elsif ( $authorised_value eq "date" ) {
624                     # require a date, provide a date picker
625                     $input = 'date';
626                 } else {
627                     # defined $authorised_value, and not 'date'
628                     my $dbh=C4::Context->dbh;
629                     my @authorised_values;
630                     my %authorised_lib;
631                     # builds list, depending on authorised value...
632                     if ( $authorised_value eq "branches" ) {
633                         my $branches = GetBranchesLoop();
634                         foreach my $thisbranch (@$branches) {
635                             push @authorised_values, $thisbranch->{value};
636                             $authorised_lib{$thisbranch->{value}} = $thisbranch->{branchname};
637                         }
638                     }
639                     elsif ( $authorised_value eq "itemtypes" ) {
640                         my $sth = $dbh->prepare("SELECT itemtype,description FROM itemtypes ORDER BY description");
641                         $sth->execute;
642                         while ( my ( $itemtype, $description ) = $sth->fetchrow_array ) {
643                             push @authorised_values, $itemtype;
644                             $authorised_lib{$itemtype} = $description;
645                         }
646                     }
647                     elsif ( $authorised_value eq "biblio_framework" ) {
648                         my $frameworks = GetFrameworksLoop();
649                         my $default_source = '';
650                         push @authorised_values,$default_source;
651                         $authorised_lib{$default_source} = 'Default';
652                         foreach my $framework (@$frameworks) {
653                             push @authorised_values, $framework->{value};
654                             $authorised_lib{$framework->{value}} = $framework->{description};
655                         }
656                     }
657                     elsif ( $authorised_value eq "cn_source" ) {
658                         my $class_sources = GetClassSources();
659                         my $default_source = C4::Context->preference("DefaultClassificationSource");
660                         foreach my $class_source (sort keys %$class_sources) {
661                             next unless $class_sources->{$class_source}->{'used'} or
662                                         ($class_source eq $default_source);
663                             push @authorised_values, $class_source;
664                             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
665                         }
666                     }
667                     elsif ( $authorised_value eq "categorycode" ) {
668                         my $sth = $dbh->prepare("SELECT categorycode, description FROM categories ORDER BY description");
669                         $sth->execute;
670                         while ( my ( $categorycode, $description ) = $sth->fetchrow_array ) {
671                             push @authorised_values, $categorycode;
672                             $authorised_lib{$categorycode} = $description;
673                         }
674
675                         #---- "true" authorised value
676                     }
677                     else {
678                         if ( IsAuthorisedValueCategory($authorised_value) ) {
679                             my $query = '
680                             SELECT authorised_value,lib
681                             FROM authorised_values
682                             WHERE category=?
683                             ORDER BY lib
684                             ';
685                             my $authorised_values_sth = $dbh->prepare($query);
686                             $authorised_values_sth->execute( $authorised_value);
687
688                             while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
689                                 push @authorised_values, $value;
690                                 $authorised_lib{$value} = $lib;
691                                 # For item location, we show the code and the libelle
692                                 $authorised_lib{$value} = $lib;
693                             }
694                         } else {
695                             # not exists $authorised_value_categories{$authorised_value})
696                             push @authval_errors, {'entry' => $text,
697                                                    'auth_val' => $authorised_value };
698                             # tell the template there's an error
699                             $template->param( auth_val_error => 1 );
700                             # skip scrolling list creation and params push
701                             next;
702                         }
703                     }
704                     $labelid = $text;
705                     $labelid =~ s/\W//g;
706                     $input =CGI::scrolling_list(      # FIXME: factor out scrolling_list
707                         -name     => "sql_params",
708                         -id       => "sql_params_".$labelid,
709                         -values   => \@authorised_values,
710 #                     -default  => $value,
711                         -labels   => \%authorised_lib,
712                         -override => 1,
713                         -size     => 1,
714                         -multiple => 0,
715                         -tabindex => 1,
716                     );
717                 }
718
719                 push @tmpl_parameters, {'entry' => $text, 'input' => $input, 'labelid' => $labelid };
720             }
721             $template->param('sql'         => $sql,
722                             'name'         => $name,
723                             'sql_params'   => \@tmpl_parameters,
724                             'auth_val_errors'  => \@authval_errors,
725                             'enter_params' => 1,
726                             'reports'      => $report_id,
727                             );
728         } else {
729             # OK, we have parameters, or there are none, we run the report
730             # if there were parameters, replace before running
731             # split on ??. Each odd (2,4,6,...) entry should be a parameter to fill
732             my @split = split /<<|>>/,$sql;
733             my @tmpl_parameters;
734             for(my $i=0;$i<$#split/2;$i++) {
735                 my $quoted = C4::Context->dbh->quote($sql_params[$i]);
736                 # if there are special regexp chars, we must \ them
737                 $split[$i*2+1] =~ s/(\||\?|\.|\*|\(|\)|\%)/\\$1/g;
738                 $sql =~ s/<<$split[$i*2+1]>>/$quoted/;
739             }
740             my ($sth, $errors) = execute_query($sql, $offset, $limit);
741             my $total = nb_rows($sql) || 0;
742             unless ($sth) {
743                 die "execute_query failed to return sth for report $report_id: $sql";
744             } else {
745                 my $headers= header_cell_loop($sth);
746                 $template->param(header_row => $headers);
747                 while (my $row = $sth->fetchrow_arrayref()) {
748                     my @cells = map { +{ cell => $_ } } @$row;
749                     push @rows, { cells => \@cells };
750                 }
751             }
752
753             my $totpages = int($total/$limit) + (($total % $limit) > 0 ? 1 : 0);
754             my $url = "/cgi-bin/koha/reports/guided_reports.pl?reports=$report_id&amp;phase=Run%20this%20report&amp;limit=$limit";
755             if (@sql_params) {
756                 $url = join('&amp;sql_params=', $url, map { URI::Escape::uri_escape($_) } @sql_params);
757             }
758             $template->param(
759                 'results' => \@rows,
760                 'sql'     => $sql,
761                 'id'      => $report_id,
762                 'execute' => 1,
763                 'name'    => $name,
764                 'notes'   => $notes,
765                 'errors'  => defined($errors) ? [ $errors ] : undef,
766                 'pagination_bar'  => pagination_bar($url, $totpages, $input->param('page')),
767                 'unlimited_total' => $total,
768                 'sql_params'      => \@sql_params,
769             );
770         }
771     }
772     else {
773         push @errors, { no_sql_for_id => $report_id };
774     }
775 }
776
777 elsif ($phase eq 'Export'){
778
779         # export results to tab separated text or CSV
780         my $sql    = $input->param('sql');  # FIXME: use sql from saved report ID#, not new user-supplied SQL!
781     my $format = $input->param('format');
782         my ($sth, $q_errors) = execute_query($sql);
783     unless ($q_errors and @$q_errors) {
784         my ( $type, $content );
785         if ($format eq 'tab') {
786             $type = 'application/octet-stream';
787             $content .= join("\t", header_cell_values($sth)) . "\n";
788             while (my $row = $sth->fetchrow_arrayref()) {
789                 $content .= join("\t", @$row) . "\n";
790             }
791         } else {
792             my $delimiter = C4::Context->preference('delimiter') || ',';
793             if ( $format eq 'csv' ) {
794                 $type = 'application/csv';
795                 my $csv = Text::CSV::Encoded->new({ encoding_out => 'utf8', sep_char => $delimiter});
796                 $csv or die "Text::CSV::Encoded->new({binary => 1}) FAILED: " . Text::CSV::Encoded->error_diag();
797                 if ($csv->combine(header_cell_values($sth))) {
798                     $content .= $csv->string(). "\n";
799                 } else {
800                     push @$q_errors, { combine => 'HEADER ROW: ' . $csv->error_diag() } ;
801                 }
802                 while (my $row = $sth->fetchrow_arrayref()) {
803                     if ($csv->combine(@$row)) {
804                         $content .= $csv->string() . "\n";
805                     } else {
806                         push @$q_errors, { combine => $csv->error_diag() } ;
807                     }
808                 }
809             }
810             elsif ( $format eq 'ods' ) {
811                 $type = 'application/vnd.oasis.opendocument.spreadsheet';
812                 my $ods_fh = File::Temp->new( UNLINK => 0 );
813                 my $ods_filepath = $ods_fh->filename;
814
815                 use OpenOffice::OODoc;
816                 my $tmpdir = dirname $ods_filepath;
817                 odfWorkingDirectory( $tmpdir );
818                 my $container = odfContainer( $ods_filepath, create => 'spreadsheet' );
819                 my $doc = odfDocument (
820                     container => $container,
821                     part      => 'content'
822                 );
823                 my $table = $doc->getTable(0);
824                 my @headers = header_cell_values( $sth );
825                 my $rows = $sth->fetchall_arrayref();
826                 my ( $nb_rows, $nb_cols ) = ( 0, 0 );
827                 $nb_rows = @$rows;
828                 $nb_cols = @headers;
829                 $doc->expandTable( $table, $nb_rows + 1, $nb_cols );
830
831                 my $row = $doc->getRow( $table, 0 );
832                 my $j = 0;
833                 for my $header ( @headers ) {
834                     $doc->cellValue( $row, $j, $header );
835                     $j++;
836                 }
837                 my $i = 1;
838                 for ( @$rows ) {
839                     $row = $doc->getRow( $table, $i );
840                     for ( my $j = 0 ; $j < $nb_cols ; $j++ ) {
841                         my $value = Encode::encode( 'UTF8', $rows->[$i - 1][$j] );
842                         $doc->cellValue( $row, $j, $value );
843                     }
844                     $i++;
845                 }
846                 $doc->save();
847                 binmode(STDOUT);
848                 open $ods_fh, '<', $ods_filepath;
849                 $content .= $_ while <$ods_fh>;
850                 unlink $ods_filepath;
851             }
852         }
853         print $input->header(
854             -type => $type,
855             -attachment=>"reportresults.$format"
856         );
857         print $content;
858
859         foreach my $err (@$q_errors, @errors) {
860             print "# ERROR: " . (map {$_ . ": " . $err->{$_}} keys %$err) . "\n";
861         }   # here we print all the non-fatal errors at the end.  Not super smooth, but better than nothing.
862         exit;
863     }
864     $template->param(
865         'sql'           => $sql,
866         'execute'       => 1,
867         'name'          => 'Error exporting report!',
868         'notes'         => '',
869         'errors'        => $q_errors,
870     );
871 }
872
873 elsif ( $phase eq 'Create report from SQL' ) {
874
875     my ($group, $subgroup);
876     # allow the user to paste in sql
877     if ( $input->param('sql') ) {
878         $group = $input->param('report_group');
879         $subgroup  = $input->param('report_subgroup');
880         $template->param(
881             'sql'           => $input->param('sql') // '',
882             'reportname'    => $input->param('reportname') // '',
883             'notes'         => $input->param('notes') // '',
884         );
885     }
886     $template->param(
887         'create' => 1,
888         'groups_with_subgroups' => groups_with_subgroups($group, $subgroup),
889         'public' => '0',
890         'cache_expiry' => 300,
891         'usecache' => $usecache,
892     );
893 }
894
895 elsif ($phase eq 'Create Compound Report'){
896         $template->param( 'savedreports' => get_saved_reports(),
897                 'compound' => 1,
898         );
899 }
900
901 elsif ($phase eq 'Save Compound'){
902     my $master    = $input->param('master');
903         my $subreport = $input->param('subreport');
904         my ($mastertables,$subtables) = create_compound($master,$subreport);
905         $template->param( 'save_compound' => 1,
906                 master=>$mastertables,
907                 subsql=>$subtables
908         );
909 }
910
911 # pass $sth, get back an array of names for the column headers
912 sub header_cell_values {
913     my $sth = shift or return ();
914     my @cols;
915     foreach my $c (@{$sth->{NAME}}) {
916         # TODO in Bug 11944
917         #FIXME apparently DBI still needs a utf8 fix for this?
918         utf8::decode($c);
919         push @cols, $c;
920     }
921     return @cols;
922 }
923
924 # pass $sth, get back a TMPL_LOOP-able set of names for the column headers
925 sub header_cell_loop {
926     my @headers = map { +{ cell => $_ } } header_cell_values (shift);
927     return \@headers;
928 }
929
930 foreach (1..6) {
931      $template->{VARS}->{'build' . $_} and $template->{VARS}->{'buildx' . $_} and last;
932 }
933 $template->param(   'referer' => $input->referer(),
934                 );
935
936 output_html_with_http_headers $input, $cookie, $template->output;
937
938 sub groups_with_subgroups {
939     my ($group, $subgroup) = @_;
940
941     my $groups_with_subgroups = get_report_groups();
942     my @g_sg;
943     my @sorted_keys = sort {
944         $groups_with_subgroups->{$a}->{name} cmp $groups_with_subgroups->{$b}->{name}
945     } keys %$groups_with_subgroups;
946     foreach my $g_id (@sorted_keys) {
947         my $v = $groups_with_subgroups->{$g_id};
948         my @subgroups;
949         if (my $sg = $v->{subgroups}) {
950             foreach my $sg_id (sort { $sg->{$a} cmp $sg->{$b} } keys %$sg) {
951                 push @subgroups, {
952                     id => $sg_id,
953                     name => $sg->{$sg_id},
954                     selected => ($group && $g_id eq $group && $subgroup && $sg_id eq $subgroup ),
955                 };
956             }
957         }
958         push @g_sg, {
959             id => $g_id,
960             name => $v->{name},
961             selected => ($group && $g_id eq $group),
962             subgroups => \@subgroups,
963         };
964     }
965     return \@g_sg;
966 }
967
968 sub create_non_existing_group_and_subgroup {
969     my ($input, $group, $subgroup) = @_;
970
971     if (defined $group and $group ne '') {
972         my $report_groups = C4::Reports::Guided::get_report_groups;
973         if (not exists $report_groups->{$group}) {
974             my $groupdesc = $input->param('groupdesc') // $group;
975             C4::Koha::AddAuthorisedValue('REPORT_GROUP', $group, $groupdesc);
976         }
977         if (defined $subgroup and $subgroup ne '') {
978             if (not exists $report_groups->{$group}->{subgroups}->{$subgroup}) {
979                 my $subgroupdesc = $input->param('subgroupdesc') // $subgroup;
980                 C4::Koha::AddAuthorisedValue('REPORT_SUBGROUP', $subgroup, $subgroupdesc, $group);
981             }
982         }
983     }
984 }