Bug 11672: Untranslatable dropdown on Guided Reports and dictionary
[koha.git] / C4 / Reports / Guided.pm
1 package C4::Reports::Guided;
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 strict;
21 #use warnings; FIXME - Bug 2505 this module needs a lot of repair to run clean under warnings
22 use CGI;
23 use Carp;
24
25 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
26 use C4::Context;
27 use C4::Dates qw/format_date format_date_in_iso/;
28 use C4::Templates qw/themelanguage/;
29 use C4::Koha;
30 use C4::Output;
31 use XML::Simple;
32 use XML::Dumper;
33 use C4::Debug;
34 # use Smart::Comments;
35 # use Data::Dumper;
36
37 BEGIN {
38     # set the version for version checking
39     $VERSION = 3.07.00.049;
40     require Exporter;
41     @ISA    = qw(Exporter);
42     @EXPORT = qw(
43       get_report_types get_report_areas get_report_groups get_columns build_query get_criteria
44       save_report get_saved_reports execute_query get_saved_report create_compound run_compound
45       get_column_type get_distinct_values save_dictionary get_from_dictionary
46       delete_definition delete_report format_results get_sql
47       nb_rows update_sql build_authorised_value_list
48       GetReservedAuthorisedValues
49       GetParametersFromSQL
50       IsAuthorisedValueValid
51       ValidateSQLParameters
52     );
53 }
54
55 =head1 NAME
56
57 C4::Reports::Guided - Module for generating guided reports 
58
59 =head1 SYNOPSIS
60
61   use C4::Reports::Guided;
62
63 =head1 DESCRIPTION
64
65 =cut
66
67 =head1 METHODS
68
69 =head2 get_report_areas
70
71 This will return a list of all the available report areas
72
73 =cut
74
75 my @REPORT_AREA = (
76     [CIRC => "Circulation"],
77     [CAT  => "Catalogue"],
78     [PAT  => "Patrons"],
79     [ACQ  => "Acquisition"],
80     [ACC  => "Accounts"],
81 );
82 my $AREA_NAME_SQL_SNIPPET
83   = "CASE report_area " .
84     join (" ", map "WHEN '$_->[0]' THEN '$_->[1]'", @REPORT_AREA) .
85     " END AS areaname";
86
87 sub get_report_areas {
88
89     my $report_areas = [ 'CIRC', 'CAT', 'PAT', 'ACQ', 'ACC' ];
90
91     return $report_areas;
92 }
93
94 my %table_areas = (
95     CIRC => [ 'borrowers', 'statistics', 'items', 'biblioitems' ],
96     CAT  => [ 'items', 'biblioitems', 'biblio' ],
97     PAT  => ['borrowers'],
98     ACQ  => [ 'aqorders', 'biblio', 'items' ],
99     ACC  => [ 'borrowers', 'accountlines' ],
100 );
101 my %keys = (
102     CIRC => [ 'statistics.borrowernumber=borrowers.borrowernumber',
103               'items.itemnumber = statistics.itemnumber',
104               'biblioitems.biblioitemnumber = items.biblioitemnumber' ],
105     CAT  => [ 'items.biblioitemnumber=biblioitems.biblioitemnumber',
106               'biblioitems.biblionumber=biblio.biblionumber' ],
107     PAT  => [],
108     ACQ  => [ 'aqorders.biblionumber=biblio.biblionumber',
109               'biblio.biblionumber=items.biblionumber' ],
110     ACC  => ['borrowers.borrowernumber=accountlines.borrowernumber'],
111 );
112
113 # have to do someting here to know if its dropdown, free text, date etc
114 my %criteria = (
115     CIRC => [ 'statistics.type', 'borrowers.categorycode', 'statistics.branch',
116               'biblioitems.publicationyear|date', 'items.dateaccessioned|date' ],
117     CAT  => [ 'items.itemnumber|textrange', 'items.biblionumber|textrange',
118               'items.barcode|textrange', 'biblio.frameworkcode',
119               'items.holdingbranch', 'items.homebranch',
120               'biblio.datecreated|daterange', 'biblio.timestamp|daterange',
121               'items.onloan|daterange', 'items.ccode',
122               'items.itemcallnumber|textrange', 'items.itype', 'items.itemlost',
123               'items.location' ],
124     PAT  => [ 'borrowers.branchcode', 'borrowers.categorycode' ],
125     ACQ  => ['aqorders.datereceived|date'],
126     ACC  => [ 'borrowers.branchcode', 'borrowers.categorycode' ],
127 );
128
129 # Adds itemtypes to criteria, according to the syspref
130 if ( C4::Context->preference('item-level_itypes') ) {
131     unshift @{ $criteria{'CIRC'} }, 'items.itype';
132     unshift @{ $criteria{'CAT'} }, 'items.itype';
133 } else {
134     unshift @{ $criteria{'CIRC'} }, 'biblioitems.itemtype';
135     unshift @{ $criteria{'CAT'} }, 'biblioitems.itemtype';
136 }
137
138 =head2 get_report_types
139
140 This will return a list of all the available report types
141
142 =cut
143
144 sub get_report_types {
145     my $dbh = C4::Context->dbh();
146
147     # FIXME these should be in the database perhaps
148     my @reports = ( 'Tabular', 'Summary', 'Matrix' );
149     my @reports2;
150     for ( my $i = 0 ; $i < 3 ; $i++ ) {
151         my %hashrep;
152         $hashrep{id}   = $i + 1;
153         $hashrep{name} = $reports[$i];
154         push @reports2, \%hashrep;
155     }
156     return ( \@reports2 );
157
158 }
159
160 =head2 get_report_groups
161
162 This will return a list of all the available report areas with groups
163
164 =cut
165
166 sub get_report_groups {
167     my $dbh = C4::Context->dbh();
168
169     my $groups = GetAuthorisedValues('REPORT_GROUP');
170     my $subgroups = GetAuthorisedValues('REPORT_SUBGROUP');
171
172     my %groups_with_subgroups = map { $_->{authorised_value} => {
173                         name => $_->{lib},
174                         groups => {}
175                     } } @$groups;
176     foreach (@$subgroups) {
177         my $sg = $_->{authorised_value};
178         my $g = $_->{lib_opac}
179           or warn( qq{REPORT_SUBGROUP "$sg" without REPORT_GROUP (lib_opac)} ),
180              next;
181         my $g_sg = $groups_with_subgroups{$g}
182           or warn( qq{REPORT_SUBGROUP "$sg" with invalid REPORT_GROUP "$g"} ),
183              next;
184         $g_sg->{subgroups}{$sg} = $_->{lib};
185     }
186     return \%groups_with_subgroups
187 }
188
189 =head2 get_all_tables
190
191 This will return a list of all tables in the database 
192
193 =cut
194
195 sub get_all_tables {
196     my $dbh   = C4::Context->dbh();
197     my $query = "SHOW TABLES";
198     my $sth   = $dbh->prepare($query);
199     $sth->execute();
200     my @tables;
201     while ( my $data = $sth->fetchrow_arrayref() ) {
202         push @tables, $data->[0];
203     }
204     $sth->finish();
205     return ( \@tables );
206
207 }
208
209 =head2 get_columns($area)
210
211 This will return a list of all columns for a report area
212
213 =cut
214
215 sub get_columns {
216
217     # this calls the internal fucntion _get_columns
218     my ( $area, $cgi ) = @_;
219     my $tables = $table_areas{$area}
220       or die qq{Unsuported report area "$area"};
221
222     my @allcolumns;
223     my $first = 1;
224     foreach my $table (@$tables) {
225         my @columns = _get_columns($table,$cgi, $first);
226         $first = 0;
227         push @allcolumns, @columns;
228     }
229     return ( \@allcolumns );
230 }
231
232 sub _get_columns {
233     my ($tablename,$cgi, $first) = @_;
234     my $dbh         = C4::Context->dbh();
235     my $sth         = $dbh->prepare("show columns from $tablename");
236     $sth->execute();
237     my @columns;
238         my $column_defs = _get_column_defs($cgi);
239         my %tablehash;
240         $tablehash{'table'}=$tablename;
241     $tablehash{'__first__'} = $first;
242         push @columns, \%tablehash;
243     while ( my $data = $sth->fetchrow_arrayref() ) {
244         my %temphash;
245         $temphash{'name'}        = "$tablename.$data->[0]";
246         $temphash{'description'} = $column_defs->{"$tablename.$data->[0]"};
247         push @columns, \%temphash;
248     }
249     $sth->finish();
250     return (@columns);
251 }
252
253 =head2 build_query($columns,$criteria,$orderby,$area)
254
255 This will build the sql needed to return the results asked for, 
256 $columns is expected to be of the format tablename.columnname.
257 This is what get_columns returns.
258
259 =cut
260
261 sub build_query {
262     my ( $columns, $criteria, $orderby, $area, $totals, $definition ) = @_;
263 ### $orderby
264     my $keys   = $keys{$area};
265     my $tables = $table_areas{$area};
266
267     my $sql =
268       _build_query( $tables, $columns, $criteria, $keys, $orderby, $totals, $definition );
269     return ($sql);
270 }
271
272 sub _build_query {
273     my ( $tables, $columns, $criteria, $keys, $orderby, $totals, $definition) = @_;
274 ### $orderby
275     # $keys is an array of joining constraints
276     my $dbh           = C4::Context->dbh();
277     my $joinedtables  = join( ',', @$tables );
278     my $joinedcolumns = join( ',', @$columns );
279     my $query =
280       "SELECT $totals $joinedcolumns FROM $tables->[0] ";
281         for (my $i=1;$i<@$tables;$i++){
282                 $query .= "LEFT JOIN $tables->[$i] on ($keys->[$i-1]) ";
283         }
284
285     if ($criteria) {
286                 $criteria =~ s/AND/WHERE/;
287         $query .= " $criteria";
288     }
289         if ($definition){
290                 my @definitions = split(',',$definition);
291                 my $deftext;
292                 foreach my $def (@definitions){
293                         my $defin=get_from_dictionary('',$def);
294                         $deftext .=" ".$defin->[0]->{'saved_sql'};
295                 }
296                 if ($query =~ /WHERE/i){
297                         $query .= $deftext;
298                 }
299                 else {
300                         $deftext  =~ s/AND/WHERE/;
301                         $query .= $deftext;                     
302                 }
303         }
304     if ($totals) {
305         my $groupby;
306         my @totcolumns = split( ',', $totals );
307         foreach my $total (@totcolumns) {
308             if ( $total =~ /\((.*)\)/ ) {
309                 if ( $groupby eq '' ) {
310                     $groupby = " GROUP BY $1";
311                 }
312                 else {
313                     $groupby .= ",$1";
314                 }
315             }
316         }
317         $query .= $groupby;
318     }
319     if ($orderby) {
320         $query .= $orderby;
321     }
322     return ($query);
323 }
324
325 =head2 get_criteria($area,$cgi);
326
327 Returns an arraref to hashrefs suitable for using in a tmpl_loop. With the criteria and available values.
328
329 =cut
330
331 sub get_criteria {
332     my ($area,$cgi) = @_;
333     my $dbh    = C4::Context->dbh();
334     my $crit   = $criteria{$area};
335     my $column_defs = _get_column_defs($cgi);
336     my @criteria_array;
337     foreach my $localcrit (@$crit) {
338         my ( $value, $type )   = split( /\|/, $localcrit );
339         my ( $table, $column ) = split( /\./, $value );
340         if ($type eq 'textrange') {
341             my %temp;
342             $temp{'name'}        = $value;
343             $temp{'from'}        = "from_" . $value;
344             $temp{'to'}          = "to_" . $value;
345             $temp{'textrange'}   = 1;
346             $temp{'description'} = $column_defs->{$value};
347             push @criteria_array, \%temp;
348         }
349         elsif ($type eq 'date') {
350             my %temp;
351             $temp{'name'}        = $value;
352             $temp{'date'}        = 1;
353             $temp{'description'} = $column_defs->{$value};
354             push @criteria_array, \%temp;
355         }
356         elsif ($type eq 'daterange') {
357             my %temp;
358             $temp{'name'}        = $value;
359             $temp{'from'}        = "from_" . $value;
360             $temp{'to'}          = "to_" . $value;
361             $temp{'daterange'}   = 1;
362             $temp{'description'} = $column_defs->{$value};
363             push @criteria_array, \%temp;
364         }
365         else {
366             my $query =
367             "SELECT distinct($column) as availablevalues FROM $table";
368             my $sth = $dbh->prepare($query);
369             $sth->execute();
370             my @values;
371             # push the runtime choosing option
372             my $list;
373             $list='branches' if $column eq 'branchcode' or $column eq 'holdingbranch' or $column eq 'homebranch';
374             $list='categorycode' if $column eq 'categorycode';
375             $list='itemtype' if $column eq 'itype';
376             $list='ccode' if $column eq 'ccode';
377             # TODO : improve to let the librarian choose the description at runtime
378             push @values, { availablevalues => "<<$column".($list?"|$list":'').">>" };
379             while ( my $row = $sth->fetchrow_hashref() ) {
380                 push @values, $row;
381                 if ($row->{'availablevalues'} eq '') { $row->{'default'} = 1 };
382             }
383             $sth->finish();
384
385             my %temp;
386             $temp{'name'}        = $value;
387             $temp{'description'} = $column_defs->{$value};
388             $temp{'values'}      = \@values;
389
390             push @criteria_array, \%temp;
391         }
392
393     }
394     return ( \@criteria_array );
395 }
396
397 sub nb_rows {
398     my $sql = shift or return;
399     my $sth = C4::Context->dbh->prepare($sql);
400     $sth->execute();
401     my $rows = $sth->fetchall_arrayref();
402     return scalar (@$rows);
403 }
404
405 =head2 execute_query
406
407   ($sth, $error) = execute_query($sql, $offset, $limit[, \@sql_params])
408
409
410 This function returns a DBI statement handler from which the caller can
411 fetch the results of the SQL passed via C<$sql>.
412
413 If passed any query other than a SELECT, or if there is a DB error,
414 C<$errors> is returned, and is a hashref containing the error after this
415 manner:
416
417 C<$error->{'sqlerr'}> contains the offending SQL keyword.
418 C<$error->{'queryerr'}> contains the native db engine error returned
419 for the query.
420
421 C<$offset>, and C<$limit> are required parameters.
422
423 C<\@sql_params> is an optional list of parameter values to paste in.
424 The caller is reponsible for making sure that C<$sql> has placeholders
425 and that the number placeholders matches the number of parameters.
426
427 =cut
428
429 # returns $sql, $offset, $limit
430 # $sql returned will be transformed to:
431 #  ~ remove any LIMIT clause
432 #  ~ repace SELECT clause w/ SELECT count(*)
433
434 sub select_2_select_count {
435     # Modify the query passed in to create a count query... (I think this covers all cases -crn)
436     my ($sql) = strip_limit(shift) or return;
437     $sql =~ s/\bSELECT\W+(?:\w+\W+){1,}?FROM\b|\bSELECT\W\*\WFROM\b/SELECT count(*) FROM /ig;
438     return $sql;
439 }
440
441 # This removes the LIMIT from the query so that a custom one can be specified.
442 # Usage:
443 #   ($new_sql, $offset, $limit) = strip_limit($sql);
444 #
445 # Where:
446 #   $sql is the query to modify
447 #   $new_sql is the resulting query
448 #   $offset is the offset value, if the LIMIT was the two-argument form,
449 #       0 if it wasn't otherwise given.
450 #   $limit is the limit value
451 #
452 # Notes:
453 #   * This makes an effort to not break subqueries that have their own
454 #     LIMIT specified. It does that by only removing a LIMIT if it comes after
455 #     a WHERE clause (which isn't perfect, but at least should make more cases
456 #     work - subqueries with a limit in the WHERE will still break.)
457 #   * If your query doesn't have a WHERE clause then all LIMITs will be
458 #     removed. This may break some subqueries, but is hopefully rare enough
459 #     to not be a big issue.
460 sub strip_limit {
461     my ($sql) = @_;
462
463     return unless $sql;
464     return ($sql, 0, undef) unless $sql =~ /\bLIMIT\b/i;
465
466     # Two options: if there's no WHERE clause in the SQL, we simply capture
467     # any LIMIT that's there. If there is a WHERE, we make sure that we only
468     # capture a LIMIT after the last one. This prevents stomping on subqueries.
469     if ($sql !~ /\bWHERE\b/i) {
470         (my $res = $sql) =~ s/\bLIMIT\b\s*(\d+)(\s*\,\s*(\d+))?\s*/ /ig;
471         return ($res, (defined $2 ? $1 : 0), (defined $3 ? $3 : $1));
472     } else {
473         my $res = $sql;
474         $res =~ m/.*\bWHERE\b/gsi;
475         $res =~ s/\G(.*)\bLIMIT\b\s*(\d+)(\s*\,\s*(\d+))?\s*/$1 /is;
476         return ($res, (defined $3 ? $2 : 0), (defined $4 ? $4 : $2));
477     }
478 }
479
480 sub execute_query {
481
482     my ( $sql, $offset, $limit, $sql_params ) = @_;
483
484     $sql_params = [] unless defined $sql_params;
485
486     # check parameters
487     unless ($sql) {
488         carp "execute_query() called without SQL argument";
489         return;
490     }
491     $offset = 0    unless $offset;
492     $limit  = 999999 unless $limit;
493     $debug and print STDERR "execute_query($sql, $offset, $limit)\n";
494     if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
495         return (undef, {  sqlerr => $1} );
496     } elsif ($sql !~ /^\s*SELECT\b\s*/i) {
497         return (undef, { queryerr => 'Missing SELECT'} );
498     }
499
500     my ($useroffset, $userlimit);
501
502     # Grab offset/limit from user supplied LIMIT and drop the LIMIT so we can control pagination
503     ($sql, $useroffset, $userlimit) = strip_limit($sql);
504     $debug and warn sprintf "User has supplied (OFFSET,) LIMIT = %s, %s",
505         $useroffset,
506         (defined($userlimit ) ? $userlimit  : 'UNDEF');
507     $offset += $useroffset;
508     if (defined($userlimit)) {
509         if ($offset + $limit > $userlimit ) {
510             $limit = $userlimit - $offset;
511         } elsif ( ! $offset && $limit < $userlimit ) {
512             $limit = $userlimit;
513         }
514     }
515     $sql .= " LIMIT ?, ?";
516
517     my $sth = C4::Context->dbh->prepare($sql);
518     $sth->execute(@$sql_params, $offset, $limit);
519     return ( $sth, { queryerr => $sth->errstr } ) if ($sth->err);
520     return ( $sth );
521     # my @xmlarray = ... ;
522     # my $url = "/cgi-bin/koha/reports/guided_reports.pl?phase=retrieve%20results&id=$id";
523     # my $xml = XML::Dumper->new()->pl2xml( \@xmlarray );
524     # store_results($id,$xml);
525 }
526
527 =head2 save_report($sql,$name,$type,$notes)
528
529 Given some sql and a name this will saved it so that it can reused
530 Returns id of the newly created report
531
532 =cut
533
534 sub save_report {
535     my ($fields) = @_;
536     my $borrowernumber = $fields->{borrowernumber};
537     my $sql = $fields->{sql};
538     my $name = $fields->{name};
539     my $type = $fields->{type};
540     my $notes = $fields->{notes};
541     my $area = $fields->{area};
542     my $group = $fields->{group};
543     my $subgroup = $fields->{subgroup};
544     my $cache_expiry = $fields->{cache_expiry} || 300;
545     my $public = $fields->{public};
546
547     my $dbh = C4::Context->dbh();
548     $sql =~ s/(\s*\;\s*)$//;    # removes trailing whitespace and /;/
549     my $query = "INSERT INTO saved_sql (borrowernumber,date_created,last_modified,savedsql,report_name,report_area,report_group,report_subgroup,type,notes,cache_expiry,public)  VALUES (?,now(),now(),?,?,?,?,?,?,?,?,?)";
550     $dbh->do($query, undef, $borrowernumber, $sql, $name, $area, $group, $subgroup, $type, $notes, $cache_expiry, $public);
551
552     my $id = $dbh->selectrow_array("SELECT max(id) FROM saved_sql WHERE borrowernumber=? AND report_name=?", undef,
553                                    $borrowernumber, $name);
554     return $id;
555 }
556
557 sub update_sql {
558     my $id         = shift || croak "No Id given";
559     my $fields     = shift;
560     my $sql = $fields->{sql};
561     my $name = $fields->{name};
562     my $notes = $fields->{notes};
563     my $group = $fields->{group};
564     my $subgroup = $fields->{subgroup};
565     my $cache_expiry = $fields->{cache_expiry};
566     my $public = $fields->{public};
567
568     if( $cache_expiry >= 2592000 ){
569       die "Please specify a cache expiry less than 30 days\n";
570     }
571
572     my $dbh        = C4::Context->dbh();
573     $sql =~ s/(\s*\;\s*)$//;    # removes trailing whitespace and /;/
574     my $query = "UPDATE saved_sql SET savedsql = ?, last_modified = now(), report_name = ?, report_group = ?, report_subgroup = ?, notes = ?, cache_expiry = ?, public = ? WHERE id = ? ";
575     $dbh->do($query, undef, $sql, $name, $group, $subgroup, $notes, $cache_expiry, $public, $id );
576 }
577
578 sub store_results {
579         my ($id,$xml)=@_;
580         my $dbh = C4::Context->dbh();
581         my $query = "SELECT * FROM saved_reports WHERE report_id=?";
582         my $sth = $dbh->prepare($query);
583         $sth->execute($id);
584         if (my $data=$sth->fetchrow_hashref()){
585                 my $query2 = "UPDATE saved_reports SET report=?,date_run=now() WHERE report_id=?";
586                 my $sth2 = $dbh->prepare($query2);
587             $sth2->execute($xml,$id);
588         }
589         else {
590                 my $query2 = "INSERT INTO saved_reports (report_id,report,date_run) VALUES (?,?,now())";
591                 my $sth2 = $dbh->prepare($query2);
592                 $sth2->execute($id,$xml);
593         }
594 }
595
596 sub format_results {
597         my ($id) = @_;
598         my $dbh = C4::Context->dbh();
599         my $query = "SELECT * FROM saved_reports WHERE report_id = ?";
600         my $sth = $dbh->prepare($query);
601         $sth->execute($id);
602         my $data = $sth->fetchrow_hashref();
603         my $dump = new XML::Dumper;
604         my $perl = $dump->xml2pl( $data->{'report'} );
605         foreach my $row (@$perl) {
606                 my $htmlrow="<tr>";
607                 foreach my $key (keys %$row){
608                         $htmlrow .= "<td>$row->{$key}</td>";
609                 }
610                 $htmlrow .= "</tr>";
611                 $row->{'row'} = $htmlrow;
612         }
613         $sth->finish;
614         $query = "SELECT * FROM saved_sql WHERE id = ?";
615         $sth = $dbh->prepare($query);
616         $sth->execute($id);
617         $data = $sth->fetchrow_hashref();
618         return ($perl,$data->{'report_name'},$data->{'notes'}); 
619 }       
620
621 sub delete_report {
622     my (@ids) = @_;
623     return unless @ids;
624     my $dbh = C4::Context->dbh;
625     my $query = 'DELETE FROM saved_sql WHERE id IN (' . join( ',', ('?') x @ids ) . ')';
626     my $sth = $dbh->prepare($query);
627     return $sth->execute(@ids);
628 }
629
630 my $SAVED_REPORTS_BASE_QRY = <<EOQ;
631 SELECT s.*, r.report, r.date_run, $AREA_NAME_SQL_SNIPPET, av_g.lib AS groupname, av_sg.lib AS subgroupname,
632 b.firstname AS borrowerfirstname, b.surname AS borrowersurname
633 FROM saved_sql s
634 LEFT JOIN saved_reports r ON r.report_id = s.id
635 LEFT OUTER JOIN authorised_values av_g ON (av_g.category = 'REPORT_GROUP' AND av_g.authorised_value = s.report_group)
636 LEFT OUTER JOIN authorised_values av_sg ON (av_sg.category = 'REPORT_SUBGROUP' AND av_sg.lib_opac = s.report_group AND av_sg.authorised_value = s.report_subgroup)
637 LEFT OUTER JOIN borrowers b USING (borrowernumber)
638 EOQ
639 my $DATE_FORMAT = "%d/%m/%Y";
640 sub get_saved_reports {
641 # $filter is either { date => $d, author => $a, keyword => $kw, }
642 # or $keyword. Optional.
643     my ($filter) = @_;
644     $filter = { keyword => $filter } if $filter && !ref( $filter );
645     my ($group, $subgroup) = @_;
646
647     my $dbh   = C4::Context->dbh();
648     my $query = $SAVED_REPORTS_BASE_QRY;
649     my (@cond,@args);
650     if ($filter) {
651         if (my $date = $filter->{date}) {
652             $date = format_date_in_iso($date);
653             push @cond, "DATE(date_run) = ? OR
654                          DATE(date_created) = ? OR
655                          DATE(last_modified) = ? OR
656                          DATE(last_run) = ?";
657             push @args, $date, $date, $date, $date;
658         }
659         if (my $author = $filter->{author}) {
660             $author = "%$author%";
661             push @cond, "surname LIKE ? OR
662                          firstname LIKE ?";
663             push @args, $author, $author;
664         }
665         if (my $keyword = $filter->{keyword}) {
666             $keyword = "%$keyword%";
667             push @cond, "report LIKE ? OR
668                          report_name LIKE ? OR
669                          notes LIKE ? OR
670                          savedsql LIKE ?";
671             push @args, $keyword, $keyword, $keyword, $keyword;
672         }
673         if ($filter->{group}) {
674             push @cond, "report_group = ?";
675             push @args, $filter->{group};
676         }
677         if ($filter->{subgroup}) {
678             push @cond, "report_subgroup = ?";
679             push @args, $filter->{subgroup};
680         }
681     }
682     $query .= " WHERE ".join( " AND ", map "($_)", @cond ) if @cond;
683     $query .= " ORDER by date_created";
684     
685     my $result = $dbh->selectall_arrayref($query, {Slice => {}}, @args);
686
687     return $result;
688 }
689
690 sub get_saved_report {
691     my $dbh   = C4::Context->dbh();
692     my $query;
693     my $report_arg;
694     if ($#_ == 0 && ref $_[0] ne 'HASH') {
695         ($report_arg) = @_;
696         $query = " SELECT * FROM saved_sql WHERE id = ?";
697     } elsif (ref $_[0] eq 'HASH') {
698         my ($selector) = @_;
699         if ($selector->{name}) {
700             $query = " SELECT * FROM saved_sql WHERE report_name = ?";
701             $report_arg = $selector->{name};
702         } elsif ($selector->{id} || $selector->{id} eq '0') {
703             $query = " SELECT * FROM saved_sql WHERE id = ?";
704             $report_arg = $selector->{id};
705         } else {
706             return;
707         }
708     } else {
709         return;
710     }
711     return $dbh->selectrow_hashref($query, undef, $report_arg);
712 }
713
714 =head2 create_compound($masterID,$subreportID)
715
716 This will take 2 reports and create a compound report using both of them
717
718 =cut
719
720 sub create_compound {
721     my ( $masterID, $subreportID ) = @_;
722     my $dbh = C4::Context->dbh();
723
724     # get the reports
725     my $master = get_saved_report($masterID);
726     my $mastersql = $master->{savedsql};
727     my $mastertype = $master->{type};
728     my $sub = get_saved_report($subreportID);
729     my $subsql = $master->{savedsql};
730     my $subtype = $master->{type};
731
732     # now we have to do some checking to see how these two will fit together
733     # or if they will
734     my ( $mastertables, $subtables );
735     if ( $mastersql =~ / from (.*) where /i ) {
736         $mastertables = $1;
737     }
738     if ( $subsql =~ / from (.*) where /i ) {
739         $subtables = $1;
740     }
741     return ( $mastertables, $subtables );
742 }
743
744 =head2 get_column_type($column)
745
746 This takes a column name of the format table.column and will return what type it is
747 (free text, set values, date)
748
749 =cut
750
751 sub get_column_type {
752         my ($tablecolumn) = @_;
753         my ($table,$column) = split(/\./,$tablecolumn);
754         my $dbh = C4::Context->dbh();
755         my $catalog;
756         my $schema;
757
758         # mysql doesnt support a column selection, set column to %
759         my $tempcolumn='%';
760         my $sth = $dbh->column_info( $catalog, $schema, $table, $tempcolumn ) || die $dbh->errstr;
761         while (my $info = $sth->fetchrow_hashref()){
762                 if ($info->{'COLUMN_NAME'} eq $column){
763                         #column we want
764                         if ($info->{'TYPE_NAME'} eq 'CHAR' || $info->{'TYPE_NAME'} eq 'VARCHAR'){
765                                 $info->{'TYPE_NAME'} = 'distinct';
766                         }
767                         return $info->{'TYPE_NAME'};            
768                 }
769         }
770 }
771
772 =head2 get_distinct_values($column)
773
774 Given a column name, return an arrary ref of hashrefs suitable for use as a tmpl_loop 
775 with the distinct values of the column
776
777 =cut
778
779 sub get_distinct_values {
780         my ($tablecolumn) = @_;
781         my ($table,$column) = split(/\./,$tablecolumn);
782         my $dbh = C4::Context->dbh();
783         my $query =
784           "SELECT distinct($column) as availablevalues FROM $table";
785         my $sth = $dbh->prepare($query);
786         $sth->execute();
787     return $sth->fetchall_arrayref({});
788 }       
789
790 sub save_dictionary {
791     my ( $name, $description, $sql, $area ) = @_;
792     my $dbh   = C4::Context->dbh();
793     my $query = "INSERT INTO reports_dictionary (name,description,saved_sql,report_area,date_created,date_modified)
794   VALUES (?,?,?,?,now(),now())";
795     my $sth = $dbh->prepare($query);
796     $sth->execute($name,$description,$sql,$area) || return 0;
797     return 1;
798 }
799
800 my $DICTIONARY_BASE_QRY = <<EOQ;
801 SELECT d.*, $AREA_NAME_SQL_SNIPPET
802 FROM reports_dictionary d
803 EOQ
804 sub get_from_dictionary {
805     my ( $area, $id ) = @_;
806     my $dbh   = C4::Context->dbh();
807     my $query = $DICTIONARY_BASE_QRY;
808     if ($area) {
809         $query .= " WHERE report_area = ?";
810     } elsif ($id) {
811         $query .= " WHERE id = ?";
812     }
813     my $sth = $dbh->prepare($query);
814     if ($id) {
815         $sth->execute($id);
816     } elsif ($area) {
817         $sth->execute($area);
818     } else {
819         $sth->execute();
820     }
821     my @loop;
822     while ( my $data = $sth->fetchrow_hashref() ) {
823         push @loop, $data;
824     }
825     return ( \@loop );
826 }
827
828 sub delete_definition {
829         my ($id) = @_ or return;
830         my $dbh = C4::Context->dbh();
831         my $query = "DELETE FROM reports_dictionary WHERE id = ?";
832         my $sth = $dbh->prepare($query);
833         $sth->execute($id);
834 }
835
836 sub get_sql {
837         my ($id) = @_ or return;
838         my $dbh = C4::Context->dbh();
839         my $query = "SELECT * FROM saved_sql WHERE id = ?";
840         my $sth = $dbh->prepare($query);
841         $sth->execute($id);
842         my $data=$sth->fetchrow_hashref();
843         return $data->{'savedsql'};
844 }
845
846 sub _get_column_defs {
847     my ($cgi) = @_;
848     my %columns;
849     my $columns_def_file = "columns.def";
850     my $htdocs = C4::Context->config('intrahtdocs');
851     my $section = 'intranet';
852
853     # We need the theme and the lang
854     # Since columns.def is not in the modules directory, we cannot sent it for the $tmpl var
855     my ($theme, $lang, $availablethemes) = C4::Templates::themelanguage($htdocs, 'about.tt', $section, $cgi);
856
857     my $full_path_to_columns_def_file="$htdocs/$theme/$lang/$columns_def_file";
858     open (my $fh, $full_path_to_columns_def_file);
859     while ( my $input = <$fh> ){
860         chomp $input;
861         if ( $input =~ m|<field name="(.*)">(.*)</field>| ) {
862             my ( $field, $translation ) = ( $1, $2 );
863             $columns{$field} = $translation;
864         }
865     }
866     close $fh;
867     return \%columns;
868 }
869
870 =head2 build_authorised_value_list($authorised_value)
871
872 Returns an arrayref - hashref pair. The hashref consists of
873 various code => name lists depending on the $authorised_value.
874 The arrayref is the hashref keys, in appropriate order
875
876 =cut
877
878 sub build_authorised_value_list {
879     my ( $authorised_value ) = @_;
880
881     my $dbh = C4::Context->dbh;
882     my @authorised_values;
883     my %authorised_lib;
884
885     # builds list, depending on authorised value...
886     if ( $authorised_value eq "branches" ) {
887         my $branches = GetBranchesLoop();
888         foreach my $thisbranch (@$branches) {
889             push @authorised_values, $thisbranch->{value};
890             $authorised_lib{ $thisbranch->{value} } = $thisbranch->{branchname};
891         }
892     } elsif ( $authorised_value eq "itemtypes" ) {
893         my $sth = $dbh->prepare("SELECT itemtype,description FROM itemtypes ORDER BY description");
894         $sth->execute;
895         while ( my ( $itemtype, $description ) = $sth->fetchrow_array ) {
896             push @authorised_values, $itemtype;
897             $authorised_lib{$itemtype} = $description;
898         }
899     } elsif ( $authorised_value eq "cn_source" ) {
900         my $class_sources  = GetClassSources();
901         my $default_source = C4::Context->preference("DefaultClassificationSource");
902         foreach my $class_source ( sort keys %$class_sources ) {
903             next
904               unless $class_sources->{$class_source}->{'used'}
905                   or ( $class_source eq $default_source );
906             push @authorised_values, $class_source;
907             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
908         }
909     } elsif ( $authorised_value eq "categorycode" ) {
910         my $sth = $dbh->prepare("SELECT categorycode, description FROM categories ORDER BY description");
911         $sth->execute;
912         while ( my ( $categorycode, $description ) = $sth->fetchrow_array ) {
913             push @authorised_values, $categorycode;
914             $authorised_lib{$categorycode} = $description;
915         }
916
917         #---- "true" authorised value
918     } else {
919         my $authorised_values_sth = $dbh->prepare("SELECT authorised_value,lib FROM authorised_values WHERE category=? ORDER BY lib");
920
921         $authorised_values_sth->execute($authorised_value);
922
923         while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
924             push @authorised_values, $value;
925             $authorised_lib{$value} = $lib;
926
927             # For item location, we show the code and the libelle
928             $authorised_lib{$value} = $lib;
929         }
930     }
931
932     return (\@authorised_values, \%authorised_lib);
933 }
934
935 =head2 GetReservedAuthorisedValues
936
937     my %reserved_authorised_values = GetReservedAuthorisedValues();
938
939 Returns a hash containig all reserved words
940
941 =cut
942
943 sub GetReservedAuthorisedValues {
944     my %reserved_authorised_values =
945             map { $_ => 1 } ( 'date',
946                               'branches',
947                               'itemtypes',
948                               'cn_source',
949                               'categorycode' );
950
951    return \%reserved_authorised_values;
952 }
953
954
955 =head2 IsAuthorisedValueValid
956
957     my $is_valid_ath_value = IsAuthorisedValueValid($authorised_value)
958
959 Returns 1 if $authorised_value is on the reserved authorised values list or
960 in the authorised value categories defined in
961
962 =cut
963
964 sub IsAuthorisedValueValid {
965
966     my $authorised_value = shift;
967     my $reserved_authorised_values = GetReservedAuthorisedValues();
968
969     if ( exists $reserved_authorised_values->{$authorised_value} ||
970          IsAuthorisedValueCategory($authorised_value)   ) {
971         return 1;
972     }
973
974     return 0;
975 }
976
977 =head2 GetParametersFromSQL
978
979     my @sql_parameters = GetParametersFromSQL($sql)
980
981 Returns an arrayref of hashes containing the keys name and authval
982
983 =cut
984
985 sub GetParametersFromSQL {
986
987     my $sql = shift ;
988     my @split = split(/<<|>>/,$sql);
989     my @sql_parameters = ();
990
991     for ( my $i = 0; $i < ($#split/2) ; $i++ ) {
992         my ($name,$authval) = split(/\|/,$split[$i*2+1]);
993         push @sql_parameters, { 'name' => $name, 'authval' => $authval };
994     }
995
996     return \@sql_parameters;
997 }
998
999 =head2 ValidateSQLParameters
1000
1001     my @problematic_parameters = ValidateSQLParameters($sql)
1002
1003 Returns an arrayref of hashes containing the keys name and authval of
1004 those SQL parameters that do not correspond to valid authorised names
1005
1006 =cut
1007
1008 sub ValidateSQLParameters {
1009
1010     my $sql = shift;
1011     my @problematic_parameters = ();
1012     my $sql_parameters = GetParametersFromSQL($sql);
1013
1014     foreach my $sql_parameter (@$sql_parameters) {
1015         if ( defined $sql_parameter->{'authval'} ) {
1016             push @problematic_parameters, $sql_parameter unless
1017                 IsAuthorisedValueValid($sql_parameter->{'authval'});
1018         }
1019     }
1020
1021     return \@problematic_parameters;
1022 }
1023
1024 1;
1025 __END__
1026
1027 =head1 AUTHOR
1028
1029 Chris Cormack <crc@liblime.com>
1030
1031 =cut