Bug 5327 Starting Unit Tests for Auth.pm
[koha.git] / reports / guided_reports.pl
bloba53712e5bd040c4fc684417eb8f57fb5f5a7fb9a
1 #!/usr/bin/perl
3 # Copyright 2007 Liblime ltd
5 # This file is part of Koha.
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.
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.
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.
20 use strict;
21 #use warnings; FIXME - Bug 2505
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 use C4::Branch; # XXX subfield_is_koha_internal_p
31 =head1 NAME
33 guided_reports.pl
35 =head1 DESCRIPTION
37 Script to control the guided report creation
39 =cut
41 my $input = new CGI;
43 my $phase = $input->param('phase');
44 my $flagsrequired;
45 if ( $phase eq 'Build new' or $phase eq 'Delete Saved' ) {
46 $flagsrequired = 'create_reports';
48 elsif ( $phase eq 'Use saved' ) {
49 $flagsrequired = 'execute_reports';
50 } else {
51 $flagsrequired = '*';
54 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
56 template_name => "reports/guided_reports_start.tmpl",
57 query => $input,
58 type => "intranet",
59 authnotrequired => 0,
60 flagsrequired => { reports => $flagsrequired },
61 debug => 1,
65 my @errors = ();
66 if ( !$phase ) {
67 $template->param( 'start' => 1 );
68 # show welcome page
70 elsif ( $phase eq 'Build new' ) {
71 # build a new report
72 $template->param( 'build1' => 1 );
73 $template->param( 'areas' => get_report_areas() );
75 elsif ( $phase eq 'Use saved' ) {
76 # use a saved report
77 # get list of reports and display them
78 $template->param( 'saved1' => 1 );
79 $template->param( 'savedreports' => get_saved_reports() );
82 elsif ( $phase eq 'Delete Saved') {
84 # delete a report from the saved reports list
85 my $id = $input->param('reports');
86 delete_report($id);
87 print $input->redirect("/cgi-bin/koha/reports/guided_reports.pl?phase=Use%20saved");
88 exit;
91 elsif ( $phase eq 'Show SQL'){
93 my $id = $input->param('reports');
94 my $sql = get_sql($id);
95 $template->param(
96 'sql' => $sql,
97 'showsql' => 1,
101 elsif ( $phase eq 'Edit SQL'){
103 my $id = $input->param('reports');
104 my ($sql,$type,$reportname,$notes) = get_saved_report($id);
105 $template->param(
106 'sql' => $sql,
107 'reportname' => $reportname,
108 'notes' => $notes,
109 'id' => $id,
110 'editsql' => 1,
114 elsif ( $phase eq 'Update SQL'){
115 my $id = $input->param('id');
116 my $sql = $input->param('sql');
117 my $reportname = $input->param('reportname');
118 my $notes = $input->param('notes');
119 my @errors;
120 if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
121 push @errors, {sqlerr => $1};
123 elsif ($sql !~ /^(SELECT)/i) {
124 push @errors, {queryerr => 1};
126 if (@errors) {
127 $template->param(
128 'errors' => \@errors,
129 'sql' => $sql,
132 else {
133 update_sql( $id, $sql, $reportname, $notes );
134 $template->param(
135 'save_successful' => 1,
141 elsif ($phase eq 'retrieve results') {
142 my $id = $input->param('id');
143 my ($results,$name,$notes) = format_results($id);
144 # do something
145 $template->param(
146 'retresults' => 1,
147 'results' => $results,
148 'name' => $name,
149 'notes' => $notes,
153 elsif ( $phase eq 'Report on this Area' ) {
155 # they have choosen a new report and the area to report on
156 $template->param(
157 'build2' => 1,
158 'area' => $input->param('areas'),
159 'types' => get_report_types(),
163 elsif ( $phase eq 'Choose this type' ) {
165 # they have chosen type and area
166 # get area and type and pass them to the template
167 my $area = $input->param('area');
168 my $type = $input->param('types');
169 $template->param(
170 'build3' => 1,
171 'area' => $area,
172 'type' => $type,
173 columns => get_columns($area,$input),
177 elsif ( $phase eq 'Choose these columns' ) {
179 # we now know type, area, and columns
180 # next step is the constraints
181 my $area = $input->param('area');
182 my $type = $input->param('type');
183 my @columns = $input->param('columns');
184 my $column = join( ',', @columns );
185 $template->param(
186 'build4' => 1,
187 'area' => $area,
188 'type' => $type,
189 'column' => $column,
190 definitions => get_from_dictionary($area),
191 criteria => get_criteria($area,$input),
195 elsif ( $phase eq 'Choose these criteria' ) {
196 my $area = $input->param('area');
197 my $type = $input->param('type');
198 my $column = $input->param('column');
199 my @definitions = $input->param('definition');
200 my $definition = join (',',@definitions);
201 my @criteria = $input->param('criteria_column');
202 my $query_criteria;
203 foreach my $crit (@criteria) {
204 my $value = $input->param( $crit . "_value" );
206 # If value is not defined, then it may be range values
207 if (!defined $value) {
209 my $fromvalue = $input->param( "from_" . $crit . "_value" );
210 my $tovalue = $input->param( "to_" . $crit . "_value" );
212 # If the range values are dates
213 if ($fromvalue =~ C4::Dates->regexp('syspref') && $tovalue =~ C4::Dates->regexp('syspref')) {
214 $fromvalue = C4::Dates->new($fromvalue)->output("iso");
215 $tovalue = C4::Dates->new($tovalue)->output("iso");
218 if ($fromvalue && $tovalue) {
219 $query_criteria .= " AND $crit >= '$fromvalue' AND $crit <= '$tovalue'";
222 } else {
224 # If value is a date
225 if ($value =~ C4::Dates->regexp('syspref')) {
226 $value = C4::Dates->new($value)->output("iso");
228 # don't escape runtime parameters, they'll be at runtime
229 if ($value =~ /<<.*>>/) {
230 $query_criteria .= " AND $crit=$value";
231 } else {
232 $query_criteria .= " AND $crit='$value'";
237 $template->param(
238 'build5' => 1,
239 'area' => $area,
240 'type' => $type,
241 'column' => $column,
242 'definition' => $definition,
243 'criteriastring' => $query_criteria,
246 # get columns
247 my @columns = split( ',', $column );
248 my @total_by;
250 # build structue for use by tmpl_loop to choose columns to order by
251 # need to do something about the order of the order :)
252 # we also want to use the %columns hash to get the plain english names
253 foreach my $col (@columns) {
254 my %total = (name => $col);
255 my @selects = map {+{ value => $_ }} (qw(sum min max avg count));
256 $total{'select'} = \@selects;
257 push @total_by, \%total;
260 $template->param( 'total_by' => \@total_by );
263 elsif ( $phase eq 'Choose These Operations' ) {
264 my $area = $input->param('area');
265 my $type = $input->param('type');
266 my $column = $input->param('column');
267 my $criteria = $input->param('criteria');
268 my $definition = $input->param('definition');
269 my @total_by = $input->param('total_by');
270 my $totals;
271 foreach my $total (@total_by) {
272 my $value = $input->param( $total . "_tvalue" );
273 $totals .= "$value($total),";
276 $template->param(
277 'build6' => 1,
278 'area' => $area,
279 'type' => $type,
280 'column' => $column,
281 'criteriastring' => $criteria,
282 'totals' => $totals,
283 'definition' => $definition,
286 # get columns
287 my @columns = split( ',', $column );
288 my @order_by;
290 # build structue for use by tmpl_loop to choose columns to order by
291 # need to do something about the order of the order :)
292 foreach my $col (@columns) {
293 my %order = (name => $col);
294 my @selects = map {+{ value => $_ }} (qw(asc desc));
295 $order{'select'} = \@selects;
296 push @order_by, \%order;
299 $template->param( 'order_by' => \@order_by );
302 elsif ( $phase eq 'Build Report' ) {
304 # now we have all the info we need and can build the sql
305 my $area = $input->param('area');
306 my $type = $input->param('type');
307 my $column = $input->param('column');
308 my $crit = $input->param('criteria');
309 my $totals = $input->param('totals');
310 my $definition = $input->param('definition');
311 my $query_criteria=$crit;
312 # split the columns up by ,
313 my @columns = split( ',', $column );
314 my @order_by = $input->param('order_by');
316 my $query_orderby;
317 foreach my $order (@order_by) {
318 my $value = $input->param( $order . "_ovalue" );
319 if ($query_orderby) {
320 $query_orderby .= ",$order $value";
322 else {
323 $query_orderby = " ORDER BY $order $value";
327 # get the sql
328 my $sql =
329 build_query( \@columns, $query_criteria, $query_orderby, $area, $totals, $definition );
330 $template->param(
331 'showreport' => 1,
332 'sql' => $sql,
333 'type' => $type
337 elsif ( $phase eq 'Save' ) {
338 # Save the report that has just been built
339 my $sql = $input->param('sql');
340 my $type = $input->param('type');
341 $template->param(
342 'save' => 1,
343 'sql' => $sql,
344 'type' => $type
348 elsif ( $phase eq 'Save Report' ) {
349 # save the sql pasted in by a user
350 my $sql = $input->param('sql');
351 my $name = $input->param('reportname');
352 my $type = $input->param('types');
353 my $notes = $input->param('notes');
354 if ($sql =~ /;?\W?(UPDATE|DELETE|DROP|INSERT|SHOW|CREATE)\W/i) {
355 push @errors, {sqlerr => $1};
357 elsif ($sql !~ /^(SELECT)/i) {
358 push @errors, {queryerr => 1};
360 if (@errors) {
361 $template->param(
362 'errors' => \@errors,
363 'sql' => $sql,
364 'reportname'=> $name,
365 'type' => $type,
366 'notes' => $notes,
369 else {
370 save_report( $borrowernumber, $sql, $name, $type, $notes );
371 $template->param(
372 'save_successful' => 1,
377 elsif ($phase eq 'Run this report'){
378 # execute a saved report
379 my $limit = 20; # page size. # TODO: move to DB or syspref?
380 my $offset = 0;
381 my $report = $input->param('reports');
382 my @sql_params = $input->param('sql_params');
383 # offset algorithm
384 if ($input->param('page')) {
385 $offset = ($input->param('page') - 1) * $limit;
387 my ($sql,$type,$name,$notes) = get_saved_report($report);
388 unless ($sql) {
389 push @errors, {no_sql_for_id=>$report};
391 my @rows = ();
392 # if we have at least 1 parameter, and it's not filled, then don't execute but ask for parameters
393 if ($sql =~ /<</ && !@sql_params) {
394 # split on ??. Each odd (2,4,6,...) entry should be a parameter to fill
395 my @split = split /<<|>>/,$sql;
396 my @tmpl_parameters;
397 for(my $i=0;$i<($#split/2);$i++) {
398 my ($text,$authorised_value) = split /\|/,$split[$i*2+1];
399 my $input;
400 if ($authorised_value) {
401 my $dbh=C4::Context->dbh;
402 my @authorised_values;
403 my %authorised_lib;
404 # builds list, depending on authorised value...
405 if ( $authorised_value eq "branches" ) {
406 my $branches = GetBranchesLoop();
407 foreach my $thisbranch (@$branches) {
408 push @authorised_values, $thisbranch->{value};
409 $authorised_lib{$thisbranch->{value}} = $thisbranch->{branchname};
412 elsif ( $authorised_value eq "itemtypes" ) {
413 my $sth = $dbh->prepare("SELECT itemtype,description FROM itemtypes ORDER BY description");
414 $sth->execute;
415 while ( my ( $itemtype, $description ) = $sth->fetchrow_array ) {
416 push @authorised_values, $itemtype;
417 $authorised_lib{$itemtype} = $description;
420 elsif ( $authorised_value eq "cn_source" ) {
421 my $class_sources = GetClassSources();
422 my $default_source = C4::Context->preference("DefaultClassificationSource");
423 foreach my $class_source (sort keys %$class_sources) {
424 next unless $class_sources->{$class_source}->{'used'} or
425 ($class_source eq $default_source);
426 push @authorised_values, $class_source;
427 $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
430 elsif ( $authorised_value eq "categorycode" ) {
431 my $sth = $dbh->prepare("SELECT categorycode, description FROM categories ORDER BY description");
432 $sth->execute;
433 while ( my ( $categorycode, $description ) = $sth->fetchrow_array ) {
434 push @authorised_values, $categorycode;
435 $authorised_lib{$categorycode} = $description;
438 #---- "true" authorised value
440 else {
441 my $authorised_values_sth = $dbh->prepare("SELECT authorised_value,lib FROM authorised_values WHERE category=? ORDER BY lib");
443 $authorised_values_sth->execute( $authorised_value);
445 while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
446 push @authorised_values, $value;
447 $authorised_lib{$value} = $lib;
448 # For item location, we show the code and the libelle
449 $authorised_lib{$value} = $lib;
452 $input =CGI::scrolling_list( # FIXME: factor out scrolling_list
453 -name => "sql_params",
454 -values => \@authorised_values,
455 # -default => $value,
456 -labels => \%authorised_lib,
457 -override => 1,
458 -size => 1,
459 -multiple => 0,
460 -tabindex => 1,
463 } else {
464 $input = "<input type='text' name='sql_params'/>";
466 push @tmpl_parameters, {'entry' => $text, 'input' => $input };
468 $template->param('sql' => $sql,
469 'name' => $name,
470 'sql_params' => \@tmpl_parameters,
471 'enter_params' => 1,
472 'reports' => $report,
474 } else {
475 # OK, we have parameters, or there are none, we run the report
476 # if there were parameters, replace before running
477 # split on ??. Each odd (2,4,6,...) entry should be a parameter to fill
478 my @split = split /<<|>>/,$sql;
479 my @tmpl_parameters;
480 for(my $i=0;$i<$#split/2;$i++) {
481 my $quoted = C4::Context->dbh->quote($sql_params[$i]);
482 # if there are special regexp chars, we must \ them
483 $split[$i*2+1] =~ s/(\||\?|\.|\*|\(|\)|\%)/\\$1/g;
484 $sql =~ s/<<$split[$i*2+1]>>/$quoted/;
486 my ($sth, $errors) = execute_query($sql, $offset, $limit);
487 my $total = select_2_select_count_value($sql) || 0;
488 unless ($sth) {
489 die "execute_query failed to return sth for report $report: $sql";
490 } else {
491 my $headref = $sth->{NAME} || [];
492 my @headers = map { +{ cell => $_ } } @$headref;
493 $template->param(header_row => \@headers);
494 while (my $row = $sth->fetchrow_arrayref()) {
495 my @cells = map { +{ cell => $_ } } @$row;
496 push @rows, { cells => \@cells };
500 my $totpages = int($total/$limit) + (($total % $limit) > 0 ? 1 : 0);
501 my $url = "/cgi-bin/koha/reports/guided_reports.pl?reports=$report&amp;phase=Run%20this%20report";
502 $template->param(
503 'results' => \@rows,
504 'sql' => $sql,
505 'execute' => 1,
506 'name' => $name,
507 'notes' => $notes,
508 'errors' => $errors,
509 'pagination_bar' => pagination_bar($url, $totpages, $input->param('page')),
510 'unlimited_total' => $total,
515 elsif ($phase eq 'Export'){
516 binmode STDOUT, ':utf8';
518 # export results to tab separated text or CSV
519 my $sql = $input->param('sql'); # FIXME: use sql from saved report ID#, not new user-supplied SQL!
520 my $format = $input->param('format');
521 my ($sth, $q_errors) = execute_query($sql);
522 unless ($q_errors and @$q_errors) {
523 print $input->header( -type => 'application/octet-stream',
524 -attachment=>"reportresults.$format"
526 if ($format eq 'tab') {
527 print join("\t", header_cell_values($sth)), "\n";
528 while (my $row = $sth->fetchrow_arrayref()) {
529 print join("\t", @$row), "\n";
531 } else {
532 my $csv = Text::CSV->new({binary => 1});
533 $csv or die "Text::CSV->new({binary => 1}) FAILED: " . Text::CSV->error_diag();
534 if ($csv->combine(header_cell_values($sth))) {
535 print $csv->string(), "\n";
536 } else {
537 push @$q_errors, { combine => 'HEADER ROW: ' . $csv->error_diag() } ;
539 while (my $row = $sth->fetchrow_arrayref()) {
540 if ($csv->combine(@$row)) {
541 print $csv->string(), "\n";
542 } else {
543 push @$q_errors, { combine => $csv->error_diag() } ;
547 foreach my $err (@$q_errors, @errors) {
548 print "# ERROR: " . (map {$_ . ": " . $err->{$_}} keys %$err) . "\n";
549 } # here we print all the non-fatal errors at the end. Not super smooth, but better than nothing.
550 exit;
552 $template->param(
553 'sql' => $sql,
554 'execute' => 1,
555 'name' => 'Error exporting report!',
556 'notes' => '',
557 'errors' => $q_errors,
561 elsif ($phase eq 'Create report from SQL') {
562 # allow the user to paste in sql
563 if ($input->param('sql')) {
564 $template->param(
565 'sql' => $input->param('sql'),
566 'reportname' => $input->param('reportname'),
567 'notes' => $input->param('notes'),
570 $template->param('create' => 1);
573 elsif ($phase eq 'Create Compound Report'){
574 $template->param( 'savedreports' => get_saved_reports(),
575 'compound' => 1,
579 elsif ($phase eq 'Save Compound'){
580 my $master = $input->param('master');
581 my $subreport = $input->param('subreport');
582 my ($mastertables,$subtables) = create_compound($master,$subreport);
583 $template->param( 'save_compound' => 1,
584 master=>$mastertables,
585 subsql=>$subtables
589 # pass $sth, get back an array of names for the column headers
590 sub header_cell_values {
591 my $sth = shift or return ();
592 return @{$sth->{NAME}};
595 # pass $sth, get back a TMPL_LOOP-able set of names for the column headers
596 sub header_cell_loop {
597 my @headers = map { +{ cell => $_ } } header_cell_values (shift);
598 return \@headers;
601 foreach (1..6) {
602 $template->param('build' . $_) and $template->param(buildx => $_) and last;
604 $template->param( 'referer' => $input->referer(),
605 'DHTMLcalendar_dateformat' => C4::Dates->DHTMLcalendar(),
608 output_html_with_http_headers $input, $cookie, $template->output;