Bug 17170: Add search filters to staff and opac interfaces
[srvgit] / catalogue / search.pl
1 #!/usr/bin/perl
2 # Script to perform searching
3 # For documentation try 'perldoc /path/to/search'
4 #
5 # Copyright 2006 LibLime
6 # Copyright 2010 BibLibre
7 #
8 # This file is part of Koha
9 #
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22
23 =head1 NAME
24
25 search - a search script for finding records in a Koha system (Version 3)
26
27 =head1 OVERVIEW
28
29 This script utilizes a new search API for Koha 3. It is designed to be 
30 simple to use and configure, yet capable of performing feats like stemming,
31 field weighting, relevance ranking, support for multiple  query language
32 formats (CCL, CQL, PQF), full support for the bib1 attribute set, extended
33 attribute sets defined in Zebra profiles, access to the full range of Z39.50
34 and SRU query options, federated searches on Z39.50/SRU targets, etc.
35
36 The API as represented in this script is mostly sound, even if the individual
37 functions in Search.pm and Koha.pm need to be cleaned up. Of course, you are
38 free to disagree :-)
39
40 I will attempt to describe what is happening at each part of this script.
41 -- Joshua Ferraro <jmf AT liblime DOT com>
42
43 =head2 INTRO
44
45 This script performs two functions:
46
47 =over 
48
49 =item 1. interacts with Koha to retrieve and display the results of a search
50
51 =item 2. loads the advanced search page
52
53 =back
54
55 These two functions share many of the same variables and modules, so the first
56 task is to load what they have in common and determine which template to use.
57 Once determined, proceed to only load the variables and procedures necessary
58 for that function.
59
60 =head2 LOADING ADVANCED SEARCH PAGE
61
62 This is fairly straightforward, and I won't go into detail ;-)
63
64 =head2 PERFORMING A SEARCH
65
66 If we're performing a search, this script  performs three primary
67 operations:
68
69 =over 
70
71 =item 1. builds query strings (yes, plural)
72
73 =item 2. perform the search and return the results array
74
75 =item 3. build the HTML for output to the template
76
77 =back
78
79 There are several additional secondary functions performed that I will
80 not cover in detail.
81
82 =head3 1. Building Query Strings
83
84 There are several types of queries needed in the process of search and retrieve:
85
86 =over
87
88 =item 1 $query - the fully-built query passed to zebra
89
90 This is the most complex query that needs to be built. The original design goal 
91 was to use a custom CCL2PQF query parser to translate an incoming CCL query into
92 a multi-leaf query to pass to Zebra. It needs to be multi-leaf to allow field 
93 weighting, koha-specific relevance ranking, and stemming. When I have a chance 
94 I'll try to flesh out this section to better explain.
95
96 This query incorporates query profiles that aren't compatible with most non-Zebra 
97 Z39.50 targets to accomplish the field weighting and relevance ranking.
98
99 =item 2 $simple_query - a simple query that doesn't contain the field weighting,
100 stemming, etc., suitable to pass off to other search targets
101
102 This query is just the user's query expressed in CCL CQL, or PQF for passing to a 
103 non-zebra Z39.50 target (one that doesn't support the extended profile that Zebra does).
104
105 =item 3 $query_cgi - passed to the template / saved for future refinements of 
106 the query (by user)
107
108 This is a simple string that completely expresses the query as a CGI string that
109 can be used for future refinements of the query or as a part of a history feature.
110
111 =item 4 $query_desc - Human search description - what the user sees in search
112 feedback area
113
114 This is a simple string that is human readable. It will contain '=', ',', etc.
115
116 =back
117
118 =head3 2. Perform the Search
119
120 This section takes the query strings and performs searches on the named servers,
121 including the Koha Zebra server, stores the results in a deeply nested object, 
122 builds 'faceted results', and returns these objects.
123
124 =head3 3. Build HTML
125
126 The final major section of this script takes the objects collected thusfar and 
127 builds the HTML for output to the template and user.
128
129 =head3 Additional Notes
130
131 Not yet completed...
132
133 =cut
134
135 use Modern::Perl;
136
137 ## STEP 1. Load things that are used in both search page and
138 # results page and decide which template to load, operations 
139 # to perform, etc.
140
141 ## load Koha modules
142 use C4::Context;
143 use C4::Output qw( output_html_with_http_headers pagination_bar );
144 use C4::Circulation qw( barcodedecode );
145 use C4::Auth qw( get_template_and_user );
146 use C4::Search qw( searchResults enabled_staff_search_views z3950_search_args new_record_from_zebra );
147 use C4::Languages qw( getlanguage getLanguages );
148 use C4::Koha qw( getitemtypeimagelocation GetAuthorisedValues );
149 use URI::Escape;
150 use POSIX qw(ceil floor);
151 use C4::Search qw( searchResults enabled_staff_search_views z3950_search_args new_record_from_zebra );
152
153 use Koha::ItemTypes;
154 use Koha::Library::Groups;
155 use Koha::Patrons;
156 use Koha::SearchEngine::Search;
157 use Koha::SearchEngine::QueryBuilder;
158 use Koha::Virtualshelves;
159 use Koha::SearchFields;
160 use Koha::SearchFilters;
161
162 use URI::Escape;
163 use JSON qw( decode_json encode_json );
164
165 my $DisplayMultiPlaceHold = C4::Context->preference("DisplayMultiPlaceHold");
166 # create a new CGI object
167 # FIXME: no_undef_params needs to be tested
168 use CGI qw('-no_undef_params' -utf8 );
169 my $cgi = CGI->new;
170
171 # decide which template to use
172 my $template_name;
173 my $template_type;
174 # limits are used to limit to results to a pre-defined category such as branch or language
175 my @limits = map uri_unescape($_), $cgi->multi_param("limit");
176 my @nolimits = map uri_unescape($_), $cgi->multi_param('nolimit');
177 my %is_nolimit = map { $_ => 1 } @nolimits;
178 @limits = grep { not $is_nolimit{$_} } @limits;
179 if  (
180         !$cgi->param('edit_search') &&
181         ( (@limits>=1) || (defined $cgi->param("q") && $cgi->param("q") ne "" ) || ($cgi->param('limit-yr')) )
182     ) {
183     $template_name = 'catalogue/results.tt';
184     $template_type = 'results';
185 }
186 else {
187     $template_name = 'catalogue/advsearch.tt';
188     $template_type = 'advsearch';
189 }
190 # load the template
191 my ($template, $borrowernumber, $cookie) = get_template_and_user({
192     template_name => $template_name,
193     query => $cgi,
194     type => "intranet",
195     flagsrequired   => { catalogue => 1 },
196     }
197 );
198
199 my $lang = C4::Languages::getlanguage($cgi);
200
201 if (C4::Context->preference("marcflavour") eq "UNIMARC" ) {
202     $template->param('UNIMARC' => 1);
203 }
204
205 if($cgi->cookie("holdfor")){ 
206     my $holdfor_patron = Koha::Patrons->find( $cgi->cookie("holdfor") );
207     if ( $holdfor_patron ) { # may have been deleted in the meanwhile
208         $template->param(
209             holdfor        => $cgi->cookie("holdfor"),
210             holdfor_patron => $holdfor_patron,
211         );
212     }
213 }
214
215 if($cgi->cookie("holdforclub")){
216     my $holdfor_club = Koha::Clubs->find( $cgi->cookie("holdforclub") );
217     if ( $holdfor_club ) { # May have been deleted in the meanwhile
218         $template->param(
219             holdforclub => $cgi->cookie("holdforclub"),
220             holdforclub_name => $holdfor_club->name,
221         );
222     }
223 }
224
225 if($cgi->cookie("searchToOrder")){
226     my ( $basketno, $vendorid ) = split( /\//, $cgi->cookie("searchToOrder") );
227     $template->param(
228         searchtoorder_basketno => $basketno,
229         searchtoorder_vendorid => $vendorid
230     );
231 }
232
233 # get biblionumbers stored in the cart
234 my @cart_list;
235
236 if($cgi->cookie("intranet_bib_list")){
237     my $cart_list = $cgi->cookie("intranet_bib_list");
238     @cart_list = split(/\//, $cart_list);
239 }
240
241 my @search_groups =
242   Koha::Library::Groups->get_search_groups( { interface => 'staff' } )->as_list;
243
244 my $branch_limit = '';
245 my $limit_param = $cgi->param('limit');
246 if ( $limit_param and $limit_param =~ /branch:([\w-]+)/ ) {
247     $branch_limit = $1;
248 }
249
250 $template->param(
251     search_groups    => \@search_groups,
252     branch_limit     => $branch_limit
253 );
254
255 # load the Type stuff
256 my $types = C4::Context->preference("AdvancedSearchTypes") || "itemtypes";
257 my $advancedsearchesloop = prepare_adv_search_types($types);
258 $template->param(advancedsearchesloop => $advancedsearchesloop);
259
260 $template->param( searchid => scalar $cgi->param('searchid'), );
261
262 # The following should only be loaded if we're bringing up the advanced search template
263 if ( $template_type eq 'advsearch' ) {
264
265     my $expanded = $cgi->param('expanded_options');
266     if( $cgi->param('edit_search') ){
267         my @operators = $cgi->multi_param('op');
268         my @indexes   = $cgi->multi_param('idx');
269         my %limit_hash;
270         foreach my $limit (@limits){
271             if ( $limit eq 'available' ){
272                 $template->param( limit_available => 1 );
273             } else {
274                 my ($index,$value) = split(':',$limit);
275                 $value =~ s/"//g;
276                 if ( $index =~ /mc-/ ){
277                     $limit_hash{$index . "_" . $value} = 1;
278                 } else {
279                     push @{$limit_hash{$index}}, $value;
280                 }
281             }
282         };
283         $expanded = 1 if scalar @operators || scalar @limits;
284         $template->param( operators => \@operators );
285         $template->param( limits => \%limit_hash );
286         $template->param(
287            indexes   => \@indexes,
288            sort      => $cgi->param('sort_by'),
289         );
290     }
291     # load the servers (used for searching -- to do federated searching, etc.)
292     my $primary_servers_loop;# = displayPrimaryServers();
293     $template->param(outer_servers_loop =>  $primary_servers_loop,);
294     
295     my $secondary_servers_loop;
296     $template->param(outer_sup_servers_loop => $secondary_servers_loop,);
297
298     # set the default sorting
299     if (   C4::Context->preference('defaultSortField')
300         && C4::Context->preference('defaultSortOrder') ) {
301         my $default_sort_by =
302             C4::Context->preference('defaultSortField') . '_'
303           . C4::Context->preference('defaultSortOrder');
304         $template->param( sort_by => $default_sort_by  );
305     }
306
307     # determine what to display next to the search boxes
308     my @queries = $cgi->multi_param('q');
309     while( scalar @queries < 3 ){
310         push @queries, "";
311     }
312     $template->param(uc(C4::Context->preference("marcflavour")) =>1 );
313
314     # load the language limits (for search)
315     my $languages_limit_loop = getLanguages($lang, 1);
316     $template->param(search_languages_loop => $languages_limit_loop,);
317     $template->param(       queries   => \@queries );
318
319     # Expanded search options in advanced search:
320     # use the global setting by default, but let the user override it
321     {
322         $expanded = C4::Context->preference("expandedSearchOption") || 0
323             if !defined($expanded) || $expanded !~ /^0|1$/;
324         $template->param( expanded_options => $expanded );
325     }
326
327     $template->param(virtualshelves => C4::Context->preference("virtualshelves"));
328
329     output_html_with_http_headers $cgi, $cookie, $template->output;
330     exit;
331 }
332
333 ### OK, if we're this far, we're performing a search, not just loading the advanced search page
334
335 # Fetch the paramater list as a hash in scalar context:
336 #  * returns paramater list as tied hash ref
337 #  * we can edit the values by changing the key
338 #  * multivalued CGI paramaters are returned as a packaged string separated by "\0" (null)
339 my $params = $cgi->Vars;
340 # Params that can have more than one value
341 # sort by is used to sort the query
342 # in theory can have more than one but generally there's just one
343 my @sort_by;
344 my $default_sort_by;
345 if (   C4::Context->preference('defaultSortField')
346     && C4::Context->preference('defaultSortOrder') ) {
347     $default_sort_by =
348         C4::Context->preference('defaultSortField') . '_'
349       . C4::Context->preference('defaultSortOrder');
350 }
351
352 @sort_by = $cgi->multi_param('sort_by');
353 $sort_by[0] = $default_sort_by unless $sort_by[0];
354 foreach my $sort (@sort_by) {
355     $template->param($sort => 1) if $sort;
356 }
357 $template->param('sort_by' => $sort_by[0]);
358
359 # Use the servers defined, or just search our local catalog(default)
360 my @servers = $cgi->multi_param('server');
361 unless (@servers) {
362     #FIXME: this should be handled using Context.pm
363     @servers = ("biblioserver");
364     # @servers = C4::Context->config("biblioserver");
365 }
366 # operators include boolean and proximity operators and are used
367 # to evaluate multiple operands
368 my @operators = map uri_unescape($_), $cgi->multi_param('op');
369
370 # indexes are query qualifiers, like 'title', 'author', etc. They
371 # can be single or multiple parameters separated by comma: kw,right-Truncation 
372 my @indexes = map uri_unescape($_), $cgi->multi_param('idx');
373
374 # if a simple index (only one)  display the index used in the top search box
375 if ($indexes[0] && (!$indexes[1] || $params->{'scan'})) {
376     my $idx = "ms_".$indexes[0];
377     $idx =~ s/\,/comma/g;  # template toolkit doesn't like variables with a , in it
378     $idx =~ s/-/dash/g;  # template toolkit doesn't like variables with a dash in it
379     $template->param(header_pulldown => $idx);
380 }
381
382 # an operand can be a single term, a phrase, or a complete ccl query
383 my @operands = map uri_unescape($_), $cgi->multi_param('q');
384
385 # if a simple search, display the value in the search box
386 my $basic_search = 0;
387 if ($operands[0] && !$operands[1]) {
388     my $ms_query = $operands[0];
389     $ms_query =~ s/ #\S+//;
390     $template->param(ms_value => $ms_query);
391     $basic_search=1;
392 }
393
394 my $available;
395 foreach my $limit(@limits) {
396     if ($limit =~/available/) {
397         $available = 1;
398     }
399 }
400 $template->param(available => $available);
401
402 # append year limits if they exist
403 my $limit_yr;
404 my $limit_yr_value;
405 if ($params->{'limit-yr'}) {
406     if ($params->{'limit-yr'} =~ /\d{4}/) {
407         $limit_yr = "yr,st-numeric:$params->{'limit-yr'}";
408         $limit_yr_value = $params->{'limit-yr'};
409     }
410     push @limits,$limit_yr;
411     #FIXME: Should return a error to the user, incorect date format specified
412 }
413
414 # convert indexes and operands to corresponding parameter names for the z3950 search
415 # $ %z3950p will be a hash ref if the indexes are present (advacned search), otherwise undef
416 my $z3950par;
417 my $indexes2z3950 = {
418     kw=>'title', au=>'author', 'au,phr'=>'author', nb=>'isbn', ns=>'issn',
419     'lcn,phr'=>'dewey', su=>'subject', 'su,phr'=>'subject',
420     ti=>'title', 'ti,phr'=>'title', se=>'title'
421 };
422 for (my $ii = 0; $ii < @operands; ++$ii)
423 {
424     my $name = $indexes2z3950->{$indexes[$ii] || 'kw'};
425     if (defined $name && defined $operands[$ii])
426     {
427         $z3950par ||= {};
428         $z3950par->{$name} = $operands[$ii] if !exists $z3950par->{$name};
429     }
430 }
431
432
433 # Params that can only have one value
434 my $scan = $params->{'scan'};
435 my $count = C4::Context->preference('numSearchResults') || 20;
436 my $results_per_page = $params->{'count'} || $count;
437 my $offset = $params->{'offset'} || 0;
438 my $whole_record = $params->{'whole_record'} || 0;
439 my $weight_search = $params->{'advsearch'} ? $params->{'weight_search'} || 0 : 1;
440 $offset = 0 if $offset < 0;
441 my $page = $cgi->param('page') || 1;
442 #my $offset = ($page-1)*$results_per_page;
443
444 # Define some global variables
445 my ( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type);
446
447 my $builder = Koha::SearchEngine::QueryBuilder->new(
448     { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
449 my $searcher = Koha::SearchEngine::Search->new(
450     { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
451
452 # If index indicates the value is a barocode, we need to preproccess it before searching
453 for ( my $i = 0; $i < @operands; $i++ ) {
454     $operands[$i] = barcodedecode($operands[$i]) if $indexes[$i] eq 'bc';
455 }
456
457 ## I. BUILD THE QUERY
458 (
459     $error,             $query, $simple_query, $query_cgi,
460     $query_desc,        $limit, $limit_cgi,    $limit_desc,
461     $query_type
462   )
463   = $builder->build_query_compat( \@operators, \@operands, \@indexes, \@limits,
464     \@sort_by, $scan, $lang, { weighted_fields => $weight_search, whole_record => $whole_record });
465
466 $template->param( search_query => $query ) if C4::Context->preference('DumpSearchQueryTemplate');
467
468 ## parse the query_cgi string and put it into a form suitable for <input>s
469 my @query_inputs;
470 my $scan_index_to_use;
471 my $scan_search_term_to_use;
472
473 if ($query_cgi) {
474     for my $this_cgi ( split('&', $query_cgi) ) {
475         next unless $this_cgi;
476         $this_cgi =~ m/(.*?)=(.*)/;
477         my $input_name = $1;
478         my $input_value = $2;
479         push @query_inputs, { input_name => $input_name, input_value => Encode::decode_utf8( uri_unescape( $input_value ) ) };
480         if ($input_name eq 'idx') {
481             # The form contains multiple fields, so take the first value as the scan index
482             $scan_index_to_use = $input_value unless $scan_index_to_use;
483         }
484         if (!defined $scan_search_term_to_use && $input_name eq 'q') {
485             $scan_search_term_to_use = Encode::decode_utf8( uri_unescape( $input_value ));
486         }
487     }
488 }
489
490 $template->param ( QUERY_INPUTS => \@query_inputs,
491                    scan_index_to_use => $scan_index_to_use,
492                    scan_search_term_to_use => $scan_search_term_to_use );
493
494 ## parse the limit_cgi string and put it into a form suitable for <input>s
495 my @limit_inputs;
496 my %active_filters;
497 if ($limit_cgi) {
498     for my $this_cgi ( split('&', $limit_cgi) ) {
499         next unless $this_cgi;
500         # handle special case limit-yr
501         if ($this_cgi =~ /yr,st-numeric/) {
502             push @limit_inputs, { input_name => 'limit-yr', input_value => $limit_yr_value };
503             next;
504         }
505         $this_cgi =~ m/(.*=)(.*)/;
506         my $input_name = $1;
507         my $input_value = $2;
508         $input_name =~ s/=$//;
509         push @limit_inputs, { input_name => $input_name, input_value => Encode::decode_utf8( uri_unescape($input_value) ) };
510         if( $input_value =~ /search_filter/ ){
511             my ($filter_id) = ( uri_unescape($input_value) =~ /^search_filter:(.*)$/ );
512             $active_filters{$filter_id} = 1;
513         }
514
515     }
516 }
517 $template->param ( LIMIT_INPUTS => \@limit_inputs );
518
519 ## II. DO THE SEARCH AND GET THE RESULTS
520 my $total = 0; # the total results for the whole set
521 my $facets; # this object stores the faceted results that display on the left-hand of the results page
522 my $results_hashref;
523
524 eval {
525     my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_localization->unblessed } };
526     ( $error, $results_hashref, $facets ) = $searcher->search_compat(
527         $query,            $simple_query, \@sort_by,       \@servers,
528         $results_per_page, $offset,       undef,           $itemtypes,
529         $query_type,       $scan
530     );
531 };
532
533 if ($@ || $error) {
534     my $query_error = q{};
535     $query_error .= $error if $error;
536     $query_error .= $@ if $@;
537     $template->param(query_error => $query_error);
538     output_html_with_http_headers $cgi, $cookie, $template->output;
539     exit;
540 }
541
542 # At this point, each server has given us a result set
543 # now we build that set for template display
544 my @sup_results_array;
545 for (my $i=0;$i<@servers;$i++) {
546     my $server = $servers[$i];
547     if ($server =~/biblioserver/) { # this is the local bibliographic server
548         my $hits = $results_hashref->{$server}->{"hits"} // 0;
549         if ( $hits == 0 && $basic_search ){
550             $operands[0] = '"'.$operands[0].'"'; #quote it
551             ## I. BUILD THE QUERY
552             (
553                 $error,             $query, $simple_query, $query_cgi,
554                 $query_desc,        $limit, $limit_cgi,    $limit_desc,
555                 $query_type
556               )
557               = $builder->build_query_compat( \@operators, \@operands, \@indexes, \@limits,
558                 \@sort_by, $scan, $lang, { weighted_fields => $weight_search, whole_record => $whole_record });
559             my $quoted_results_hashref;
560             eval {
561                 my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_localization->unblessed } };
562                 ( $error, $quoted_results_hashref, $facets ) = $searcher->search_compat(
563                     $query,            $simple_query, \@sort_by,       ['biblioserver'],
564                     $results_per_page, $offset,       undef,           $itemtypes,
565                     $query_type,       $scan
566                 );
567             };
568             my $quoted_hits = $quoted_results_hashref->{$server}->{"hits"} // 0;
569             if ( $quoted_hits ){
570                 $results_hashref->{'biblioserver'} = $quoted_results_hashref->{'biblioserver'};
571                 $hits = $quoted_hits;
572             }
573         }
574         my $page = $cgi->param('page') || 0;
575         my @newresults = searchResults({ 'interface' => 'intranet' }, $query_desc, $hits, $results_per_page, $offset, $scan,
576                                        $results_hashref->{$server}->{"RECORDS"});
577         $total = $total + $hits;
578
579         # Search history
580         if (C4::Context->preference('EnableSearchHistory')) {
581             unless ( $offset ) {
582                 my $path_info = $cgi->url(-path_info=>1);
583                 my $query_cgi_history = $cgi->url(-query=>1);
584                 $query_cgi_history =~ s/^$path_info\?//;
585                 $query_cgi_history =~ s/;/&/g;
586                 my $query_desc_history = $query_desc;
587                 $query_desc_history .= ", $limit_desc"
588                     if $limit_desc;
589
590                 C4::Search::History::add({
591                     userid => $borrowernumber,
592                     sessionid => $cgi->cookie("CGISESSID"),
593                     query_desc => $query_desc_history,
594                     query_cgi => $query_cgi_history,
595                     total => $total,
596                     type => "biblio",
597                 });
598             }
599             $template->param( EnableSearchHistory => 1 );
600         }
601
602         ## If there's just one result, redirect to the detail page unless doing an index scan
603         if ($total == 1 && !$scan) {
604             my $biblionumber = $newresults[0]->{biblionumber};
605             my $defaultview = C4::Context->preference('IntranetBiblioDefaultView');
606             my $views = { C4::Search::enabled_staff_search_views };
607             if ($defaultview eq 'isbd' && $views->{can_view_ISBD}) {
608                 print $cgi->redirect("/cgi-bin/koha/catalogue/ISBDdetail.pl?biblionumber=$biblionumber&found1=1");
609             } elsif  ($defaultview eq 'marc' && $views->{can_view_MARC}) {
610                 print $cgi->redirect("/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=$biblionumber&found1=1");
611             } elsif  ($defaultview eq 'labeled_marc' && $views->{can_view_labeledMARC}) {
612                 print $cgi->redirect("/cgi-bin/koha/catalogue/labeledMARCdetail.pl?biblionumber=$biblionumber&found1=1");
613             } else {
614                 print $cgi->redirect("/cgi-bin/koha/catalogue/detail.pl?biblionumber=$biblionumber&found1=1");
615             } 
616             exit;
617         }
618
619         # set up parameters if user wishes to re-run the search
620         # as a Z39.50 search
621         $template->param (z3950_search_params => C4::Search::z3950_search_args($z3950par || $query_desc));
622         $template->param(limit_cgi => $limit_cgi);
623         $template->param(query_cgi => $query_cgi);
624         $template->param(query_json => encode_json({
625             operators => \@operators,
626             operands => \@operands,
627             indexes => \@indexes
628         }));
629         $template->param(limit_json => encode_json({
630             limits => \@limits
631         }));
632         $template->param(query_desc => $query_desc);
633         $template->param(limit_desc => $limit_desc);
634         $template->param(offset     => $offset);
635         $template->param(offset     => $offset);
636
637
638         if ($hits) {
639             $template->param(total => $hits);
640             if ($limit_cgi) {
641                 my $limit_cgi_not_availablity = $limit_cgi;
642                 $limit_cgi_not_availablity =~ s/&limit=available//g;
643                 $template->param(limit_cgi_not_availablity => $limit_cgi_not_availablity);
644             }
645             $template->param(DisplayMultiPlaceHold => $DisplayMultiPlaceHold);
646             if ($query_desc || $limit_desc) {
647                 $template->param(searchdesc => 1);
648             }
649             $template->param(results_per_page =>  $results_per_page);
650             # must define a value for size if not present in DB
651             # in order to avoid problems generated by the default size value in TT
652             foreach my $line (@newresults) {
653                 if ( not exists $line->{'size'} ) { $line->{'size'} = "" }
654                 # while we're checking each line, see if item is in the cart
655                 if ( grep {$_ eq $line->{'biblionumber'}} @cart_list) {
656                     $line->{'incart'} = 1;
657                 }
658             }
659             my( $page_numbers, $hits_to_paginate, $pages, $current_page_number, $previous_page_offset, $next_page_offset, $last_page_offset ) =
660                 Koha::SearchEngine::Search->pagination_bar(
661                     {
662                         hits              => $hits,
663                         max_result_window => $searcher->max_result_window,
664                         results_per_page  => $results_per_page,
665                         offset            => $offset,
666                         sort_by           => \@sort_by
667                     }
668                 );
669             $template->param( hits_to_paginate => $hits_to_paginate );
670             $template->param(SEARCH_RESULTS => \@newresults);
671             # FIXME: no previous_page_offset when pages < 2
672             $template->param(   PAGE_NUMBERS => $page_numbers,
673                                 last_page_offset => $last_page_offset,
674                                 previous_page_offset => $previous_page_offset) unless $pages < 2;
675             $template->param(   next_page_offset => $next_page_offset) unless $pages eq $current_page_number;
676         }
677
678
679         # no hits
680         else {
681             $template->param(searchdesc => 1,query_desc => $query_desc,limit_desc => $limit_desc);
682         }
683
684     } # end of the if local
685
686     # asynchronously search the authority server
687     elsif ($server =~/authorityserver/) { # this is the local authority server
688         my @inner_sup_results_array;
689         for my $sup_record ( @{$results_hashref->{$server}->{"RECORDS"}} ) {
690             my $marc_record_object = C4::Search::new_record_from_zebra(
691                 'authorityserver',
692                 $sup_record
693             );
694             # warn "Authority Found: ".$marc_record_object->as_formatted();
695             push @inner_sup_results_array, {
696                 'title' => $marc_record_object->field(100)->subfield('a'),
697                 'link' => "&amp;idx=an&amp;q=".$marc_record_object->field('001')->as_string(),
698             };
699         }
700         push @sup_results_array, {  servername => $server, 
701                                     inner_sup_results_loop => \@inner_sup_results_array} if @inner_sup_results_array;
702     }
703     # FIXME: can add support for other targets as needed here
704     $template->param(           outer_sup_results_loop => \@sup_results_array);
705 } #/end of the for loop
706 #$template->param(FEDERATED_RESULTS => \@results_array);
707
708 my $gotonumber = $cgi->param('gotoNumber');
709 if ( $gotonumber && ( $gotonumber eq 'last' || $gotonumber eq 'first' ) ) {
710     $template->{'VARS'}->{'gotoNumber'} = $gotonumber;
711 }
712 $template->{'VARS'}->{'gotoPage'}   = 'detail.pl';
713 my $gotopage = $cgi->param('gotoPage');
714 $template->{'VARS'}->{'gotoPage'} = $gotopage
715   if $gotopage && $gotopage =~ m/^(ISBD|labeledMARC|MARC|more)?detail.pl$/;
716
717 for my $facet ( @$facets ) {
718     for my $entry ( @{ $facet->{facets} } ) {
719         my $index = $entry->{type_link_value};
720         my $value = $entry->{facet_link_value};
721         $entry->{active} = grep { $_->{input_value} eq qq{$index:$value} } @limit_inputs;
722     }
723 }
724
725
726 $template->param(
727     search_filters => Koha::SearchFilters->search({ staff_client => 1 }, { order_by => "name" }),
728     active_filters => \%active_filters,
729 ) if C4::Context->preference('SavedSearchFilters');
730
731 $template->param(
732             #classlist => $classlist,
733             total => $total,
734             opacfacets => 1,
735             facets_loop => $facets,
736             displayFacetCount=> C4::Context->preference('displayFacetCount')||0,
737             scan => $scan,
738             search_error => $error,
739 );
740
741 if ($query_desc || $limit_desc) {
742     $template->param(searchdesc => 1);
743 }
744
745 # VI. BUILD THE TEMPLATE
746
747 my $some_private_shelves = Koha::Virtualshelves->get_some_shelves(
748     {
749         borrowernumber => $borrowernumber,
750         add_allowed    => 1,
751         public         => 0,
752     }
753 );
754 my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
755     {
756         borrowernumber => $borrowernumber,
757         add_allowed    => 1,
758         public         => 1,
759     }
760 );
761
762
763 $template->param(
764     add_to_some_private_shelves => $some_private_shelves,
765     add_to_some_public_shelves  => $some_public_shelves,
766 );
767
768 output_html_with_http_headers $cgi, $cookie, $template->output;
769
770
771 =head2 prepare_adv_search_types
772
773     my $type = C4::Context->preference("AdvancedSearchTypes") || "itemtypes";
774     my @advanced_search_types = prepare_adv_search_types($type);
775
776 Different types can be searched for in the advanced search. This takes the
777 system preference that defines these types and parses it into an arrayref for
778 the template.
779
780 "itemtypes" is handled specially, as itemtypes aren't an authorised value.
781 It also accounts for the "item-level_itypes" system preference.
782
783 =cut
784
785 sub prepare_adv_search_types {
786     my ($types) = @_;
787
788     my @advanced_search_types = split( /\|/, $types );
789
790     # the index parameter is different for item-level itemtypes
791     my $itype_or_itemtype =
792       ( C4::Context->preference("item-level_itypes") ) ? 'itype' : 'itemtype';
793     my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_localization->unblessed } };
794
795     my ( $cnt, @result );
796     foreach my $advanced_srch_type (@advanced_search_types) {
797         $advanced_srch_type =~ s/^\s*//;
798         $advanced_srch_type =~ s/\s*$//;
799         if ( $advanced_srch_type eq 'itemtypes' ) {
800
801        # itemtype is a special case, since it's not defined in authorized values
802             my @itypesloop;
803             foreach my $thisitemtype (
804                 sort {
805                     $itemtypes->{$a}->{'translated_description'}
806                       cmp $itemtypes->{$b}->{'translated_description'}
807                 } keys %$itemtypes
808               )
809             {
810                 my %row = (
811                     number      => $cnt++,
812                     ccl         => "$itype_or_itemtype,phr",
813                     code        => $thisitemtype,
814                     description => $itemtypes->{$thisitemtype}->{'translated_description'},
815                     imageurl    => getitemtypeimagelocation(
816                         'intranet', $itemtypes->{$thisitemtype}->{'imageurl'}
817                     ),
818                 );
819                 push @itypesloop, \%row;
820             }
821             my %search_code = (
822                 advanced_search_type => $advanced_srch_type,
823                 code_loop            => \@itypesloop
824             );
825             push @result, \%search_code;
826         }
827         else {
828             # covers all the other cases: non-itemtype authorized values
829             my $advsearchtypes = GetAuthorisedValues($advanced_srch_type);
830             my @authvalueloop;
831             for my $thisitemtype (@$advsearchtypes) {
832                 my %row = (
833                     number      => $cnt++,
834                     ccl         => $advanced_srch_type,
835                     code        => $thisitemtype->{authorised_value},
836                     description => $thisitemtype->{'lib'},
837                     imageurl    => getitemtypeimagelocation(
838                         'intranet', $thisitemtype->{'imageurl'}
839                     ),
840                 );
841                 push @authvalueloop, \%row;
842             }
843             my %search_code = (
844                 advanced_search_type => $advanced_srch_type,
845                 code_loop            => \@authvalueloop
846             );
847             push @result, \%search_code;
848         }
849     }
850     return \@result;
851 }