re-input an old function.
[koha.git] / opac / opac-zoomsearch.pl
1 #!/usr/bin/perl
2
3 # Copyright 2006 Liblime
4 #
5 # This file is part of Koha.
6
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details. 
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
18 # Suite 330, Boston, MA  02111-1307 USA
19
20 # load our Koha modules
21 use C4::Context;
22 use C4::Interface::CGI::Output;
23 use C4::Auth;
24 use C4::Search;
25 use C4::Biblio;
26 use C4::Koha;
27 use POSIX qw(ceil floor);
28
29 # load other modules
30 use HTML::Template;
31 use CGI;
32 use strict; 
33
34 my $query=new CGI;
35 my $op = $query->param('op'); # show the search form or execute the search
36
37 # expanded facet?
38 my $expanded_facet = $query->param('expand');
39
40 ### Gather up all our search queries
41 ## CQL
42 my $cql_query = $query->param('cql_query');
43
44 ## CCL
45 my @previous_ccl_queries; # array of hashes
46 my @previous_ccl_queries_array = $query->param('previous_ccl_queries');
47
48 my @ccl_query = $query->param('ccl_query');
49 my $ccl_query;
50 foreach my $ccl (@ccl_query) {
51         $ccl_query.="$ccl " if $ccl;
52 }
53 push @previous_ccl_queries_array, $ccl_query;
54 # put the queries in a form the template can use
55 my $previous_ccl_queries_hash;
56 foreach my $ccl (@previous_ccl_queries_array) {
57         if ($ccl) {
58         my %row =(
59                 value => $ccl
60                 );
61         push @previous_ccl_queries, %row;
62         }
63 }
64 ## PQF
65 my $pqf_query = $query->param('pqf_query');
66
67 my @newresults;
68 my ($template,$borrowernumber,$cookie);
69 my @forminputs;         # this is for the links to navigate among the results when they are more than the maximum number of results per page
70 my $searchdesc; 
71 my $search_type = $query->param('search_type');
72
73 my $dbh = C4::Context->dbh;
74 ## Get Itemtypes (Collection Codes)
75 my $itemtypequery="Select itemtype,description from itemtypes order by description";    
76 my $sth=$dbh->prepare($itemtypequery);
77 $sth->execute;
78 my @itemtypeloop;
79 my %itemtypes;
80 while (my ($value,$lib) = $sth->fetchrow_array) {
81         my %row =(
82             value => $value,
83             description => $lib,
84             );
85         push @itemtypeloop, \%row;
86
87 }
88 $sth->finish;
89
90 ## Get Branches
91 my @branches;
92 my @select_branch;
93 my %select_branches;
94 my $branches = getallbranches();
95 my @branchloop;
96 foreach my $thisbranch (keys %$branches) {
97                 my $selected = 1 if (C4::Context->userenv && ($thisbranch eq C4::Context->userenv->{branch}));            
98                 my %row =(
99                 value => $thisbranch,
100                 selected => $selected,
101                 branchname => $branches->{$thisbranch}->{'branchname'},
102                 );
103         push @branchloop, \%row;
104 }
105
106 ## Check if we're searching
107 if ($op eq 'get_results') { # Yea, we're searching, load the results template
108         ($template, $borrowernumber, $cookie)
109                 = get_template_and_user({template_name => "opac-results.tmpl",
110                                          query => $query,
111                                          type => "opac",
112                                          authnotrequired => 1,});
113
114         my $number_of_results = $query->param('results_per_page');
115         $number_of_results = 20 unless ($number_of_results); #this could be a parameter with 20 50 or 100 results per page
116         my $startfrom = $query->param('startfrom');
117         ($startfrom) || ($startfrom=0);
118
119         ## OK, WE'RE SEARCHING
120         # STEP 1. We're a CGI script,so first thing to do is get the
121         # query into PQF format so we can use the Koha API properly
122         my ($error,$pqf_sort_by, $pqf_prox_ops, $pqf_bool_ops, $nice_query);
123         ($error,$pqf_sort_by, $pqf_prox_ops, $pqf_bool_ops, $pqf_query, $nice_query)= cgi2pqf($query);
124         my $then_sort_by = $query->param('then_sort_by');
125         # implement a query history
126
127         # lets store the query details in an array for later
128         push @forminputs, { field => "cql_query" , value => $cql_query} ;
129         push @forminputs, { field => "ccl_query" , value => $ccl_query} ;
130         push @forminputs, { field => 'pqf_sort_by', value => $pqf_sort_by} ;
131         push @forminputs, { field => 'pqf_prox_ops', value => $pqf_prox_ops};
132         push @forminputs, { field => 'pqf_bool_ops' , value => $pqf_bool_ops};
133         push @forminputs, { field => 'pqf_query' , value => $pqf_query };
134         $searchdesc=$cql_query.$ccl_query.$nice_query; # FIXME: this should be a more use-friendly string
135
136         # STEP 2. OK, now we have PQF, so we can pass off the query to
137         # the API
138         my ($count,@results,$facets);
139
140         # queries are handled differently, so alert our API and pass in the variables
141         if ($ccl_query) { # CCL
142                 if ($query->param('scan')) {
143                         ($error,$count, $facets,@results) = searchZOOM('scan','ccl',$ccl_query,$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
144                         $template->param(scan => 1);
145                 } else {
146                         ($error,$count,$facets,@results) = searchZOOM('search','ccl',$ccl_query,$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
147                 }
148         } elsif ($query->param('cql_query')) { # CQL
149                 if ($query->param('scan')) {
150                         ($error,$count,$facets,@results) = searchZOOM('scan','cql',$cql_query,$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
151                         $template->param(scan => 1);
152                 } else {
153                         ($error,$count,$facets,@results) = searchZOOM('search','cql',$cql_query,$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
154                 }
155         } else { # we're in PQF territory now
156                 if ($query->param('scan')) {
157                         $template->param(scan => 1);
158                         ($error,$count,$facets,@results) = searchZOOM('scan','pqf',"$pqf_sort_by $pqf_prox_ops $pqf_bool_ops $pqf_query",$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
159                 } else {
160                         ($error,$count,$facets,@results) = searchZOOM('search','pqf',"$pqf_sort_by $pqf_prox_ops $pqf_bool_ops $pqf_query",$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
161                 }
162         }
163         @newresults=searchResults( $searchdesc,$number_of_results,$count,@results) ;
164
165         # How many did we get back?
166         my $num = scalar(@newresults);
167
168         # sorting out which results to display.
169         # the result number to start to show
170         $template->param(starting => $startfrom+1);
171         $template->param(ending => $startfrom+$number_of_results);
172         # the result number to end to show
173         ($startfrom+$num<=$count) ? ($template->param(endat => $startfrom+$num)) : ($template->param(endat => $count));
174         # the total results found
175         $template->param(total => $count);
176         $template->param(FORMINPUTS => \@forminputs);
177         #$template->param(pqf_query => $pqf_query);
178         $template->param(ccl_query => $ccl_query);
179         $template->param(searchdesc => $searchdesc );
180         $template->param(results_per_page =>  $number_of_results );
181         $template->param(SEARCH_RESULTS => \@newresults);
182         $template->param(PREVIOUS_CCL_QUERIES => \@previous_ccl_queries);
183         $template->param(facets_loop => $facets);
184
185         #this is to show the page numbers to navigate among the results, whether it has to show the number highlighted or not
186         my $numbers;
187         @$numbers = ();
188         my $pg = 1;
189         if (defined($query->param('pg'))) {
190                 $pg = $query->param('pg');
191         }
192         my $start = 0;
193         
194         $start = ($pg - 1) * $number_of_results;
195         my $pages = ceil($count / $number_of_results);
196         my $total_pages = ceil($count / $number_of_results);
197         my $url;
198         if ($pg > 1) {
199                 $url = $pg - 1;
200                 push @$numbers, {               
201                                 number => "&lt;&lt;", 
202                                 highlight => 0 , 
203                                 startfrom => 0, 
204                                 pg => '1' };
205
206                 push @$numbers, {               
207                                 number => "&lt;", 
208                                 highlight => 0 , 
209                                 startfrom => ($url-1)*$number_of_results, 
210                                 pg => $url };
211         }
212         my $current_ten = $pg / 10;
213         if ($current_ten == 0) {
214                  $current_ten = 0.1;           # In case it's in ten = 0
215         } 
216         my $from = $current_ten * 10; # Calculate the initial page
217         my $end_ten = $from + 9;
218         my $to;
219         if ($pages > $end_ten) {
220                 $to = $end_ten;
221         } else {
222                 $to = $pages;
223         }
224         for (my $i =$from; $i <= $to ; $i++) {
225                 if ($i == $pg) {   
226                         if ($count > $number_of_results) {
227                                 push @$numbers, { 
228                                                 number => $i, 
229                                                 highlight => 1 , 
230                                                 startfrom => ($i-1)*$number_of_results , 
231                                                 pg => $i };
232                         }
233                 } else {
234                         push @$numbers, {       
235                                                 number => $i, 
236                                                 highlight => 0 , 
237                                                 startfrom => ($i-1)*$number_of_results , 
238                                                 pg => $i };
239                 }
240         }                                                       
241         if ($pg < $pages) {
242                  $url = $pg + 1;
243                 push @$numbers, {               
244                                                 number => "&gt;", 
245                                                 highlight => 0 , 
246                                                 startfrom => ($url-1)*$number_of_results, 
247                                                 pg => $url };
248
249                 push @$numbers, {               
250                                                 number => "&gt;&gt;", 
251                                                 highlight => 0 , 
252                                                 startfrom => ($total_pages-1)*$number_of_results, 
253                                                 pg => $total_pages};
254         }
255
256         $template->param(                       
257                                                 pqf_sort_by => $pqf_sort_by,
258                                                 pqf_query => "$pqf_prox_ops $pqf_bool_ops $pqf_query",
259                                                 numbers => $numbers);
260
261
262     $template->param('Disable_Dictionary'=>C4::Context->preference("Disable_Dictionary")) if (C4::Context->preference("Disable_Dictionary"));
263     my $scan_use = $query->param('use1');
264     $template->param(
265                         #classlist => $classlist,
266                         suggestion => C4::Context->preference("suggestion"),
267                         virtualshelves => C4::Context->preference("virtualshelves"),
268                         LibraryName => C4::Context->preference("LibraryName"),
269                         OpacNav => C4::Context->preference("OpacNav"),
270                         opaccredits => C4::Context->preference("opaccredits"),
271                         AmazonContent => C4::Context->preference("AmazonContent"),
272                         opacsmallimage => C4::Context->preference("opacsmallimage"),
273                         opaclayoutstylesheet => C4::Context->preference("opaclayoutstylesheet"),
274                         opaccolorstylesheet => C4::Context->preference("opaccolorstylesheet"),
275                         scan_use => $scan_use,
276                         search_error => $error,
277     );
278
279         ## Now let's find out if we have any supplemental data to show the user
280         #  and in the meantime, save the current query for statistical purposes, etc.
281         my $koha_spsuggest; # a flag to tell if we've got suggestions coming from Koha
282         my @koha_spsuggest; # place we store the suggestions to be returned to the template as LOOP
283         my $phrases = $searchdesc;
284         my $ipaddress;
285         
286         if ( C4::Context->preference("kohaspsuggest") ) {
287                 eval {
288                         my $koha_spsuggest_dbh;
289                         eval {
290                                 $koha_spsuggest_dbh=DBI->connect("DBI:mysql:suggest:66.213.78.76","auth","Free2cirC");
291                         };
292                         if ($@) { warn "can't connect to spsuggest db";
293                         }
294                         else {
295                                 my $koha_spsuggest_insert = "INSERT INTO phrase_log(phr_phrase,phr_resultcount,phr_ip) VALUES(?,?,?)";
296                                 my $koha_spsuggest_query = "SELECT display FROM distincts WHERE strcmp(soundex(suggestion), soundex(?)) = 0 order by soundex(suggestion) limit 0,5";
297                                 my $koha_spsuggest_sth = $koha_spsuggest_dbh->prepare($koha_spsuggest_query);
298                                 $koha_spsuggest_sth->execute($phrases);
299                                 while (my $spsuggestion = $koha_spsuggest_sth->fetchrow_array) {
300                                         $spsuggestion =~ s/(:|\/)//g;
301                                         my %line;
302                                         $line{spsuggestion} = $spsuggestion;
303                                         push @koha_spsuggest,\%line;
304                                         $koha_spsuggest = 1;
305                                 }
306
307                                 # Now save the current query
308                                 $koha_spsuggest_sth=$koha_spsuggest_dbh->prepare($koha_spsuggest_insert);
309                                 #$koha_spsuggest_sth->execute($phrases,$count,$ipaddress);
310                                 $koha_spsuggest_sth->finish;
311
312                                 $template->param( koha_spsuggest => $koha_spsuggest ) unless $num;
313                                 $template->param( SPELL_SUGGEST => \@koha_spsuggest,
314                                                 branchloop=>\@branchloop,
315                                                         itemtypeloop=>\@itemtypeloop,
316                                 );
317                         }
318                 };
319                 if ($@) {
320                         warn "Kohaspsuggest failure:".$@;
321                 }
322         }
323         
324         ## Spellchecking using Google API
325         ## Did you mean? Suggestions using spsuggest table
326         #       
327         # Related Searches
328         #
329 ## OK, we're not searching, load the search template
330 } else {
331
332         ($template, $borrowernumber, $cookie)
333         = get_template_and_user({template_name => "opac-zoomsearch.tmpl",
334                     query => $query,
335                     type => "opac",
336                     authnotrequired => 1,
337                 });
338
339         # set the default tab, etc.
340         my $search_type = $query->param('query_form');
341         if ((!$search_type) || ($search_type eq 'advanced'))  {
342                 $template->param(advanced_search => 1);
343         } elsif ($search_type eq 'format') {
344                 $template->param(format_search => 1);
345         } elsif ($search_type eq 'power') {
346                 $template->param(power_search => 1);
347         } elsif ($search_type eq 'cql') {
348                 $template->param(power_search => 1);
349         } elsif ($search_type eq 'pqf') {
350                 $template->param(power_search => 1);
351         } elsif ($search_type eq 'proximity') {
352                 $template->param(proximity_search => 1);
353         }
354
355         $template->param(
356                 branchloop=>\@branchloop,
357                 itemtypeloop=>\@itemtypeloop,
358         );
359
360 }
361 output_html_with_http_headers $query, $cookie, $template->output;
362
363 =head2 cgi2pdf
364 =cut
365 # build a valid PQF query from a CGI form
366 sub cgi2pqf {
367         my ($query) = @_;
368         my $nice_query; # just a string storing a nicely formatted query
369         my @default_attributes = ('sort_by');
370         # attributes specific to the advanced search - a search_point is actually a combination of
371         #  several bib1 attributes
372         my @advanced_attributes = ('search_point','op','query');
373         # attributes specific to the power search
374         my @power_attributes = ( 'use','relation','structure','truncation','completeness','op','query');
375         # attributes specific to the proximity search
376         my @proximity_attributes = ('prox_op','prox_exclusion','prox_distance','prox_ordered','prox_relation','prox_which-code','prox_unit-code','query');
377
378         my @specific_attributes; # these will be looped as many times as needed
379
380         my $query_form = $query->param('query_form');
381
382         # bunch of places to store the various queries we're working with
383         my $cql_query = $query->param('cql_query');
384
385         my $pqf_query = $query->param('pqf_query');
386
387         my @pqf_query_array;
388         my @counting_pqf_query_array;
389         
390         my $pqf_prox_ops = $query->param('pqf_prox_ops');
391         my @pqf_prox_ops_array;
392         
393         my $pqf_bool_ops = $query->param('pqf_bool_ops');
394         my @pqf_bool_ops_array;
395
396         my $pqf_sort_by = $query->param('pqf_sort_by');
397
398         # operators:
399
400         # ADVANCED SEARCH
401         if (($query_form) eq 'advanced') {
402                 @specific_attributes = @advanced_attributes;
403         # POWER SEARCH
404         } elsif (($query_form) eq 'power') {
405                 @specific_attributes = @power_attributes;
406         # PROXIMITY SEARCH
407         } elsif (($query_form) eq 'proximity') {
408                 @specific_attributes = @proximity_attributes;
409         }
410         
411
412         # load the default attributes, set once per query
413         foreach my $def_attr (@default_attributes) {
414                 $pqf_sort_by .= " ".$query->param($def_attr);
415         }
416         # these are attributes specific to this query_form, set many times per query
417         # First, process the 'operators' and put them in an array
418         # proximity and boolean
419         foreach my $spec_attr (@specific_attributes) {
420                 for (my $i=1;$i<15;$i++) {
421                         if ($query->param("query$i")) { # make sure this set should be used
422                                 if ($spec_attr =~ /^op/) { # process the operators separately
423                                         push @pqf_bool_ops_array, $query->param("$spec_attr$i");
424                                         $nice_query .=" ".$query->param("$spec_attr$i")." ".$query->param("query$i");
425                                         $nice_query =~ s/\@and/AND/g;
426                                         $nice_query =~ s/\@or/OR/g;
427                                 } elsif ($spec_attr =~ /^prox/) { # process the proximity operators separately
428                                         if ($query->param("$spec_attr$i")) {
429                                                 #warn "PQF:".$query->param("$spec_attr$i");
430                                                 push @pqf_prox_ops_array,$query->param("$spec_attr$i");
431                                         } else {
432                                                 if (($spec_attr =~ /^prox_exclusion/) || ($spec_attr =~ /^prox_ordered/)) { # this is an exception, sloppy way to handle it
433                                                         if ($i==2) {
434                                                                 push @pqf_prox_ops_array,0;
435                                                         }
436                                                 }
437                                         }
438                                 }
439                         }
440                 }
441         }
442         # by now, we have two operator arrays: @pqf_bool_ops_array (boolean) and @pqf_prox_ops_array (proximity)
443
444         # Next, we process the attributes (operands)
445         for (my $i=1;$i<15;$i++) {
446                 foreach my $spec_attr (@specific_attributes) {
447                         if ($query->param("query$i")) {
448                                 if ($spec_attr =~ /^query/) {
449                                         push @counting_pqf_query_array,$query->param("$spec_attr$i") if $query->param("$spec_attr$i");
450                                         push @pqf_query_array,$query->param("$spec_attr$i") if $query->param("$spec_attr$i")
451                                 } elsif ($spec_attr =~ /^op/) { # don't process the operators again
452                                 } elsif ($spec_attr =~ /^prox/) { 
453                                 } else {
454                                         push @pqf_query_array,$query->param("$spec_attr$i") if $query->param("$spec_attr$i");
455                                 }
456                         }
457                 }
458         }
459
460         # we have to make sure that there # of operators == # of operands-1
461         # because second, third, etc queries, come in with an operator attached
462         # ...but if there is no previous queries, it should be dropped ... 
463         # that's what we're doing here
464         my $count_pqf_query = @counting_pqf_query_array;
465         my $count_pqf_bool_ops = @pqf_bool_ops_array;
466
467         if ($count_pqf_bool_ops == $count_pqf_query-1) {
468                 for (my $i=$count_pqf_query;$i>=0;$i--) {
469                         $pqf_bool_ops.=" ".$pqf_bool_ops_array[$i];
470                 }
471         } else {
472                 for (my $i=$count_pqf_query;$i>=1;$i--) {
473                         $pqf_bool_ops.=" ".$pqf_bool_ops_array[$i];
474                 }
475         }
476         foreach my $que(@pqf_query_array) {
477                 if ($que =~ /@/) {
478                         $pqf_query .=" ".$que;
479                 } else {
480                         $que =~ s/(\"|\'|\.)//g;
481                         $pqf_query .=" \"".$que."\"";
482                 }
483         }
484         foreach my $prox(@pqf_prox_ops_array) {
485                 $pqf_prox_ops.=" ".$prox;
486         }
487
488         # finally, nice_query needs to be populated if it hasn't been
489         $nice_query = $pqf_query unless $nice_query;
490         # and cleaned up FIXME: bad bad ... 
491         $nice_query =~ s/\@attr//g;
492         $nice_query =~ s/\d+=\d+//g;
493         # OK, done with that, now lets have a look
494         #warn "Boolean Operators: ".$pqf_bool_ops if $pqf_bool_ops;
495         #warn "Proximigy Operators: ".$pqf_prox_ops if $pqf_prox_ops;
496         #warn "Sort by: ".$pqf_sort_by;
497         #warn "PQF:".$pqf_query;        
498         #warn "Full PQF: $pqf_sort_by $pqf_prox_ops $pqf_bool_ops $pqf_query";
499         #warn "NICE: $nice_query";
500         return ('',$pqf_sort_by, $pqf_prox_ops, $pqf_bool_ops, $pqf_query, $nice_query);
501 }