(bug #4050) update datelastseen even if document isn't on loan
[koha.git] / C4 / Search.pm
1 package C4::Search;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA  02111-1307 USA
17
18 use strict;
19 # use warnings; # FIXME
20 require Exporter;
21 use C4::Context;
22 use C4::Biblio;    # GetMarcFromKohaField, GetBiblioData
23 use C4::Koha;      # getFacets
24 use Lingua::Stem;
25 use C4::Search::PazPar2;
26 use XML::Simple;
27 use C4::Dates qw(format_date);
28 use C4::XSLT;
29 use C4::Branch;
30 use C4::Debug;
31 use YAML;
32 use URI::Escape;
33
34 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
35
36 # set the version for version checking
37 BEGIN {
38     $VERSION = 3.01;
39     $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
40 }
41
42 =head1 NAME
43
44 C4::Search - Functions for searching the Koha catalog.
45
46 =head1 SYNOPSIS
47
48 See opac/opac-search.pl or catalogue/search.pl for example of usage
49
50 =head1 DESCRIPTION
51
52 This module provides searching functions for Koha's bibliographic databases
53
54 =head1 FUNCTIONS
55
56 =cut
57
58 @ISA    = qw(Exporter);
59 @EXPORT = qw(
60   &FindDuplicate
61   &SimpleSearch
62   &searchResults
63   &SearchAcquisitions
64   &getRecords
65   &buildQuery
66   &NZgetRecords
67 );
68
69 # make all your functions, whether exported or not;
70
71 =head2 FindDuplicate
72
73 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
74
75 This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm
76
77 =cut
78
79 sub FindDuplicate {
80     my ($record) = @_;
81     my $dbh = C4::Context->dbh;
82     my $result = TransformMarcToKoha( $dbh, $record, '' );
83     my $sth;
84     my $query;
85     my $search;
86     my $type;
87     my ( $biblionumber, $title );
88
89     # search duplicate on ISBN, easy and fast..
90     # ... normalize first
91     if ( $result->{isbn} ) {
92         $result->{isbn} =~ s/\(.*$//;
93         $result->{isbn} =~ s/\s+$//;
94         $query = "isbn=$result->{isbn}";
95     }
96     else {
97         $result->{title} =~ s /\\//g;
98         $result->{title} =~ s /\"//g;
99         $result->{title} =~ s /\(//g;
100         $result->{title} =~ s /\)//g;
101
102         # FIXME: instead of removing operators, could just do
103         # quotes around the value
104         $result->{title} =~ s/(and|or|not)//g;
105         $query = "ti,ext=$result->{title}";
106         $query .= " and itemtype=$result->{itemtype}"
107           if ( $result->{itemtype} );
108         if   ( $result->{author} ) {
109             $result->{author} =~ s /\\//g;
110             $result->{author} =~ s /\"//g;
111             $result->{author} =~ s /\(//g;
112             $result->{author} =~ s /\)//g;
113
114             # remove valid operators
115             $result->{author} =~ s/(and|or|not)//g;
116             $query .= " and au,ext=$result->{author}";
117         }
118     }
119
120     # FIXME: add error handling
121     my ( $error, $searchresults ) = SimpleSearch($query); # FIXME :: hardcoded !
122     my @results;
123     foreach my $possible_duplicate_record (@$searchresults) {
124         my $marcrecord =
125           MARC::Record->new_from_usmarc($possible_duplicate_record);
126         my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
127
128         # FIXME :: why 2 $biblionumber ?
129         if ($result) {
130             push @results, $result->{'biblionumber'};
131             push @results, $result->{'title'};
132         }
133     }
134     return @results;
135 }
136
137 =head2 SimpleSearch
138
139 ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
140
141 This function provides a simple search API on the bibliographic catalog
142
143 =over 2
144
145 =item C<input arg:>
146
147     * $query can be a simple keyword or a complete CCL query
148     * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
149     * $offset - If present, represents the number of records at the beggining to omit. Defaults to 0
150     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
151
152
153 =item C<Output:>
154
155     * $error is a empty unless an error is detected
156     * \@results is an array of records.
157     * $total_hits is the number of hits that would have been returned with no limit
158
159 =item C<usage in the script:>
160
161 =back
162
163 my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);
164
165 if (defined $error) {
166     $template->param(query_error => $error);
167     warn "error: ".$error;
168     output_html_with_http_headers $input, $cookie, $template->output;
169     exit;
170 }
171
172 my $hits = scalar @$marcresults;
173 my @results;
174
175 for my $i (0..$hits) {
176     my %resultsloop;
177     my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
178     my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
179
180     #build the hash for the template.
181     $resultsloop{title}           = $biblio->{'title'};
182     $resultsloop{subtitle}        = $biblio->{'subtitle'};
183     $resultsloop{biblionumber}    = $biblio->{'biblionumber'};
184     $resultsloop{author}          = $biblio->{'author'};
185     $resultsloop{publishercode}   = $biblio->{'publishercode'};
186     $resultsloop{publicationyear} = $biblio->{'publicationyear'};
187
188     push @results, \%resultsloop;
189 }
190
191 $template->param(result=>\@results);
192
193 =cut
194
195 sub SimpleSearch {
196     my ( $query, $offset, $max_results, $servers )  = @_;
197     
198     if ( C4::Context->preference('NoZebra') ) {
199         my $result = NZorder( NZanalyse($query) )->{'biblioserver'};
200         my $search_result =
201           (      $result->{hits}
202               && $result->{hits} > 0 ? $result->{'RECORDS'} : [] );
203         return ( undef, $search_result, scalar($result->{hits}) );
204     }
205     else {
206         # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
207         my @servers = defined ( $servers ) ? @$servers : ( "biblioserver" );
208         my @results;
209         my @zoom_queries;
210         my @tmpresults;
211         my @zconns;
212         my $total_hits;
213         return ( "No query entered", undef, undef ) unless $query;
214
215         # Initialize & Search Zebra
216         for ( my $i = 0 ; $i < @servers ; $i++ ) {
217             eval {
218                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
219                 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
220                 $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
221
222                 # error handling
223                 my $error =
224                     $zconns[$i]->errmsg() . " ("
225                   . $zconns[$i]->errcode() . ") "
226                   . $zconns[$i]->addinfo() . " "
227                   . $zconns[$i]->diagset();
228
229                 return ( $error, undef, undef ) if $zconns[$i]->errcode();
230             };
231             if ($@) {
232
233                 # caught a ZOOM::Exception
234                 my $error =
235                     $@->message() . " ("
236                   . $@->code() . ") "
237                   . $@->addinfo() . " "
238                   . $@->diagset();
239                 warn $error;
240                 return ( $error, undef, undef );
241             }
242         }
243         while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
244             my $event = $zconns[ $i - 1 ]->last_event();
245             if ( $event == ZOOM::Event::ZEND ) {
246
247                 my $first_record = defined( $offset ) ? $offset+1 : 1;
248                 my $hits = $tmpresults[ $i - 1 ]->size();
249                 $total_hits += $hits;
250                 my $last_record = $hits;
251                 if ( defined $max_results && $offset + $max_results < $hits ) {
252                     $last_record  = $offset + $max_results;
253                 }
254
255                 for my $j ( $first_record..$last_record ) {
256                     my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
257                     push @results, $record;
258                 }
259             }
260         }
261
262         foreach my $result (@tmpresults) {
263             $result->destroy();
264         }
265         foreach my $zoom_query (@zoom_queries) {
266             $zoom_query->destroy();
267         }
268
269         return ( undef, \@results, $total_hits );
270     }
271 }
272
273 =head2 getRecords
274
275 ( undef, $results_hashref, \@facets_loop ) = getRecords (
276
277         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
278         $results_per_page, $offset,       $expanded_facet, $branches,
279         $query_type,       $scan
280     );
281
282 The all singing, all dancing, multi-server, asynchronous, scanning,
283 searching, record nabbing, facet-building 
284
285 See verbse embedded documentation.
286
287 =cut
288
289 sub getRecords {
290     my (
291         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
292         $results_per_page, $offset,       $expanded_facet, $branches,
293         $query_type,       $scan
294     ) = @_;
295
296     my @servers = @$servers_ref;
297     my @sort_by = @$sort_by_ref;
298
299     # Initialize variables for the ZOOM connection and results object
300     my $zconn;
301     my @zconns;
302     my @results;
303     my $results_hashref = ();
304
305     # Initialize variables for the faceted results objects
306     my $facets_counter = ();
307     my $facets_info    = ();
308     my $facets         = getFacets();
309
310     my @facets_loop
311       ;    # stores the ref to array of hashes for template facets loop
312
313     ### LOOP THROUGH THE SERVERS
314     for ( my $i = 0 ; $i < @servers ; $i++ ) {
315         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
316
317 # perform the search, create the results objects
318 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
319         my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
320
321         #$query_to_use = $simple_query if $scan;
322         warn $simple_query if ( $scan and $DEBUG );
323
324         # Check if we've got a query_type defined, if so, use it
325         eval {
326             if ($query_type) {
327                 if ($query_type =~ /^ccl/) {
328                     $query_to_use =~ s/\:/\=/g;    # change : to = last minute (FIXME)
329                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
330                 } elsif ($query_type =~ /^cql/) {
331                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
332                 } elsif ($query_type =~ /^pqf/) {
333                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
334                 } else {
335                     warn "Unknown query_type '$query_type'.  Results undetermined.";
336                 }
337             } elsif ($scan) {
338                     $results[$i] = $zconns[$i]->scan(  new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
339             } else {
340                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
341             }
342         };
343         if ($@) {
344             warn "WARNING: query problem with $query_to_use " . $@;
345         }
346
347         # Concatenate the sort_by limits and pass them to the results object
348         # Note: sort will override rank
349         my $sort_by;
350         foreach my $sort (@sort_by) {
351             if ( $sort eq "author_az" ) {
352                 $sort_by .= "1=1003 <i ";
353             }
354             elsif ( $sort eq "author_za" ) {
355                 $sort_by .= "1=1003 >i ";
356             }
357             elsif ( $sort eq "popularity_asc" ) {
358                 $sort_by .= "1=9003 <i ";
359             }
360             elsif ( $sort eq "popularity_dsc" ) {
361                 $sort_by .= "1=9003 >i ";
362             }
363             elsif ( $sort eq "call_number_asc" ) {
364                 $sort_by .= "1=20  <i ";
365             }
366             elsif ( $sort eq "call_number_dsc" ) {
367                 $sort_by .= "1=20 >i ";
368             }
369             elsif ( $sort eq "pubdate_asc" ) {
370                 $sort_by .= "1=31 <i ";
371             }
372             elsif ( $sort eq "pubdate_dsc" ) {
373                 $sort_by .= "1=31 >i ";
374             }
375             elsif ( $sort eq "acqdate_asc" ) {
376                 $sort_by .= "1=32 <i ";
377             }
378             elsif ( $sort eq "acqdate_dsc" ) {
379                 $sort_by .= "1=32 >i ";
380             }
381             elsif ( $sort eq "title_az" ) {
382                 $sort_by .= "1=4 <i ";
383             }
384             elsif ( $sort eq "title_za" ) {
385                 $sort_by .= "1=4 >i ";
386             }
387             else {
388                 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
389             }
390         }
391         if ($sort_by) {
392             if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
393                 warn "WARNING sort $sort_by failed";
394             }
395         }
396     }    # finished looping through servers
397
398     # The big moment: asynchronously retrieve results from all servers
399     while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
400         my $ev = $zconns[ $i - 1 ]->last_event();
401         if ( $ev == ZOOM::Event::ZEND ) {
402             next unless $results[ $i - 1 ];
403             my $size = $results[ $i - 1 ]->size();
404             if ( $size > 0 ) {
405                 my $results_hash;
406
407                 # loop through the results
408                 $results_hash->{'hits'} = $size;
409                 my $times;
410                 if ( $offset + $results_per_page <= $size ) {
411                     $times = $offset + $results_per_page;
412                 }
413                 else {
414                     $times = $size;
415                 }
416                 for ( my $j = $offset ; $j < $times ; $j++ ) {
417                     my $records_hash;
418                     my $record;
419                     my $facet_record;
420
421                     ## Check if it's an index scan
422                     if ($scan) {
423                         my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
424
425                  # here we create a minimal MARC record and hand it off to the
426                  # template just like a normal result ... perhaps not ideal, but
427                  # it works for now
428                         my $tmprecord = MARC::Record->new();
429                         $tmprecord->encoding('UTF-8');
430                         my $tmptitle;
431                         my $tmpauthor;
432
433                 # the minimal record in author/title (depending on MARC flavour)
434                         if (C4::Context->preference("marcflavour") eq "UNIMARC") {
435                             $tmptitle = MARC::Field->new('200',' ',' ', a => $term, f => $occ);
436                             $tmprecord->append_fields($tmptitle);
437                         } else {
438                             $tmptitle  = MARC::Field->new('245',' ',' ', a => $term,);
439                             $tmpauthor = MARC::Field->new('100',' ',' ', a => $occ,);
440                             $tmprecord->append_fields($tmptitle);
441                             $tmprecord->append_fields($tmpauthor);
442                         }
443                         $results_hash->{'RECORDS'}[$j] = $tmprecord->as_usmarc();
444                     }
445
446                     # not an index scan
447                     else {
448                         $record = $results[ $i - 1 ]->record($j)->raw();
449
450                         # warn "RECORD $j:".$record;
451                         $results_hash->{'RECORDS'}[$j] = $record;
452
453             # Fill the facets while we're looping, but only for the biblioserver
454                         $facet_record = MARC::Record->new_from_usmarc($record)
455                           if $servers[ $i - 1 ] =~ /biblioserver/;
456
457                     #warn $servers[$i-1]."\n".$record; #.$facet_record->title();
458                         if ($facet_record) {
459                             for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
460                                 ($facets->[$k]) or next;
461                                 my @fields = map {$facet_record->field($_)} @{$facets->[$k]->{'tags'}} ;
462                                 for my $field (@fields) {
463                                     my @subfields = $field->subfields();
464                                     for my $subfield (@subfields) {
465                                         my ( $code, $data ) = @$subfield;
466                                         ($code eq $facets->[$k]->{'subfield'}) or next;
467                                         $facets_counter->{ $facets->[$k]->{'link_value'} }->{$data}++;
468                                     }
469                                 }
470                                 $facets_info->{ $facets->[$k]->{'link_value'} }->{'label_value'} =
471                                     $facets->[$k]->{'label_value'};
472                                 $facets_info->{ $facets->[$k]->{'link_value'} }->{'expanded'} =
473                                     $facets->[$k]->{'expanded'};
474                             }
475                         }
476                     }
477                 }
478                 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
479             }
480
481             # warn "connection ", $i-1, ": $size hits";
482             # warn $results[$i-1]->record(0)->render() if $size > 0;
483
484             # BUILD FACETS
485             if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
486                 for my $link_value (
487                     sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
488                         keys %$facets_counter )
489                 {
490                     my $expandable;
491                     my $number_of_facets;
492                     my @this_facets_array;
493                     for my $one_facet (
494                         sort {
495                              $facets_counter->{$link_value}->{$b}
496                          <=> $facets_counter->{$link_value}->{$a}
497                         } keys %{ $facets_counter->{$link_value} }
498                       )
499                     {
500                         $number_of_facets++;
501                         if (   ( $number_of_facets < 6 )
502                             || ( $expanded_facet eq $link_value )
503                             || ( $facets_info->{$link_value}->{'expanded'} ) )
504                         {
505
506                       # Sanitize the link value ), ( will cause errors with CCL,
507                             my $facet_link_value = $one_facet;
508                             $facet_link_value =~ s/(\(|\))/ /g;
509
510                             # fix the length that will display in the label,
511                             my $facet_label_value = $one_facet;
512                             $facet_label_value =
513                               substr( $one_facet, 0, 20 ) . "..."
514                               unless length($facet_label_value) <= 20;
515
516                             # if it's a branch, label by the name, not the code,
517                             if ( $link_value =~ /branch/ ) {
518                                 $facet_label_value =
519                                   $branches->{$one_facet}->{'branchname'};
520                             }
521
522                             # but we're down with the whole label being in the link's title.
523                             push @this_facets_array, {
524                                 facet_count       => $facets_counter->{$link_value}->{$one_facet},
525                                 facet_label_value => $facet_label_value,
526                                 facet_title_value => $one_facet,
527                                 facet_link_value  => $facet_link_value,
528                                 type_link_value   => $link_value,
529                             };
530                         }
531                     }
532
533                     # handle expanded option
534                     unless ( $facets_info->{$link_value}->{'expanded'} ) {
535                         $expandable = 1
536                           if ( ( $number_of_facets > 6 )
537                             && ( $expanded_facet ne $link_value ) );
538                     }
539                     push @facets_loop, {
540                         type_link_value => $link_value,
541                         type_id         => $link_value . "_id",
542                         "type_label_" . $facets_info->{$link_value}->{'label_value'} => 1, 
543                         facets     => \@this_facets_array,
544                         expandable => $expandable,
545                         expand     => $link_value,
546                     } unless ( ($facets_info->{$link_value}->{'label_value'} =~ /Libraries/) and (C4::Context->preference('singleBranchMode')) );
547                 }
548             }
549         }
550     }
551     return ( undef, $results_hashref, \@facets_loop );
552 }
553
554 sub pazGetRecords {
555     my (
556         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
557         $results_per_page, $offset,       $expanded_facet, $branches,
558         $query_type,       $scan
559     ) = @_;
560
561     my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
562     $paz->init();
563     $paz->search($simple_query);
564     sleep 1;
565
566     # do results
567     my $results_hashref = {};
568     my $stats = XMLin($paz->stat);
569     my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
570    
571     # for a grouped search result, the number of hits
572     # is the number of groups returned; 'bib_hits' will have
573     # the total number of bibs. 
574     $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
575     $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
576
577     HIT: foreach my $hit (@{ $results->{'hit'} }) {
578         my $recid = $hit->{recid}->[0];
579
580         my $work_title = $hit->{'md-work-title'}->[0];
581         my $work_author;
582         if (exists $hit->{'md-work-author'}) {
583             $work_author = $hit->{'md-work-author'}->[0];
584         }
585         my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
586
587         my $result_group = {};
588         $result_group->{'group_label'} = $group_label;
589         $result_group->{'group_merge_key'} = $recid;
590
591         my $count = 1;
592         if (exists $hit->{count}) {
593             $count = $hit->{count}->[0];
594         }
595         $result_group->{'group_count'} = $count;
596
597         for (my $i = 0; $i < $count; $i++) {
598             # FIXME -- may need to worry about diacritics here
599             my $rec = $paz->record($recid, $i);
600             push @{ $result_group->{'RECORDS'} }, $rec;
601         }
602
603         push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
604     }
605     
606     # pass through facets
607     my $termlist_xml = $paz->termlist('author,subject');
608     my $terms = XMLin($termlist_xml, forcearray => 1);
609     my @facets_loop = ();
610     #die Dumper($results);
611 #    foreach my $list (sort keys %{ $terms->{'list'} }) {
612 #        my @facets = ();
613 #        foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
614 #            push @facets, {
615 #                facet_label_value => $facet->{'name'}->[0],
616 #            };
617 #        }
618 #        push @facets_loop, ( {
619 #            type_label => $list,
620 #            facets => \@facets,
621 #        } );
622 #    }
623
624     return ( undef, $results_hashref, \@facets_loop );
625 }
626
627 # STOPWORDS
628 sub _remove_stopwords {
629     my ( $operand, $index ) = @_;
630     my @stopwords_removed;
631
632     # phrase and exact-qualified indexes shouldn't have stopwords removed
633     if ( $index !~ m/phr|ext/ ) {
634
635 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
636 #       we use IsAlpha unicode definition, to deal correctly with diacritics.
637 #       otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
638 #       is a stopword, we'd get "çon" and wouldn't find anything...
639 #       
640                 foreach ( keys %{ C4::Context->stopwords } ) {
641                         next if ( $_ =~ /(and|or|not)/ );    # don't remove operators
642                         $debug && warn "$_ Dump($operand)";
643                         if ( my ($matched) = ($operand =~
644                                 /([^\X\p{isAlnum}]\Q$_\E[^\X\p{isAlnum}]|[^\X\p{isAlnum}]\Q$_\E$|^\Q$_\E[^\X\p{isAlnum}])/gi))
645                         {
646                                 $operand =~ s/\Q$matched\E/ /gi;
647                                 push @stopwords_removed, $_;
648                         }
649                 }
650         }
651     return ( $operand, \@stopwords_removed );
652 }
653
654 # TRUNCATION
655 sub _detect_truncation {
656     my ( $operand, $index ) = @_;
657     my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
658         @regexpr );
659     $operand =~ s/^ //g;
660     my @wordlist = split( /\s/, $operand );
661     foreach my $word (@wordlist) {
662         if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
663             push @rightlefttruncated, $word;
664         }
665         elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
666             push @lefttruncated, $word;
667         }
668         elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
669             push @righttruncated, $word;
670         }
671         elsif ( index( $word, "*" ) < 0 ) {
672             push @nontruncated, $word;
673         }
674         else {
675             push @regexpr, $word;
676         }
677     }
678     return (
679         \@nontruncated,       \@righttruncated, \@lefttruncated,
680         \@rightlefttruncated, \@regexpr
681     );
682 }
683
684 # STEMMING
685 sub _build_stemmed_operand {
686     my ($operand) = @_;
687     my $stemmed_operand;
688
689     # If operand contains a digit, it is almost certainly an identifier, and should
690     # not be stemmed.  This is particularly relevant for ISBNs and ISSNs, which
691     # can contain the letter "X" - for example, _build_stemmend_operand would reduce 
692     # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
693     # results (e.g., "23 x 29 cm." from the 300$c).  Bug 2098.
694     return $operand if $operand =~ /\d/;
695
696 # FIXME: the locale should be set based on the user's language and/or search choice
697     my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
698
699 # FIXME: these should be stored in the db so the librarian can modify the behavior
700     $stemmer->add_exceptions(
701         {
702             'and' => 'and',
703             'or'  => 'or',
704             'not' => 'not',
705         }
706     );
707     my @words = split( / /, $operand );
708     my $stems = $stemmer->stem(@words);
709     for my $stem (@$stems) {
710         $stemmed_operand .= "$stem";
711         $stemmed_operand .= "?"
712           unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
713         $stemmed_operand .= " ";
714     }
715     warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
716     return $stemmed_operand;
717 }
718
719 # FIELD WEIGHTING
720 sub _build_weighted_query {
721
722 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
723 # pretty well but could work much better if we had a smarter query parser
724     my ( $operand, $stemmed_operand, $index ) = @_;
725     my $stemming      = C4::Context->preference("QueryStemming")     || 0;
726     my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
727     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy")        || 0;
728
729     my $weighted_query .= "(rk=(";    # Specifies that we're applying rank
730
731     # Keyword, or, no index specified
732     if ( ( $index eq 'kw' ) || ( !$index ) ) {
733         $weighted_query .=
734           "Title-cover,ext,r1=\"$operand\"";    # exact title-cover
735         $weighted_query .= " or ti,ext,r2=\"$operand\"";    # exact title
736         $weighted_query .= " or ti,phr,r3=\"$operand\"";    # phrase title
737           #$weighted_query .= " or any,ext,r4=$operand";               # exact any
738           #$weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # word list any
739         $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
740           if $fuzzy_enabled;    # add fuzzy, word list
741         $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
742           if ( $stemming and $stemmed_operand )
743           ;                     # add stemming, right truncation
744         $weighted_query .= " or wrdl,r9=\"$operand\"";
745
746         # embedded sorting: 0 a-z; 1 z-a
747         # $weighted_query .= ") or (sort1,aut=1";
748     }
749
750     # Barcode searches should skip this process
751     elsif ( $index eq 'bc' ) {
752         $weighted_query .= "bc=\"$operand\"";
753     }
754
755     # Authority-number searches should skip this process
756     elsif ( $index eq 'an' ) {
757         $weighted_query .= "an=\"$operand\"";
758     }
759
760     # If the index already has more than one qualifier, wrap the operand
761     # in quotes and pass it back (assumption is that the user knows what they
762     # are doing and won't appreciate us mucking up their query
763     elsif ( $index =~ ',' ) {
764         $weighted_query .= " $index=\"$operand\"";
765     }
766
767     #TODO: build better cases based on specific search indexes
768     else {
769         $weighted_query .= " $index,ext,r1=\"$operand\"";    # exact index
770           #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
771         $weighted_query .= " or $index,phr,r3=\"$operand\"";    # phrase index
772         $weighted_query .=
773           " or $index,rt,wrdl,r3=\"$operand\"";    # word list index
774     }
775
776     $weighted_query .= "))";                       # close rank specification
777     return $weighted_query;
778 }
779
780 =head2 getIndexes
781
782 Return an array with available indexes.
783
784 =cut
785
786 sub getIndexes{
787     my @indexes = (
788                     # biblio indexes
789                     'ab',
790                     'Abstract',
791                     'acqdate',
792                     'allrecords',
793                     'an',
794                     'Any',
795                     'at',
796                     'au',
797                     'aub',
798                     'aud',
799                     'audience',
800                     'auo',
801                     'aut',
802                     'Author',
803                     'Author-in-order ',
804                     'Author-personal-bibliography',
805                     'Authority-Number',
806                     'authtype',
807                     'bc',
808                     'biblionumber',
809                     'bio',
810                     'biography',
811                     'callnum',          
812                     'cfn',
813                     'Chronological-subdivision',
814                     'cn-bib-source',
815                     'cn-bib-sort',
816                     'cn-class',
817                     'cn-item',
818                     'cn-prefix',
819                     'cn-suffix',
820                     'cpn',
821                     'Code-institution',
822                     'Conference-name',
823                     'Conference-name-heading',
824                     'Conference-name-see',
825                     'Conference-name-seealso',
826                     'Content-type',
827                     'Control-number',
828                     'copydate',
829                     'Corporate-name',
830                     'Corporate-name-heading',
831                     'Corporate-name-see',
832                     'Corporate-name-seealso',
833                     'ctype',
834                     'date-entered-on-file',
835                     'Date-of-acquisition',
836                     'Date-of-publication',
837                     'Dewey-classification',
838                     'extent',
839                     'fic',
840                     'fiction',
841                     'Form-subdivision',
842                     'format',
843                     'Geographic-subdivision',
844                     'he',
845                     'Heading',
846                     'Heading-use-main-or-added-entry',
847                     'Heading-use-series-added-entry ',
848                     'Heading-use-subject-added-entry',
849                     'Host-item',
850                     'id-other',
851                     'Illustration-code',
852                     'ISBN',
853                     'ISSN',
854                     'itemtype',
855                     'kw',
856                     'Koha-Auth-Number',
857                     'l-format',
858                     'language',
859                     'lc-card',
860                     'LC-card-number',
861                     'lcn',
862                     'llength',
863                     'ln',
864                     'Local-classification',
865                     'Local-number',
866                     'Match-heading',
867                     'Match-heading-see-from',
868                     'Material-type',
869                     'mc-itemtype',
870                     'mc-rtype',
871                     'mus',
872                     'Name-geographic',
873                     'Name-geographic-heading',
874                     'Name-geographic-see',
875                     'Name-geographic-seealso',
876                     'nb',
877                     'Note',
878                     'ns',
879                     'nt',
880                     'pb',
881                     'Personal-name',
882                     'Personal-name-heading',
883                     'Personal-name-see',
884                     'Personal-name-seealso',
885                     'pl',
886                     'Place-publication',
887                     'pn',
888                     'popularity',
889                     'pubdate',
890                     'Publisher',
891                     'Record-type',
892                     'rtype',
893                     'se',
894                     'See',
895                     'See-also',
896                     'sn',
897                     'Stock-number',
898                     'su',
899                     'Subject',
900                     'Subject-heading-thesaurus',
901                     'Subject-name-personal',
902                     'Subject-subdivision',
903                     'Summary',
904                     'Suppress',
905                     'su-geo',
906                     'su-na',
907                     'su-to',
908                     'su-ut',
909                     'ut',
910                     'Term-genre-form',
911                     'Term-genre-form-heading',
912                     'Term-genre-form-see',
913                     'Term-genre-form-seealso',
914                     'ti',
915                     'Title',
916                     'Title-cover',
917                     'Title-series',
918                     'Title-uniform',
919                     'Title-uniform-heading',
920                     'Title-uniform-see',
921                     'Title-uniform-seealso',
922                     'totalissues',
923                     'yr',
924                     
925                     # items indexes
926                     'acqsource',
927                     'barcode',
928                     'bc',
929                     'branch',
930                     'ccode',
931                     'classification-source',
932                     'cn-sort',
933                     'coded-location-qualifier',
934                     'copynumber',
935                     'damaged',
936                     'datelastborrowed',
937                     'datelastseen',
938                     'holdingbranch',
939                     'homebranch',
940                     'issues',
941                     'itemnumber',
942                     'itype',
943                     'Local-classification',
944                     'location',
945                     'lost',
946                     'materials-specified',
947                     'mc-ccode',
948                     'mc-itype',
949                     'mc-loc',
950                     'notforloan',
951                     'onloan',
952                     'price',
953                     'renewals',
954                     'replacementprice',
955                     'replacementpricedate',
956                     'reserves',
957                     'restricted',
958                     'stack',
959                     'uri',
960                     'withdrawn',
961                     
962                     # subject related
963                   );
964                   
965     return \@indexes;
966 }
967
968 =head2 buildQuery
969
970 ( $error, $query,
971 $simple_query, $query_cgi,
972 $query_desc, $limit,
973 $limit_cgi, $limit_desc,
974 $stopwords_removed, $query_type ) = getRecords ( $operators, $operands, $indexes, $limits, $sort_by, $scan);
975
976 Build queries and limits in CCL, CGI, Human,
977 handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
978
979 See verbose embedded documentation.
980
981
982 =cut
983
984 sub buildQuery {
985     my ( $operators, $operands, $indexes, $limits, $sort_by, $scan ) = @_;
986
987     warn "---------\nEnter buildQuery\n---------" if $DEBUG;
988
989     # dereference
990     my @operators = $operators ? @$operators : ();
991     my @indexes   = $indexes   ? @$indexes   : ();
992     my @operands  = $operands  ? @$operands  : ();
993     my @limits    = $limits    ? @$limits    : ();
994     my @sort_by   = $sort_by   ? @$sort_by   : ();
995
996     my $stemming         = C4::Context->preference("QueryStemming")        || 0;
997     my $auto_truncation  = C4::Context->preference("QueryAutoTruncate")    || 0;
998     my $weight_fields    = C4::Context->preference("QueryWeightFields")    || 0;
999     my $fuzzy_enabled    = C4::Context->preference("QueryFuzzy")           || 0;
1000     my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
1001
1002     # no stemming/weight/fuzzy in NoZebra
1003     if ( C4::Context->preference("NoZebra") ) {
1004         $stemming         = 0;
1005         $weight_fields    = 0;
1006         $fuzzy_enabled    = 0;
1007         $auto_truncation  = 0;
1008     }
1009
1010     my $query        = $operands[0];
1011     my $simple_query = $operands[0];
1012
1013     # initialize the variables we're passing back
1014     my $query_cgi;
1015     my $query_desc;
1016     my $query_type;
1017
1018     my $limit;
1019     my $limit_cgi;
1020     my $limit_desc;
1021
1022     my $stopwords_removed;    # flag to determine if stopwords have been removed
1023
1024     my $cclq;
1025     my $cclindexes = getIndexes();
1026     if( $query !~ /\s*ccl=/ ){
1027         for my $index (@$cclindexes){
1028             if($query =~ /($index)(,?\w)*[:=]/){
1029                 $cclq = 1;
1030             }
1031         }
1032         $query = "ccl=$query" if($cclq);
1033     }
1034
1035 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
1036 # DIAGNOSTIC ONLY!!
1037     if ( $query =~ /^ccl=/ ) {
1038         return ( undef, $', $', "q=ccl=$'", $', '', '', '', '', 'ccl' );
1039     }
1040     if ( $query =~ /^cql=/ ) {
1041         return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
1042     }
1043     if ( $query =~ /^pqf=/ ) {
1044         return ( undef, $', $', "q=pqf=$'", $', '', '', '', '', 'pqf' );
1045     }
1046
1047     # pass nested queries directly
1048     # FIXME: need better handling of some of these variables in this case
1049     if ( $query =~ /(\(|\))/ ) {
1050         return (
1051             undef,              $query, $simple_query, $query_cgi,
1052             $query,             $limit, $limit_cgi,    $limit_desc,
1053             $stopwords_removed, 'ccl'
1054         );
1055     }
1056
1057 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
1058 # query operands and indexes and add stemming, truncation, field weighting, etc.
1059 # Once we do so, we'll end up with a value in $query, just like if we had an
1060 # incoming $query from the user
1061     else {
1062         $query = ""
1063           ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
1064         my $previous_operand
1065           ;    # a flag used to keep track if there was a previous query
1066                # if there was, we can apply the current operator
1067                # for every operand
1068         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
1069
1070             # COMBINE OPERANDS, INDEXES AND OPERATORS
1071             if ( $operands[$i] ) {
1072
1073               # A flag to determine whether or not to add the index to the query
1074                 my $indexes_set;
1075
1076 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
1077                 if ( $operands[$i] =~ /(:|=)/ || $scan ) {
1078                     $weight_fields    = 0;
1079                     $stemming         = 0;
1080                     $remove_stopwords = 0;
1081                 }
1082                 my $operand = $operands[$i];
1083                 my $index   = $indexes[$i];
1084
1085                 # Add index-specific attributes
1086                 # Date of Publication
1087                 if ( $index eq 'yr' ) {
1088                     $index .= ",st-numeric";
1089 #                     $indexes_set++;
1090                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1091                 }
1092
1093                 # Date of Acquisition
1094                 elsif ( $index eq 'acqdate' ) {
1095                     $index .= ",st-date-normalized";
1096 #                     $indexes_set++;
1097                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1098                 }
1099                 # ISBN,ISSN,Standard Number, don't need special treatment
1100                 elsif ( $index eq 'nb' || $index eq 'ns' ) {
1101 #                     $indexes_set++;
1102                     (   
1103                         $stemming,      $auto_truncation,
1104                         $weight_fields, $fuzzy_enabled,
1105                         $remove_stopwords
1106                     ) = ( 0, 0, 0, 0, 0 );
1107
1108                 }
1109                 
1110                 if(not $index){
1111                     $index = 'kw';
1112                 }
1113                 
1114                 # Set default structure attribute (word list)
1115                 my $struct_attr;
1116                 unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl)/ ) {
1117                     $struct_attr = ",wrdl";
1118                 }
1119
1120                 # Some helpful index variants
1121                 my $index_plus       = $index . $struct_attr . ":" if $index;
1122                 my $index_plus_comma = $index . $struct_attr . "," if $index;
1123
1124                 # Remove Stopwords
1125                 if ($remove_stopwords) {
1126                     ( $operand, $stopwords_removed ) =
1127                       _remove_stopwords( $operand, $index );
1128                     warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
1129                     warn "REMOVED STOPWORDS: @$stopwords_removed"
1130                       if ( $stopwords_removed && $DEBUG );
1131                 }
1132
1133                 if ($auto_truncation){
1134                                         unless ( $index =~ /(st-|phr|ext)/ ) {
1135                                                 #FIXME only valid with LTR scripts
1136                                                 $operand=join(" ",map{ 
1137                                                                                         (index($_,"*")>0?"$_":"$_*")
1138                                                                                          }split (/\s+/,$operand));
1139                                                 warn $operand if $DEBUG;
1140                                         }
1141                                 }
1142
1143                 # Detect Truncation
1144                 my $truncated_operand;
1145                 my( $nontruncated, $righttruncated, $lefttruncated,
1146                     $rightlefttruncated, $regexpr
1147                 ) = _detect_truncation( $operand, $index );
1148                 warn
1149 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
1150                   if $DEBUG;
1151
1152                 # Apply Truncation
1153                 if (
1154                     scalar(@$righttruncated) + scalar(@$lefttruncated) +
1155                     scalar(@$rightlefttruncated) > 0 )
1156                 {
1157
1158                # Don't field weight or add the index to the query, we do it here
1159                     $indexes_set = 1;
1160                     undef $weight_fields;
1161                     my $previous_truncation_operand;
1162                     if (scalar @$nontruncated) {
1163                         $truncated_operand .= "$index_plus @$nontruncated ";
1164                         $previous_truncation_operand = 1;
1165                     }
1166                     if (scalar @$righttruncated) {
1167                         $truncated_operand .= "and " if $previous_truncation_operand;
1168                         $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
1169                         $previous_truncation_operand = 1;
1170                     }
1171                     if (scalar @$lefttruncated) {
1172                         $truncated_operand .= "and " if $previous_truncation_operand;
1173                         $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
1174                         $previous_truncation_operand = 1;
1175                     }
1176                     if (scalar @$rightlefttruncated) {
1177                         $truncated_operand .= "and " if $previous_truncation_operand;
1178                         $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
1179                         $previous_truncation_operand = 1;
1180                     }
1181                 }
1182                 $operand = $truncated_operand if $truncated_operand;
1183                 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
1184
1185                 # Handle Stemming
1186                 my $stemmed_operand;
1187                 $stemmed_operand = _build_stemmed_operand($operand) if $stemming;
1188
1189                 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
1190
1191                 # Handle Field Weighting
1192                 my $weighted_operand;
1193                 if ($weight_fields) {
1194                     $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
1195                     $operand = $weighted_operand;
1196                     $indexes_set = 1;
1197                 }
1198
1199                 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1200
1201                 # If there's a previous operand, we need to add an operator
1202                 if ($previous_operand) {
1203
1204                     # User-specified operator
1205                     if ( $operators[ $i - 1 ] ) {
1206                         $query     .= " $operators[$i-1] ";
1207                         $query     .= " $index_plus " unless $indexes_set;
1208                         $query     .= " $operand";
1209                         $query_cgi .= "&op=$operators[$i-1]";
1210                         $query_cgi .= "&idx=$index" if $index;
1211                         $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1212                         $query_desc .=
1213                           " $operators[$i-1] $index_plus $operands[$i]";
1214                     }
1215
1216                     # Default operator is and
1217                     else {
1218                         $query      .= " and ";
1219                         $query      .= "$index_plus " unless $indexes_set;
1220                         $query      .= "$operand";
1221                         $query_cgi  .= "&op=and&idx=$index" if $index;
1222                         $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1223                         $query_desc .= " and $index_plus $operands[$i]";
1224                     }
1225                 }
1226
1227                 # There isn't a pervious operand, don't need an operator
1228                 else {
1229
1230                     # Field-weighted queries already have indexes set
1231                     $query .= " $index_plus " unless $indexes_set;
1232                     $query .= $operand;
1233                     $query_desc .= " $index_plus $operands[$i]";
1234                     $query_cgi  .= "&idx=$index" if $index;
1235                     $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1236                     $previous_operand = 1;
1237                 }
1238             }    #/if $operands
1239         }    # /for
1240     }
1241     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1242
1243     # add limits
1244     my $group_OR_limits;
1245     my $availability_limit;
1246     foreach my $this_limit (@limits) {
1247         if ( $this_limit =~ /available/ ) {
1248
1249 # 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1250 # In English:
1251 # all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1252             $availability_limit .=
1253 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1254             $limit_cgi  .= "&limit=available";
1255             $limit_desc .= "";
1256         }
1257
1258         # group_OR_limits, prefixed by mc-
1259         # OR every member of the group
1260         elsif ( $this_limit =~ /mc/ ) {
1261             $group_OR_limits .= " or " if $group_OR_limits;
1262             $limit_desc      .= " or " if $group_OR_limits;
1263             $group_OR_limits .= "$this_limit";
1264             $limit_cgi       .= "&limit=$this_limit";
1265             $limit_desc      .= " $this_limit";
1266         }
1267
1268         # Regular old limits
1269         else {
1270             $limit .= " and " if $limit || $query;
1271             $limit      .= "$this_limit";
1272             $limit_cgi  .= "&limit=$this_limit";
1273             if ($this_limit =~ /^branch:(.+)/) {
1274                 my $branchcode = $1;
1275                 my $branchname = GetBranchName($branchcode);
1276                 if (defined $branchname) {
1277                     $limit_desc .= " branch:$branchname";
1278                 } else {
1279                     $limit_desc .= " $this_limit";
1280                 }
1281             } else {
1282                 $limit_desc .= " $this_limit";
1283             }
1284         }
1285     }
1286     if ($group_OR_limits) {
1287         $limit .= " and " if ( $query || $limit );
1288         $limit .= "($group_OR_limits)";
1289     }
1290     if ($availability_limit) {
1291         $limit .= " and " if ( $query || $limit );
1292         $limit .= "($availability_limit)";
1293     }
1294
1295     # Normalize the query and limit strings
1296     $query =~ s/:/=/g;
1297     $limit =~ s/:/=/g;
1298     for ( $query, $query_desc, $limit, $limit_desc ) {
1299         s/  / /g;    # remove extra spaces
1300         s/^ //g;     # remove any beginning spaces
1301         s/ $//g;     # remove any ending spaces
1302         s/==/=/g;    # remove double == from query
1303     }
1304     $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1305
1306     for ($query_cgi,$simple_query) {
1307         s/"//g;
1308     }
1309     # append the limit to the query
1310     $query .= " " . $limit;
1311
1312     # Warnings if DEBUG
1313     if ($DEBUG) {
1314         warn "QUERY:" . $query;
1315         warn "QUERY CGI:" . $query_cgi;
1316         warn "QUERY DESC:" . $query_desc;
1317         warn "LIMIT:" . $limit;
1318         warn "LIMIT CGI:" . $limit_cgi;
1319         warn "LIMIT DESC:" . $limit_desc;
1320         warn "---------\nLeave buildQuery\n---------";
1321     }
1322     return (
1323         undef,              $query, $simple_query, $query_cgi,
1324         $query_desc,        $limit, $limit_cgi,    $limit_desc,
1325         $stopwords_removed, $query_type
1326     );
1327 }
1328
1329 =head2 searchResults
1330
1331 Format results in a form suitable for passing to the template
1332
1333 =cut
1334
1335 # IMO this subroutine is pretty messy still -- it's responsible for
1336 # building the HTML output for the template
1337 sub searchResults {
1338     my ( $searchdesc, $hits, $results_per_page, $offset, $scan, @marcresults ) = @_;
1339     my $dbh = C4::Context->dbh;
1340     my @newresults;
1341     #Build branchnames hash
1342     #find branchname
1343     #get branch information.....
1344     my %branches;
1345     my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1346     $bsth->execute();
1347     while ( my $bdata = $bsth->fetchrow_hashref ) {
1348         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1349     }
1350 # FIXME - We build an authorised values hash here, using the default framework
1351 # though it is possible to have different authvals for different fws.
1352
1353     my $shelflocations =GetKohaAuthorisedValues('items.location','');
1354
1355     # get notforloan authorised value list (see $shelflocations  FIXME)
1356     my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1357
1358     #Build itemtype hash
1359     #find itemtype & itemtype image
1360     my %itemtypes;
1361     $bsth =
1362       $dbh->prepare(
1363         "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1364       );
1365     $bsth->execute();
1366     while ( my $bdata = $bsth->fetchrow_hashref ) {
1367                 foreach (qw(description imageurl summary notforloan)) {
1368                 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1369                 }
1370     }
1371
1372     #search item field code
1373     my $sth =
1374       $dbh->prepare(
1375 "SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
1376       );
1377     $sth->execute;
1378     my ($itemtag) = $sth->fetchrow;
1379
1380     ## find column names of items related to MARC
1381     my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1382     $sth2->execute;
1383     my %subfieldstosearch;
1384     while ( ( my $column ) = $sth2->fetchrow ) {
1385         my ( $tagfield, $tagsubfield ) =
1386           &GetMarcFromKohaField( "items." . $column, "" );
1387         $subfieldstosearch{$column} = $tagsubfield;
1388     }
1389
1390     # handle which records to actually retrieve
1391     my $times;
1392     if ( $hits && $offset + $results_per_page <= $hits ) {
1393         $times = $offset + $results_per_page;
1394     }
1395     else {
1396         $times = $hits;  # FIXME: if $hits is undefined, why do we want to equal it?
1397     }
1398     my $marcflavour = C4::Context->preference("marcflavour");
1399
1400     # We get the biblionumber position in MARC 
1401     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1402     my $fw = '';
1403     
1404     # loop through all of the records we've retrieved
1405     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1406         my $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
1407                 my $biblionumber;
1408
1409         if(not $scan){
1410             if ($bibliotag<10){
1411                 $biblionumber = $marcrecord->field($bibliotag) ? $marcrecord->field($bibliotag)->data : undef;
1412             }else{
1413                 $biblionumber = $marcrecord->subfield($bibliotag,$bibliosubf);
1414             } 
1415             $fw = (defined $biblionumber) ? GetFrameworkCode($biblionumber) : '';
1416         }
1417         
1418         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1419         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1420         $oldbiblio->{result_number} = $i + 1;
1421
1422         # add imageurl to itemtype if there is one
1423         $oldbiblio->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1424
1425         $oldbiblio->{'authorised_value_images'}  = C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) );
1426         (my $aisbn) = $oldbiblio->{isbn} =~ /([\d-]*[X]*)/;
1427         $aisbn =~ s/-//g;
1428         $oldbiblio->{amazonisbn} = $aisbn;
1429         $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1430         $oldbiblio->{normalized_upc} = GetNormalizedUPC($marcrecord,$marcflavour);
1431         $oldbiblio->{normalized_ean} = GetNormalizedEAN($marcrecord,$marcflavour);
1432         $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1433         $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1434         $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1435         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1436         $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1437  # Build summary if there is one (the summary is defined in the itemtypes table)
1438  # FIXME: is this used anywhere, I think it can be commented out? -- JF
1439         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1440             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1441             my @fields  = $marcrecord->fields();
1442             
1443             my $newsummary;
1444             foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1445                 my $tags = {};
1446                 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1447                     $tag =~ /(.{3})(.)/;
1448                     if($marcrecord->field($1)){
1449                         my @abc = $marcrecord->field($1)->subfield($2);
1450                         $tags->{$tag} = $#abc + 1 ;
1451                     }
1452                 }
1453                 
1454                 # We catch how many times to repeat this line
1455                 my $max = 0;
1456                 foreach my $tag (keys(%$tags)){
1457                     $max = $tags->{$tag} if($tags->{$tag} > $max);
1458                  }
1459                 
1460                 # we replace, and repeat each line
1461                 for (my $i = 0 ; $i < $max ; $i++){
1462                     my $newline = $line;
1463
1464                     foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1465                         $tag =~ /(.{3})(.)/;
1466                         
1467                         if($marcrecord->field($1)){
1468                             my @repl = $marcrecord->field($1)->subfield($2);
1469                             my $subfieldvalue = $repl[$i];
1470                             
1471                             if (! utf8::is_utf8($subfieldvalue)) {
1472                                 utf8::decode($subfieldvalue);
1473                             }
1474  
1475                              $newline =~ s/\[$tag\]/$subfieldvalue/g;
1476                         }
1477                     }
1478                     $newsummary .= "$newline\n";
1479                 }
1480             }
1481
1482             $newsummary =~ s/\[(.*?)]//g;
1483             $newsummary =~ s/\n/<br\/>/g;
1484             $oldbiblio->{summary} = $newsummary;
1485         }
1486
1487         # Pull out the items fields
1488         my @fields = $marcrecord->field($itemtag);
1489
1490         # Setting item statuses for display
1491         my @available_items_loop;
1492         my @onloan_items_loop;
1493         my @notforloan_items_loop;
1494         my @other_items_loop;
1495
1496         my $available_items;
1497         my $onloan_items;
1498         my $notforloan_items;
1499         my $other_items;
1500
1501         my $ordered_count         = 0;
1502         my $available_count       = 0;
1503         my $onloan_count          = 0;
1504         my $notforloan_count      = 0;
1505         my $longoverdue_count     = 0;
1506         my $other_count           = 0;
1507         my $wthdrawn_count        = 0;
1508         my $itemlost_count        = 0;
1509         my $itembinding_count     = 0;
1510         my $itemdamaged_count     = 0;
1511         my $item_in_transit_count = 0;
1512         my $can_place_holds       = 0;
1513         my $items_count           = scalar(@fields);
1514         my $maxitems =
1515           ( C4::Context->preference('maxItemsinSearchResults') )
1516           ? C4::Context->preference('maxItemsinSearchResults') - 1
1517           : 1;
1518
1519         # loop through every item
1520         foreach my $field (@fields) {
1521             my $item;
1522
1523             # populate the items hash
1524             foreach my $code ( keys %subfieldstosearch ) {
1525                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1526             }
1527                         my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
1528                         my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1529             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1530             if ($item->{$hbranch}) {
1531                 $item->{'branchname'} = $branches{$item->{$hbranch}};
1532             }
1533             elsif ($item->{$otherbranch}) {     # Last resort
1534                 $item->{'branchname'} = $branches{$item->{$otherbranch}}; 
1535             }
1536             
1537             ($item->{'reserved'}) = C4::Reserves::CheckReserves($item->{itemnumber});
1538             
1539                         my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1540 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1541             if ( $item->{onloan} or $item->{reserved} ) {
1542                 $onloan_count++;
1543                                 my $key = $prefix . $item->{onloan} . $item->{barcode};
1544                                 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1545                                 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1546                                 $onloan_items->{$key}->{branchname} = $item->{branchname};
1547                                 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1548                                 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1549                                 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1550                                 $onloan_items->{$key}->{barcode} = $item->{barcode};
1551                                 $onloan_items->{$key}->{reserved} = $item->{reserved};
1552                 # if something's checked out and lost, mark it as 'long overdue'
1553                 if ( $item->{itemlost} ) {
1554                     $onloan_items->{$prefix}->{longoverdue}++;
1555                     $longoverdue_count++;
1556                 } else {        # can place holds as long as item isn't lost
1557                     $can_place_holds = 1;
1558                 }
1559             }
1560
1561          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1562             else {
1563
1564                 # item is on order
1565                 if ( $item->{notforloan} == -1 ) {
1566                     $ordered_count++;
1567                 }
1568
1569                 # is item in transit?
1570                 my $transfertwhen = '';
1571                 my ($transfertfrom, $transfertto);
1572                 
1573                 unless ($item->{wthdrawn}
1574                         || $item->{itemlost}
1575                         || $item->{damaged}
1576                         || $item->{notforloan}
1577                         || $items_count > 20) {
1578
1579                     # A couple heuristics to limit how many times
1580                     # we query the database for item transfer information, sacrificing
1581                     # accuracy in some cases for speed;
1582                     #
1583                     # 1. don't query if item has one of the other statuses
1584                     # 2. don't check transit status if the bib has
1585                     #    more than 20 items
1586                     #
1587                     # FIXME: to avoid having the query the database like this, and to make
1588                     #        the in transit status count as unavailable for search limiting,
1589                     #        should map transit status to record indexed in Zebra.
1590                     #
1591                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1592                 }
1593
1594                 # item is withdrawn, lost or damaged
1595                 if (   $item->{wthdrawn}
1596                     || $item->{itemlost}
1597                     || $item->{damaged}
1598                     || $item->{notforloan} 
1599                     || $item->{reserved}
1600                     || ($transfertwhen ne ''))
1601                 {
1602                     $wthdrawn_count++        if $item->{wthdrawn};
1603                     $itemlost_count++        if $item->{itemlost};
1604                     $itemdamaged_count++     if $item->{damaged};
1605                     $item_in_transit_count++ if $transfertwhen ne '';
1606                     $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1607
1608                                         my $key = $prefix . $item->{status};
1609                                         
1610                                         foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
1611                                             if($item->{notforloan} == 1){
1612                                                 $notforloan_items->{$key}->{$_} = $item->{$_};
1613                                             }else{
1614                            $other_items->{$key}->{$_} = $item->{$_};
1615                                             }
1616                                         }
1617                                         if($item->{notforloan} == 1){
1618                         $notforloan_count++;
1619
1620                         $notforloan_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
1621                                         $notforloan_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1622                                         $notforloan_items->{$key}->{count}++ if $item->{$hbranch};
1623                                         $notforloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1624                                         $notforloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1625                                         $notforloan_items->{$key}->{barcode} = $item->{barcode};
1626                     }else{
1627                         $other_count++;
1628                                         
1629                         $other_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
1630                                         $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1631                                         $other_items->{$key}->{count}++ if $item->{$hbranch};
1632                                         $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1633                                         $other_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1634                                         $other_items->{$key}->{barcode} = $item->{barcode};
1635                     }
1636
1637                 }
1638                 # item is available
1639                 else {
1640                     $can_place_holds = 1;
1641                     $available_count++;
1642                                         $available_items->{$prefix}->{count}++ if $item->{$hbranch};
1643                                         foreach (qw(branchname itemcallnumber barcode)) {
1644                         $available_items->{$prefix}->{$_} = $item->{$_};
1645                                         }
1646                                         $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
1647                                         $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1648                 }
1649             }
1650         }    # notforloan, item level and biblioitem level
1651         my ( $availableitemscount, $onloanitemscount, $notforloanitemscount,$otheritemscount );
1652         $maxitems =
1653           ( C4::Context->preference('maxItemsinSearchResults') )
1654           ? C4::Context->preference('maxItemsinSearchResults') - 1
1655           : 1;
1656         for my $key ( sort keys %$onloan_items ) {
1657             (++$onloanitemscount > $maxitems) and last;
1658             push @onloan_items_loop, $onloan_items->{$key};
1659         }
1660         for my $key ( sort keys %$other_items ) {
1661             (++$otheritemscount > $maxitems) and last;
1662             push @other_items_loop, $other_items->{$key};
1663         }
1664         for my $key ( sort keys %$notforloan_items ) {
1665             (++$notforloanitemscount > $maxitems) and last;
1666             push @notforloan_items_loop, $notforloan_items->{$key};
1667         }
1668         for my $key ( sort keys %$available_items ) {
1669             (++$availableitemscount > $maxitems) and last;
1670             push @available_items_loop, $available_items->{$key}
1671         }
1672
1673         # XSLT processing of some stuff
1674         # FIXME : This needs some work in order to be more flexible : Can not use a result list for intranet different from OPAC
1675         if (C4::Context->preference("XSLTResultsDisplay") && !$scan) {
1676             $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display(
1677                 $oldbiblio->{biblionumber}, $marcrecord, C4::Context->preference("XSLTResultsDisplay") );
1678         }
1679
1680         # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1681         $can_place_holds = 0 if $itemtypes{ $oldbiblio->{itemtype} }->{notforloan};
1682         $oldbiblio->{norequests} = 1 unless $can_place_holds;
1683         $oldbiblio->{itemsplural}          = 1 if $items_count > 1;
1684         $oldbiblio->{items_count}          = $items_count;
1685         $oldbiblio->{available_items_loop} = \@available_items_loop;
1686         $oldbiblio->{notforloan_items_loop}= \@notforloan_items_loop;
1687         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
1688         $oldbiblio->{other_items_loop}     = \@other_items_loop;
1689         $oldbiblio->{availablecount}       = $available_count;
1690         $oldbiblio->{availableplural}      = 1 if $available_count > 1;
1691         $oldbiblio->{onloancount}          = $onloan_count;
1692         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
1693         $oldbiblio->{notforloancount}      = $notforloan_count;
1694         $oldbiblio->{othercount}           = $other_count;
1695         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
1696         $oldbiblio->{wthdrawncount}        = $wthdrawn_count;
1697         $oldbiblio->{itemlostcount}        = $itemlost_count;
1698         $oldbiblio->{damagedcount}         = $itemdamaged_count;
1699         $oldbiblio->{intransitcount}       = $item_in_transit_count;
1700         $oldbiblio->{orderedcount}         = $ordered_count;
1701         $oldbiblio->{isbn} =~
1702           s/-//g;    # deleting - in isbn to enable amazon content
1703         push( @newresults, $oldbiblio );
1704     }
1705     return @newresults;
1706 }
1707
1708 =head2 SearchAcquisitions
1709     Search for acquisitions 
1710 =cut
1711
1712 sub SearchAcquisitions{
1713     my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
1714     
1715     my $dbh=C4::Context->dbh;
1716     # Variable initialization
1717     my $str=qq|
1718     SELECT marcxml 
1719     FROM biblio 
1720     LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
1721     LEFT JOIN items ON items.biblionumber=biblio.biblionumber
1722     WHERE dateaccessioned BETWEEN ? AND ? 
1723     |;
1724     
1725     my (@params,@loopcriteria);
1726     
1727     push @params, $datebegin->output("iso");
1728     push @params, $dateend->output("iso");
1729
1730     if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
1731         if(C4::Context->preference("item-level_itypes")){
1732             $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1733         }else{
1734             $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1735         }    
1736         push @params, @$itemtypes;
1737     }
1738         
1739     if ($criteria =~/itemtype/){
1740         if(C4::Context->preference("item-level_itypes")){
1741             $str .= "AND items.itype=? ";
1742         }else{
1743             $str .= "AND biblioitems.itemtype=? ";
1744         }
1745         
1746         if(scalar(@$itemtypes) == 0){
1747             my $itypes = GetItemTypes();
1748             for my $key (keys %$itypes){
1749                 push @$itemtypes, $key;
1750             }
1751         }
1752         
1753         @loopcriteria= @$itemtypes;
1754     }elsif ($criteria=~/itemcallnumber/){
1755         $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%') 
1756                  OR items.itemcallnumber is NULL
1757                  OR items.itemcallnumber = '')";
1758
1759         @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);  
1760     }else {
1761         $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
1762         @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);  
1763     }
1764         
1765     if ($orderby =~ /date_desc/){
1766         $str.=" ORDER BY dateaccessioned DESC";
1767     } else {
1768         $str.=" ORDER BY title";
1769     }
1770     
1771     my $qdataacquisitions=$dbh->prepare($str);
1772         
1773     my @loopacquisitions;
1774     foreach my $value(@loopcriteria){
1775         push @params,$value;
1776         my %cell;
1777         $cell{"title"}=$value;
1778         $cell{"titlecode"}=$value;
1779         
1780         eval{$qdataacquisitions->execute(@params);};
1781   
1782         if ($@){ warn "recentacquisitions Error :$@";}
1783         else {
1784             my @loopdata;
1785             while (my $data=$qdataacquisitions->fetchrow_hashref){
1786                 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
1787             }
1788             $cell{"loopdata"}=\@loopdata;
1789         }
1790         push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
1791         pop @params;
1792     }
1793     $qdataacquisitions->finish;
1794     return \@loopacquisitions;
1795 }
1796
1797 #----------------------------------------------------------------------
1798 #
1799 # Non-Zebra GetRecords#
1800 #----------------------------------------------------------------------
1801
1802 =head2 NZgetRecords
1803
1804   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1805
1806 =cut
1807
1808 sub NZgetRecords {
1809     my (
1810         $query,            $simple_query, $sort_by_ref,    $servers_ref,
1811         $results_per_page, $offset,       $expanded_facet, $branches,
1812         $query_type,       $scan
1813     ) = @_;
1814     warn "query =$query" if $DEBUG;
1815     my $result = NZanalyse($query);
1816     warn "results =$result" if $DEBUG;
1817     return ( undef,
1818         NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1819         undef );
1820 }
1821
1822 =head2 NZanalyse
1823
1824   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1825   the list is built from an inverted index in the nozebra SQL table
1826   note that title is here only for convenience : the sorting will be very fast when requested on title
1827   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1828
1829 =cut
1830
1831 sub NZanalyse {
1832     my ( $string, $server ) = @_;
1833 #     warn "---------"       if $DEBUG;
1834     warn " NZanalyse" if $DEBUG;
1835 #     warn "---------"       if $DEBUG;
1836
1837  # $server contains biblioserver or authorities, depending on what we search on.
1838  #warn "querying : $string on $server";
1839     $server = 'biblioserver' unless $server;
1840
1841 # if we have a ", replace the content to discard temporarily any and/or/not inside
1842     my $commacontent;
1843     if ( $string =~ /"/ ) {
1844         $string =~ s/"(.*?)"/__X__/;
1845         $commacontent = $1;
1846         warn "commacontent : $commacontent" if $DEBUG;
1847     }
1848
1849 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1850 # then, call again NZanalyse with $left and $right
1851 # (recursive until we find a leaf (=> something without and/or/not)
1852 # delete repeated operator... Would then go in infinite loop
1853     while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
1854     }
1855
1856     #process parenthesis before.
1857     if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
1858         my $left     = $1;
1859         my $right    = $4;
1860         my $operator = lc($3);   # FIXME: and/or/not are operators, not operands
1861         warn
1862 "dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
1863           if $DEBUG;
1864         my $leftresult = NZanalyse( $left, $server );
1865         if ($operator) {
1866             my $rightresult = NZanalyse( $right, $server );
1867
1868             # OK, we have the results for right and left part of the query
1869             # depending of operand, intersect, union or exclude both lists
1870             # to get a result list
1871             if ( $operator eq ' and ' ) {
1872                 return NZoperatorAND($leftresult,$rightresult);      
1873             }
1874             elsif ( $operator eq ' or ' ) {
1875
1876                 # just merge the 2 strings
1877                 return $leftresult . $rightresult;
1878             }
1879             elsif ( $operator eq ' not ' ) {
1880                 return NZoperatorNOT($leftresult,$rightresult);      
1881             }
1882         }      
1883         else {
1884 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1885             return $leftresult;
1886         } 
1887     }
1888     warn "string :" . $string if $DEBUG;
1889     my $left = "";
1890     my $right = "";
1891     my $operator = "";
1892     if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
1893         $left     = $1;
1894         $right    = $3;
1895         $operator = lc($2);    # FIXME: and/or/not are operators, not operands
1896     }
1897     warn "no parenthesis. left : $left operator: $operator right: $right"
1898       if $DEBUG;
1899
1900     # it's not a leaf, we have a and/or/not
1901     if ($operator) {
1902
1903         # reintroduce comma content if needed
1904         $right =~ s/__X__/"$commacontent"/ if $commacontent;
1905         $left  =~ s/__X__/"$commacontent"/ if $commacontent;
1906         warn "node : $left / $operator / $right\n" if $DEBUG;
1907         my $leftresult  = NZanalyse( $left,  $server );
1908         my $rightresult = NZanalyse( $right, $server );
1909         warn " leftresult : $leftresult" if $DEBUG;
1910         warn " rightresult : $rightresult" if $DEBUG;
1911         # OK, we have the results for right and left part of the query
1912         # depending of operand, intersect, union or exclude both lists
1913         # to get a result list
1914         if ( $operator eq ' and ' ) {
1915             warn "NZAND";
1916             return NZoperatorAND($leftresult,$rightresult);
1917         }
1918         elsif ( $operator eq ' or ' ) {
1919
1920             # just merge the 2 strings
1921             return $leftresult . $rightresult;
1922         }
1923         elsif ( $operator eq ' not ' ) {
1924             return NZoperatorNOT($leftresult,$rightresult);
1925         }
1926         else {
1927
1928 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1929             die "error : operand unknown : $operator for $string";
1930         }
1931
1932         # it's a leaf, do the real SQL query and return the result
1933     }
1934     else {
1935         $string =~ s/__X__/"$commacontent"/ if $commacontent;
1936         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
1937         #remove trailing blank at the beginning
1938         $string =~ s/^ //g;
1939         warn "leaf:$string" if $DEBUG;
1940
1941         # parse the string in in operator/operand/value again
1942         my $left = "";
1943         my $operator = "";
1944         my $right = "";
1945         if ($string =~ /(.*)(>=|<=)(.*)/) {
1946             $left     = $1;
1947             $operator = $2;
1948             $right    = $3;
1949         } else {
1950             $left = $string;
1951         }
1952 #         warn "handling leaf... left:$left operator:$operator right:$right"
1953 #           if $DEBUG;
1954         unless ($operator) {
1955             if ($string =~ /(.*)(>|<|=)(.*)/) {
1956                 $left     = $1;
1957                 $operator = $2;
1958                 $right    = $3;
1959                 warn
1960     "handling unless (operator)... left:$left operator:$operator right:$right"
1961                 if $DEBUG;
1962             } else {
1963                 $left = $string;
1964             }
1965         }
1966         my $results;
1967
1968 # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
1969         $left =~ s/ .*$//;
1970
1971         # automatic replace for short operators
1972         $left = 'title'            if $left =~ '^ti$';
1973         $left = 'author'           if $left =~ '^au$';
1974         $left = 'publisher'        if $left =~ '^pb$';
1975         $left = 'subject'          if $left =~ '^su$';
1976         $left = 'koha-Auth-Number' if $left =~ '^an$';
1977         $left = 'keyword'          if $left =~ '^kw$';
1978         $left = 'itemtype'         if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra 
1979         warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
1980         my $dbh = C4::Context->dbh;
1981         if ( $operator && $left ne 'keyword' ) {
1982             #do a specific search
1983             $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
1984             my $sth = $dbh->prepare(
1985 "SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
1986             );
1987             warn "$left / $operator / $right\n" if $DEBUG;
1988
1989             # split each word, query the DB and build the biblionumbers result
1990             #sanitizing leftpart
1991             $left =~ s/^\s+|\s+$//;
1992             foreach ( split / /, $right ) {
1993                 my $biblionumbers;
1994                 $_ =~ s/^\s+|\s+$//;
1995                 next unless $_;
1996                 warn "EXECUTE : $server, $left, $_" if $DEBUG;
1997                 $sth->execute( $server, $left, $_ )
1998                   or warn "execute failed: $!";
1999                 while ( my ( $line, $value ) = $sth->fetchrow ) {
2000
2001 # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
2002 # otherwise, fill the result
2003                     $biblionumbers .= $line
2004                       unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
2005                     warn "result : $value "
2006                       . ( $right  =~ /\d/ ) . "=="
2007                       . ( $value =~ /\D/?$line:"" ) if $DEBUG;         #= $line";
2008                 }
2009
2010 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2011                 if ($results) {
2012                     warn "NZAND" if $DEBUG;
2013                     $results = NZoperatorAND($biblionumbers,$results);
2014                 } else {
2015                     $results = $biblionumbers;
2016                 }
2017             }
2018         }
2019         else {
2020       #do a complete search (all indexes), if index='kw' do complete search too.
2021             my $sth = $dbh->prepare(
2022 "SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
2023             );
2024
2025             # split each word, query the DB and build the biblionumbers result
2026             foreach ( split (/ /, $string )) {
2027                 next if C4::Context->stopwords->{ uc($_) };   # skip if stopword
2028                 warn "search on all indexes on $_" if $DEBUG;
2029                 my $biblionumbers;
2030                 next unless $_;
2031                 $sth->execute( $server, $_ );
2032                 while ( my $line = $sth->fetchrow ) {
2033                     $biblionumbers .= $line;
2034                 }
2035
2036 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2037                 if ($results) {
2038                     $results = NZoperatorAND($biblionumbers,$results);
2039                 }
2040                 else {
2041                     warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
2042                     $results = $biblionumbers;
2043                 }
2044             }
2045         }
2046         warn "return : $results for LEAF : $string" if $DEBUG;
2047         return $results;
2048     }
2049     warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
2050 }
2051
2052 sub NZoperatorAND{
2053     my ($rightresult, $leftresult)=@_;
2054     
2055     my @leftresult = split (/;/, $leftresult);
2056     warn " @leftresult / $rightresult \n" if $DEBUG;
2057     
2058     #             my @rightresult = split /;/,$leftresult;
2059     my $finalresult;
2060
2061 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
2062 # the result is stored twice, to have the same weight for AND than OR.
2063 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
2064 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
2065     foreach (@leftresult) {
2066         my $value = $_;
2067         my $countvalue;
2068         ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
2069         if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
2070             $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
2071             $finalresult .=
2072                 "$value-$countvalue;$value-$countvalue;";
2073         }
2074     }
2075     warn "NZAND DONE : $finalresult \n" if $DEBUG;
2076     return $finalresult;
2077 }
2078       
2079 sub NZoperatorOR{
2080     my ($rightresult, $leftresult)=@_;
2081     return $rightresult.$leftresult;
2082 }
2083
2084 sub NZoperatorNOT{
2085     my ($leftresult, $rightresult)=@_;
2086     
2087     my @leftresult = split /;/, $leftresult;
2088
2089     #             my @rightresult = split /;/,$leftresult;
2090     my $finalresult;
2091     foreach (@leftresult) {
2092         my $value=$_;
2093         $value=$1 if $value=~m/(.*)-\d+$/;
2094         unless ($rightresult =~ "$value-") {
2095             $finalresult .= "$_;";
2096         }
2097     }
2098     return $finalresult;
2099 }
2100
2101 =head2 NZorder
2102
2103   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
2104   
2105   TODO :: Description
2106
2107 =cut
2108
2109 sub NZorder {
2110     my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
2111     warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
2112
2113     # order title asc by default
2114     #     $ordering = '1=36 <i' unless $ordering;
2115     $results_per_page = 20 unless $results_per_page;
2116     $offset           = 0  unless $offset;
2117     my $dbh = C4::Context->dbh;
2118
2119     #
2120     # order by POPULARITY
2121     #
2122     if ( $ordering =~ /popularity/ ) {
2123         my %result;
2124         my %popularity;
2125
2126         # popularity is not in MARC record, it's builded from a specific query
2127         my $sth =
2128           $dbh->prepare("select sum(issues) from items where biblionumber=?");
2129         foreach ( split (/;/, $biblionumbers )) {
2130             my ( $biblionumber, $title ) = split (/,/, $_);
2131             $result{$biblionumber} = GetMarcBiblio($biblionumber);
2132             $sth->execute($biblionumber);
2133             my $popularity = $sth->fetchrow || 0;
2134
2135 # hint : the key is popularity.title because we can have
2136 # many results with the same popularity. In this case, sub-ordering is done by title
2137 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
2138 # (un-frequent, I agree, but we won't forget anything that way ;-)
2139             $popularity{ sprintf( "%10d", $popularity ) . $title
2140                   . $biblionumber } = $biblionumber;
2141         }
2142
2143     # sort the hash and return the same structure as GetRecords (Zebra querying)
2144         my $result_hash;
2145         my $numbers = 0;
2146         if ( $ordering eq 'popularity_dsc' ) {    # sort popularity DESC
2147             foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
2148                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2149                   $result{ $popularity{$key} }->as_usmarc();
2150             }
2151         }
2152         else {                                    # sort popularity ASC
2153             foreach my $key ( sort ( keys %popularity ) ) {
2154                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2155                   $result{ $popularity{$key} }->as_usmarc();
2156             }
2157         }
2158         my $finalresult = ();
2159         $result_hash->{'hits'}         = $numbers;
2160         $finalresult->{'biblioserver'} = $result_hash;
2161         return $finalresult;
2162
2163         #
2164         # ORDER BY author
2165         #
2166     }
2167     elsif ( $ordering =~ /author/ ) {
2168         my %result;
2169         foreach ( split (/;/, $biblionumbers )) {
2170             my ( $biblionumber, $title ) = split (/,/, $_);
2171             my $record = GetMarcBiblio($biblionumber);
2172             my $author;
2173             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2174                 $author = $record->subfield( '200', 'f' );
2175                 $author = $record->subfield( '700', 'a' ) unless $author;
2176             }
2177             else {
2178                 $author = $record->subfield( '100', 'a' );
2179             }
2180
2181 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2182 # and we don't want to get only 1 result for each of them !!!
2183             $result{ $author . $biblionumber } = $record;
2184         }
2185
2186     # sort the hash and return the same structure as GetRecords (Zebra querying)
2187         my $result_hash;
2188         my $numbers = 0;
2189         if ( $ordering eq 'author_za' ) {    # sort by author desc
2190             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2191                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2192                   $result{$key}->as_usmarc();
2193             }
2194         }
2195         else {                               # sort by author ASC
2196             foreach my $key ( sort ( keys %result ) ) {
2197                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2198                   $result{$key}->as_usmarc();
2199             }
2200         }
2201         my $finalresult = ();
2202         $result_hash->{'hits'}         = $numbers;
2203         $finalresult->{'biblioserver'} = $result_hash;
2204         return $finalresult;
2205
2206         #
2207         # ORDER BY callnumber
2208         #
2209     }
2210     elsif ( $ordering =~ /callnumber/ ) {
2211         my %result;
2212         foreach ( split (/;/, $biblionumbers )) {
2213             my ( $biblionumber, $title ) = split (/,/, $_);
2214             my $record = GetMarcBiblio($biblionumber);
2215             my $callnumber;
2216             my ( $callnumber_tag, $callnumber_subfield ) =
2217               GetMarcFromKohaField( 'items.itemcallnumber','' );
2218             ( $callnumber_tag, $callnumber_subfield ) =
2219               GetMarcFromKohaField('biblioitems.callnumber','')
2220               unless $callnumber_tag;
2221             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2222                 $callnumber = $record->subfield( '200', 'f' );
2223             }
2224             else {
2225                 $callnumber = $record->subfield( '100', 'a' );
2226             }
2227
2228 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2229 # and we don't want to get only 1 result for each of them !!!
2230             $result{ $callnumber . $biblionumber } = $record;
2231         }
2232
2233     # sort the hash and return the same structure as GetRecords (Zebra querying)
2234         my $result_hash;
2235         my $numbers = 0;
2236         if ( $ordering eq 'call_number_dsc' ) {    # sort by title desc
2237             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2238                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2239                   $result{$key}->as_usmarc();
2240             }
2241         }
2242         else {                                     # sort by title ASC
2243             foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
2244                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2245                   $result{$key}->as_usmarc();
2246             }
2247         }
2248         my $finalresult = ();
2249         $result_hash->{'hits'}         = $numbers;
2250         $finalresult->{'biblioserver'} = $result_hash;
2251         return $finalresult;
2252     }
2253     elsif ( $ordering =~ /pubdate/ ) {             #pub year
2254         my %result;
2255         foreach ( split (/;/, $biblionumbers )) {
2256             my ( $biblionumber, $title ) = split (/,/, $_);
2257             my $record = GetMarcBiblio($biblionumber);
2258             my ( $publicationyear_tag, $publicationyear_subfield ) =
2259               GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
2260             my $publicationyear =
2261               $record->subfield( $publicationyear_tag,
2262                 $publicationyear_subfield );
2263
2264 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2265 # and we don't want to get only 1 result for each of them !!!
2266             $result{ $publicationyear . $biblionumber } = $record;
2267         }
2268
2269     # sort the hash and return the same structure as GetRecords (Zebra querying)
2270         my $result_hash;
2271         my $numbers = 0;
2272         if ( $ordering eq 'pubdate_dsc' ) {    # sort by pubyear desc
2273             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2274                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2275                   $result{$key}->as_usmarc();
2276             }
2277         }
2278         else {                                 # sort by pub year ASC
2279             foreach my $key ( sort ( keys %result ) ) {
2280                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2281                   $result{$key}->as_usmarc();
2282             }
2283         }
2284         my $finalresult = ();
2285         $result_hash->{'hits'}         = $numbers;
2286         $finalresult->{'biblioserver'} = $result_hash;
2287         return $finalresult;
2288
2289         #
2290         # ORDER BY title
2291         #
2292     }
2293     elsif ( $ordering =~ /title/ ) {
2294
2295 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
2296         my %result;
2297         foreach ( split (/;/, $biblionumbers )) {
2298             my ( $biblionumber, $title ) = split (/,/, $_);
2299
2300 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2301 # and we don't want to get only 1 result for each of them !!!
2302 # hint & speed improvement : we can order without reading the record
2303 # so order, and read records only for the requested page !
2304             $result{ $title . $biblionumber } = $biblionumber;
2305         }
2306
2307     # sort the hash and return the same structure as GetRecords (Zebra querying)
2308         my $result_hash;
2309         my $numbers = 0;
2310         if ( $ordering eq 'title_az' ) {    # sort by title desc
2311             foreach my $key ( sort ( keys %result ) ) {
2312                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2313             }
2314         }
2315         else {                              # sort by title ASC
2316             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2317                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2318             }
2319         }
2320
2321         # limit the $results_per_page to result size if it's more
2322         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2323
2324         # for the requested page, replace biblionumber by the complete record
2325         # speed improvement : avoid reading too much things
2326         for (
2327             my $counter = $offset ;
2328             $counter <= $offset + $results_per_page ;
2329             $counter++
2330           )
2331         {
2332             $result_hash->{'RECORDS'}[$counter] =
2333               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
2334         }
2335         my $finalresult = ();
2336         $result_hash->{'hits'}         = $numbers;
2337         $finalresult->{'biblioserver'} = $result_hash;
2338         return $finalresult;
2339     }
2340     else {
2341
2342 #
2343 # order by ranking
2344 #
2345 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
2346         my %result;
2347         my %count_ranking;
2348         foreach ( split /;/, $biblionumbers ) {
2349             my ( $biblionumber, $title ) = split /,/, $_;
2350             $title =~ /(.*)-(\d)/;
2351
2352             # get weight
2353             my $ranking = $2;
2354
2355 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
2356 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
2357 # biblio N has ranking = 6
2358             $count_ranking{$biblionumber} += $ranking;
2359         }
2360
2361 # build the result by "inverting" the count_ranking hash
2362 # hing : as usual, we don't order by ranking only, to avoid having only 1 result for each rank. We build an hash on concat(ranking,biblionumber) instead
2363 #         warn "counting";
2364         foreach ( keys %count_ranking ) {
2365             $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
2366         }
2367
2368     # sort the hash and return the same structure as GetRecords (Zebra querying)
2369         my $result_hash;
2370         my $numbers = 0;
2371         foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2372             $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2373         }
2374
2375         # limit the $results_per_page to result size if it's more
2376         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2377
2378         # for the requested page, replace biblionumber by the complete record
2379         # speed improvement : avoid reading too much things
2380         for (
2381             my $counter = $offset ;
2382             $counter <= $offset + $results_per_page ;
2383             $counter++
2384           )
2385         {
2386             $result_hash->{'RECORDS'}[$counter] =
2387               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2388               if $result_hash->{'RECORDS'}[$counter];
2389         }
2390         my $finalresult = ();
2391         $result_hash->{'hits'}         = $numbers;
2392         $finalresult->{'biblioserver'} = $result_hash;
2393         return $finalresult;
2394     }
2395 }
2396
2397 =head2 enabled_staff_search_views
2398
2399 %hash = enabled_staff_search_views()
2400
2401 This function returns a hash that contains three flags obtained from the system
2402 preferences, used to determine whether a particular staff search results view
2403 is enabled.
2404
2405 =over 2
2406
2407 =item C<Output arg:>
2408
2409     * $hash{can_view_MARC} is true only if the MARC view is enabled
2410     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2411     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2412
2413 =item C<usage in the script:>
2414
2415 =back
2416
2417 $template->param ( C4::Search::enabled_staff_search_views );
2418
2419 =cut
2420
2421 sub enabled_staff_search_views
2422 {
2423         return (
2424                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2425                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2426                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2427         );
2428 }
2429
2430 =head2 enabled_opac_search_views
2431
2432 %hash = enabled_opac_search_views()
2433
2434 This function returns a hash that contains two flags obtained from the system
2435 preferences, used to determine whether a particular opac search results view
2436 is enabled.
2437
2438 =over 2
2439
2440 =item C<Output arg:>
2441
2442     * $hash{can_view_MARC} is true only if the MARC view is enabled
2443     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2444
2445 =item C<usage in the script:>
2446
2447 =back
2448
2449 $template->param ( C4::Search::enabled_opac_search_views );
2450
2451 =cut
2452
2453 sub enabled_opac_search_views
2454 {
2455         return (
2456                 can_opac_view_MARC              => C4::Context->preference('OPACviewMARC'),             # 1 if the opac search allows the MARC view
2457                 can_opac_view_ISBD              => C4::Context->preference('OPACviewISBD'),             # 1 if the opac search allows the ISBD view
2458         );
2459 }
2460
2461
2462 =head2 z3950_search_args
2463
2464 $arrayref = z3950_search_args($matchpoints)
2465
2466 This function returns an array reference that contains the search parameters to be
2467 passed to the Z39.50 search script (z3950_search.pl). The array elements
2468 are hash refs whose keys are name, value and encvalue, and whose values are the
2469 name of a search parameter, the value of that search parameter and the URL encoded
2470 value of that parameter.
2471
2472 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2473
2474 The search parameter values are obtained from the bibliographic record whose
2475 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2476
2477 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2478 a general purpose search argument. In this case, the returned array contains only
2479 entry: the key is 'title' and the value and encvalue are derived from $matchpoints.
2480
2481 If a search parameter value is undefined or empty, it is not included in the returned
2482 array.
2483
2484 The returned array reference may be passed directly to the template parameters.
2485
2486 =over 2
2487
2488 =item C<Output arg:>
2489
2490     * $array containing hash refs as described above
2491
2492 =item C<usage in the script:>
2493
2494 =back
2495
2496 $data = Biblio::GetBiblioData($bibno);
2497 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2498
2499 *OR*
2500
2501 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2502
2503 =cut
2504
2505 sub z3950_search_args {
2506     my $bibrec = shift;
2507     $bibrec = { title => $bibrec } if !ref $bibrec;
2508     my $array = [];
2509     for my $field (qw/ lccn isbn issn title author dewey subject /)
2510     {
2511         my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2512         push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2513     }
2514     return $array;
2515 }
2516
2517
2518 END { }    # module clean-up code here (global destructor)
2519
2520 1;
2521 __END__
2522
2523 =head1 AUTHOR
2524
2525 Koha Developement team <info@koha.org>
2526
2527 =cut