added autocomplete="off"
[koha.git] / C4 / Search.pm
index 24ed772..e86bb26 100644 (file)
@@ -30,7 +30,11 @@ use C4::XSLT;
 use C4::Branch;
 use C4::Reserves;    # CheckReserves
 use C4::Debug;
+use C4::Items;
+use C4::Charset;
+use YAML;
 use URI::Escape;
+use Business::ISBN;
 
 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
 
@@ -121,18 +125,19 @@ sub FindDuplicate {
         }
     }
 
-    # FIXME: add error handling
-    my ( $error, $searchresults ) = SimpleSearch($query); # FIXME :: hardcoded !
+    my ( $error, $searchresults, undef ) = SimpleSearch($query); # FIXME :: hardcoded !
     my @results;
-    foreach my $possible_duplicate_record (@$searchresults) {
-        my $marcrecord =
-          MARC::Record->new_from_usmarc($possible_duplicate_record);
-        my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
-
-        # FIXME :: why 2 $biblionumber ?
-        if ($result) {
-            push @results, $result->{'biblionumber'};
-            push @results, $result->{'title'};
+    if (!defined $error) {
+        foreach my $possible_duplicate_record (@{$searchresults}) {
+            my $marcrecord =
+            MARC::Record->new_from_usmarc($possible_duplicate_record);
+            my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
+
+            # FIXME :: why 2 $biblionumber ?
+            if ($result) {
+                push @results, $result->{'biblionumber'};
+                push @results, $result->{'title'};
+            }
         }
     }
     return @results;
@@ -154,12 +159,16 @@ This function provides a simple search API on the bibliographic catalog
     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
 
 
-=item C<Output:>
+=item C<Return:>
 
-    * $error is a empty unless an error is detected
-    * \@results is an array of records.
+    Returns an array consisting of three elements
+    * $error is undefined unless an error is detected
+    * $results is a reference to an array of records.
     * $total_hits is the number of hits that would have been returned with no limit
 
+    If an error is returned the two other return elements are undefined. If error itself is undefined
+    the other two elements are always defined
+
 =item C<usage in the script:>
 
 =back
@@ -173,23 +182,23 @@ if (defined $error) {
     exit;
 }
 
-my $hits = scalar @$marcresults;
+my $hits = @{$marcresults};
 my @results;
 
-for my $i (0..$hits) {
-    my %resultsloop;
-    my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
-    my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
-
-    #build the hash for the template.
-    $resultsloop{title}           = $biblio->{'title'};
-    $resultsloop{subtitle}        = $biblio->{'subtitle'};
-    $resultsloop{biblionumber}    = $biblio->{'biblionumber'};
-    $resultsloop{author}          = $biblio->{'author'};
-    $resultsloop{publishercode}   = $biblio->{'publishercode'};
-    $resultsloop{publicationyear} = $biblio->{'publicationyear'};
+for my $r ( @{$marcresults} ) {
+    my $marcrecord = MARC::File::USMARC::decode($r);
+    my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,q{});
+
+    #build the iarray of hashs for the template.
+    push @results, {
+        title           => $biblio->{'title'},
+        subtitle        => $biblio->{'subtitle'},
+        biblionumber    => $biblio->{'biblionumber'},
+        author          => $biblio->{'author'},
+        publishercode   => $biblio->{'publishercode'},
+        publicationyear => $biblio->{'publicationyear'},
+        };
 
-    push @results, \%resultsloop;
 }
 
 $template->param(result=>\@results);
@@ -207,14 +216,14 @@ sub SimpleSearch {
         return ( undef, $search_result, scalar($result->{hits}) );
     }
     else {
+        return ( 'No query entered', undef, undef ) unless $query;
         # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
-        my @servers = defined ( $servers ) ? @$servers : ( "biblioserver" );
-        my @results;
+        my @servers = defined ( $servers ) ? @$servers : ( 'biblioserver' );
         my @zoom_queries;
         my @tmpresults;
         my @zconns;
-        my $total_hits;
-        return ( "No query entered", undef, undef ) unless $query;
+        my $results = [];
+        my $total_hits = 0;
 
         # Initialize & Search Zebra
         for ( my $i = 0 ; $i < @servers ; $i++ ) {
@@ -258,7 +267,7 @@ sub SimpleSearch {
 
                 for my $j ( $first_record..$last_record ) {
                     my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
-                    push @results, $record;
+                    push @{$results}, $record;
                 }
             }
         }
@@ -270,7 +279,7 @@ sub SimpleSearch {
             $zoom_query->destroy();
         }
 
-        return ( undef, \@results, $total_hits );
+        return ( undef, $results, $total_hits );
     }
 }
 
@@ -352,10 +361,10 @@ sub getRecords {
         # Note: sort will override rank
         my $sort_by;
         foreach my $sort (@sort_by) {
-            if ( $sort eq "author_az" ) {
+            if ( $sort eq "author_az" || $sort eq "author_asc" ) {
                 $sort_by .= "1=1003 <i ";
             }
-            elsif ( $sort eq "author_za" ) {
+            elsif ( $sort eq "author_za" || $sort eq "author_dsc" ) {
                 $sort_by .= "1=1003 >i ";
             }
             elsif ( $sort eq "popularity_asc" ) {
@@ -382,10 +391,10 @@ sub getRecords {
             elsif ( $sort eq "acqdate_dsc" ) {
                 $sort_by .= "1=32 >i ";
             }
-            elsif ( $sort eq "title_az" ) {
+            elsif ( $sort eq "title_az" || $sort eq "title_asc" ) {
                 $sort_by .= "1=4 <i ";
             }
-            elsif ( $sort eq "title_za" ) {
+            elsif ( $sort eq "title_za" || $sort eq "title_dsc" ) {
                 $sort_by .= "1=4 >i ";
             }
             else {
@@ -835,6 +844,7 @@ sub getIndexes{
                     'Authority-Number',
                     'authtype',
                     'bc',
+                   'Bib-level',
                     'biblionumber',
                     'bio',
                     'biography',
@@ -865,6 +875,7 @@ sub getIndexes{
                     'Date-of-acquisition',
                     'Date-of-publication',
                     'Dewey-classification',
+                    'EAN',
                     'extent',
                     'fic',
                     'fiction',
@@ -902,6 +913,7 @@ sub getIndexes{
                     'mc-rtype',
                     'mus',
                     'name',
+                    'Music-number',
                     'Name-geographic',
                     'Name-geographic-heading',
                     'Name-geographic-see',
@@ -943,6 +955,7 @@ sub getIndexes{
                     'su-to',
                     'su-ut',
                     'ut',
+                    'UPC',
                     'Term-genre-form',
                     'Term-genre-form-heading',
                     'Term-genre-form-see',
@@ -951,6 +964,7 @@ sub getIndexes{
                     'Title',
                     'Title-cover',
                     'Title-series',
+                    'Title-host',
                     'Title-uniform',
                     'Title-uniform-heading',
                     'Title-uniform-see',
@@ -1060,15 +1074,14 @@ sub buildQuery {
 
     my $stopwords_removed;    # flag to determine if stopwords have been removed
 
-    my $cclq;
+    my $cclq       = 0;
     my $cclindexes = getIndexes();
-    if( $query !~ /\s*ccl=/ ){
-        for my $index (@$cclindexes){
-            if($query =~ /($index)(,?\w)*[:=]/){
-                $cclq = 1;
-            }
+    if ( $query !~ /\s*ccl=/ ) {
+        while ( !$cclq && $query =~ /(?:^|\W)([\w-]+)(,[\w-]+)*[:=]/g ) {
+            my $dx = lc($1);
+            $cclq = grep { lc($_) eq $dx } @$cclindexes;
         }
-        $query = "ccl=$query" if($cclq);
+        $query = "ccl=$query" if $cclq;
     }
 
 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
@@ -1148,7 +1161,6 @@ sub buildQuery {
                 }
                 # ISBN,ISSN,Standard Number, don't need special treatment
                 elsif ( $index eq 'nb' || $index eq 'ns' ) {
-                    $indexes_set++;
                     (
                         $stemming,      $auto_truncation,
                         $weight_fields, $fuzzy_enabled,
@@ -1163,7 +1175,7 @@ sub buildQuery {
 
                 # Set default structure attribute (word list)
                 my $struct_attr = q{};
-                unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl)/ ) {
+                unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl|nb|ns)/ ) {
                     $struct_attr = ",wrdl";
                 }
 
@@ -1355,7 +1367,7 @@ sub buildQuery {
     # This is flawed , means we can't search anything with : in it
     # if user wants to do ccl or cql, start the query with that
 #    $query =~ s/:/=/g;
-    $query =~ s/(?<=(ti|au|pb|su|an|kw|mc)):/=/g;
+    $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
     $query =~ s/(?<=(wrdl)):/=/g;
     $query =~ s/(?<=(trn|phr)):/=/g;
     $limit =~ s/:/=/g;
@@ -1403,11 +1415,16 @@ Format results in a form suitable for passing to the template
 # IMO this subroutine is pretty messy still -- it's responsible for
 # building the HTML output for the template
 sub searchResults {
-    my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, @marcresults, $hidelostitems ) = @_;
+    my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, $marcresults ) = @_;
     my $dbh = C4::Context->dbh;
     my @newresults;
 
-    $search_context = 'opac' unless $search_context eq 'opac' or $search_context eq 'intranet';
+    $search_context = 'opac' if !$search_context || $search_context ne 'intranet';
+    my ($is_opac, $hidelostitems);
+    if ($search_context eq 'opac') {
+        $hidelostitems = C4::Context->preference('hidelostitems');
+        $is_opac       = 1;
+    }
 
     #Build branchnames hash
     #find branchname
@@ -1470,12 +1487,11 @@ sub searchResults {
        my $marcflavour = C4::Context->preference("marcflavour");
     # We get the biblionumber position in MARC
     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
-    my $fw;
 
     # loop through all of the records we've retrieved
     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
-        my $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
-        $fw = $scan
+        my $marcrecord = MARC::File::USMARC::decode( $marcresults->[$i] );
+        my $fw = $scan
              ? undef
              : $bibliotag < 10
                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
@@ -1549,6 +1565,33 @@ sub searchResults {
 
         # Pull out the items fields
         my @fields = $marcrecord->field($itemtag);
+        my $marcflavor = C4::Context->preference("marcflavour");
+        # adding linked items that belong to host records
+        my $analyticsfield = '773';
+        if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
+            $analyticsfield = '773';
+        } elsif ($marcflavor eq 'UNIMARC') {
+            $analyticsfield = '461';
+        }
+        foreach my $hostfield ( $marcrecord->field($analyticsfield)) {
+            my $hostbiblionumber = $hostfield->subfield("0");
+            my $linkeditemnumber = $hostfield->subfield("9");
+            if(!$hostbiblionumber eq undef){
+                my $hostbiblio = GetMarcBiblio($hostbiblionumber, 1);
+                my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber', GetFrameworkCode($hostbiblionumber) );
+                if(!$hostbiblio eq undef){
+                    my @hostitems = $hostbiblio->field($itemfield);
+                    foreach my $hostitem (@hostitems){
+                        if ($hostitem->subfield("9") eq $linkeditemnumber){
+                            my $linkeditem =$hostitem;
+                            # append linked items if they exist
+                            if (!$linkeditem eq undef){
+                                push (@fields, $linkeditem);}
+                        }
+                    }
+                }
+            }
+        }
 
         # Setting item statuses for display
         my @available_items_loop;
@@ -1566,18 +1609,18 @@ sub searchResults {
         my $other_count           = 0;
         my $wthdrawn_count        = 0;
         my $itemlost_count        = 0;
+        my $hideatopac_count      = 0;
         my $itembinding_count     = 0;
         my $itemdamaged_count     = 0;
         my $item_in_transit_count = 0;
         my $can_place_holds       = 0;
-       my $item_onhold_count     = 0;
+        my $item_onhold_count     = 0;
         my $items_count           = scalar(@fields);
-        my $maxitems =
-          ( C4::Context->preference('maxItemsinSearchResults') )
-          ? C4::Context->preference('maxItemsinSearchResults') - 1
-          : 1;
+        my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
+        my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
 
         # loop through every item
+             my @hiddenitems;
         foreach my $field (@fields) {
             my $item;
 
@@ -1586,8 +1629,16 @@ sub searchResults {
                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
             }
 
-                       my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
-                       my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
+               # Hidden items
+            if ($is_opac) {
+                   my @hi = GetHiddenItemnumbers($item);
+               $item->{'hideatopac'} = @hi;
+              push @hiddenitems, @hi;
+            }
+
+            my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
+            my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
+
             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
             if ($item->{$hbranch}) {
                 $item->{'branchname'} = $branches{$item->{$hbranch}};
@@ -1630,7 +1681,7 @@ sub searchResults {
                 my ($transfertfrom, $transfertto);
 
                 # is item on the reserve shelf?
-               my $reservestatus = 0;
+               my $reservestatus = '';
                my $reserveitem;
 
                 unless ($item->{wthdrawn}
@@ -1652,30 +1703,41 @@ sub searchResults {
                     #        should map transit status to record indexed in Zebra.
                     #
                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
-                   ($reservestatus, $reserveitem) = C4::Reserves::CheckReserves($item->{itemnumber});
+                   ($reservestatus, $reserveitem, undef) = C4::Reserves::CheckReserves($item->{itemnumber});
                 }
 
-                # item is withdrawn, lost or damaged
+                # item is withdrawn, lost, damaged, not for loan, reserved or in transit
                 if (   $item->{wthdrawn}
                     || $item->{itemlost}
                     || $item->{damaged}
                     || $item->{notforloan} > 0
+                    || $item->{hideatopac}
                    || $reservestatus eq 'Waiting'
                     || ($transfertwhen ne ''))
                 {
                     $wthdrawn_count++        if $item->{wthdrawn};
                     $itemlost_count++        if $item->{itemlost};
                     $itemdamaged_count++     if $item->{damaged};
+                    $hideatopac_count++      if $item->{hideatopac};
                     $item_in_transit_count++ if $transfertwhen ne '';
                    $item_onhold_count++     if $reservestatus eq 'Waiting';
                     $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
+
+                    # can place hold on item ?
+                    if ((!$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems'))
+                      && !$item->{itemlost}
+                      && !$item->{withdrawn}
+                    ) {
+                        $can_place_holds = 1;
+                    }
+                    
                     $other_count++;
 
-                                       my $key = $prefix . $item->{status};
-                                       foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
-                       $other_items->{$key}->{$_} = $item->{$_};
-                                       }
-                    $other_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
+                    my $key = $prefix . $item->{status};
+                    foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber hideatopac)) {
+                        $other_items->{$key}->{$_} = $item->{$_};
+                    }
+                    $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
                     $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
                                        $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
                                        $other_items->{$key}->{count}++ if $item->{$hbranch};
@@ -1687,7 +1749,7 @@ sub searchResults {
                     $can_place_holds = 1;
                     $available_count++;
                                        $available_items->{$prefix}->{count}++ if $item->{$hbranch};
-                                       foreach (qw(branchname itemcallnumber)) {
+                                       foreach (qw(branchname itemcallnumber hideatopac)) {
                        $available_items->{$prefix}->{$_} = $item->{$_};
                                        }
                                        $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
@@ -1695,11 +1757,11 @@ sub searchResults {
                 }
             }
         }    # notforloan, item level and biblioitem level
+
+        next if $is_opac       && $hideatopac_count >= $items_count;
+        next if $hidelostitems && $itemlost_count   >= $items_count;
+
         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
-        $maxitems =
-          ( C4::Context->preference('maxItemsinSearchResults') )
-          ? C4::Context->preference('maxItemsinSearchResults') - 1
-          : 1;
         for my $key ( sort keys %$onloan_items ) {
             (++$onloanitemscount > $maxitems) and last;
             push @onloan_items_loop, $onloan_items->{$key};
@@ -1713,20 +1775,7 @@ sub searchResults {
             push @available_items_loop, $available_items->{$key}
         }
 
-        # XSLT processing of some stuff
-       use C4::Charset;
-       SetUTF8Flag($marcrecord);
-       $debug && warn $marcrecord->as_formatted;
-        if (!$scan && $search_context eq 'opac' && C4::Context->preference("OPACXSLTResultsDisplay")) {
-            # FIXME note that XSLTResultsDisplay (use of XSLT to format staff interface bib search results)
-            # is not implemented yet
-            $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, 'Results', 
-                                                                $search_context, 1);
-                # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
-
-        }
-
-        # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
+         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
         if (!C4::Context->preference("item-level_itypes")) {
             if ($itemtypes{ $oldbiblio->{itemtype} }->{notforloan}) {
                 $can_place_holds = 0;
@@ -1750,8 +1799,8 @@ sub searchResults {
         $oldbiblio->{intransitcount}       = $item_in_transit_count;
         $oldbiblio->{onholdcount}          = $item_onhold_count;
         $oldbiblio->{orderedcount}         = $ordered_count;
-        $oldbiblio->{isbn} =~
-          s/-//g;    # deleting - in isbn to enable amazon content
+        # deleting - in isbn to enable amazon content
+        $oldbiblio->{isbn} =~ s/-//g;
 
         if (C4::Context->preference("AlternateHoldingsField") && $items_count == 0) {
             my $fieldspec = C4::Context->preference("AlternateHoldingsField");
@@ -1781,10 +1830,24 @@ sub searchResults {
             $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
         }
 
-        push( @newresults, $oldbiblio )
-            if(not $hidelostitems
-               or (($items_count > $itemlost_count )
-                    && $hidelostitems));
+       # XSLT processing of some stuff
+        if (!$scan && $search_context eq 'opac' && C4::Context->preference("OPACXSLTResultsDisplay")) {
+            SetUTF8Flag($marcrecord);
+            $debug && warn $marcrecord->as_formatted;
+            # FIXME note that XSLTResultsDisplay (use of XSLT to format staff interface bib search results)
+            # is not implemented yet
+            $oldbiblio->{XSLTResultsRecord}
+              = XSLTParse4Display($oldbiblio->{biblionumber},
+                                  $marcrecord,
+                                  'Results',
+                                  $search_context,
+                                  1, # clean up the problematic ampersand entities that Zebra outputs
+                                  \@hiddenitems
+                                );
+
+        }
+
+        push( @newresults, $oldbiblio );
     }
 
     return @newresults;
@@ -2269,7 +2332,7 @@ sub NZorder {
     # sort the hash and return the same structure as GetRecords (Zebra querying)
         my $result_hash;
         my $numbers = 0;
-        if ( $ordering eq 'author_za' ) {    # sort by author desc
+        if ( $ordering eq 'author_za' || $ordering eq 'author_dsc' ) {    # sort by author desc
             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
                 $result_hash->{'RECORDS'}[ $numbers++ ] =
                   $result{$key}->as_usmarc();
@@ -2575,7 +2638,15 @@ $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
 
 sub z3950_search_args {
     my $bibrec = shift;
-    $bibrec = { title => $bibrec } if !ref $bibrec;
+    my $isbn = Business::ISBN->new($bibrec);
+
+    if (defined $isbn && $isbn->is_valid)
+    {
+        $bibrec = { isbn => $bibrec } if !ref $bibrec;
+    }
+    else {
+        $bibrec = { title => $bibrec } if !ref $bibrec;
+    }
     my $array = [];
     for my $field (qw/ lccn isbn issn title author dewey subject /)
     {
@@ -2642,11 +2713,11 @@ AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
         warn "BIBLIOADDSAUTHORITIES: $error";
             return (0,0) ;
           }
-      if ($results && scalar(@$results)==1) {
+      if ( @{$results} == 1 ) {
         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
         $field->add_subfields('9'=>$marcrecord->field('001')->data);
         $countlinked++;
-      } elsif (scalar(@$results)>1) {
+      } elsif ( @{$results} > 1 ) {
    #More than One result
    #This can comes out of a lack of a subfield.
 #         my $marcrecord = MARC::File::USMARC::decode($results->[0]);