Bugzilla – Attachment 7085 Details for
Bug 7284
Authority matching algorithm improvements
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 7284: Improvements to authority linker script
Bug-7284-Improvements-to-authority-linker-script.patch (text/plain), 89.13 KB, created by
Jared Camins-Esakov
on 2012-01-07 22:33:23 UTC
(
hide
)
Description:
Bug 7284: Improvements to authority linker script
Filename:
MIME Type:
Creator:
Jared Camins-Esakov
Created:
2012-01-07 22:33:23 UTC
Size:
89.13 KB
patch
obsolete
>From 6dd563383c624e8fa3b8bcd185a37546428bd97a Mon Sep 17 00:00:00 2001 >From: Jared Camins-Esakov <jcamins@cpbibliography.com> >Date: Fri, 30 Dec 2011 21:38:57 -0500 >Subject: [PATCH] Bug 7284: Improvements to authority linker script >Content-Type: text/plain; charset="UTF-8" > >This commit improves Koha's authority linker cron job >(misc/link_bibs_to_authorities.pl) to make it more useful: > >Added the following options to the misc/link_bibs_to_authorities.pl script: >--auth-limit Only process those headings that match the authorities > matching the user-specified WHERE clause. >--bib-limit Only process those bib records that match the > user-specified WHERE clause. >--commit Commit the results to the database after every N records > are processed. >--link-report Display a report of all the headings that were processed. > >Converted misc/link_bibs_to_authorities.pl to use POD. > >Added a detailed report of headings that linked, did not link, and linked >in a "fuzzy" fashion (the exact semantics of fuzzy are up to the individual >linker modules) during the run. > >Implemented new C4::Linker functionality to make it possible to easily add >custom authority linker algorithms. Currently available linker options are: >* Default: retains the current behavior of only creating links when there is > an exact match to one and only one authority record >* First Match: based on Default, creates a link to the *first* authority > record that matches a given heading, even if there is more than one > authority record that matches >* Last Match: based on Default, creates a link to the *last* authority > record that matches a given heading, even if there is more than one record > that matches > >Made the linking functionality use the SearchAuthorities in C4::AuthoritiesMarc >rather than SimpleSearch in C4::Search. Once C4::Search has been refactored, >SearchAuthorities should be rewritten to simply call into C4::Search. However, >at this time C4::Search cannot handle authority searching. Also fixed numerous >performance issues in SearchAuthorities and the Linker script: >* Correctly destroy ZOOM recordsets in SearchAuthorities when finished. If left > undestroyed, efficiency appears to approach O(log n^n) >* Add an optional $skipmetadata flag to SearchAuthorities that can be used to > avoid additional calls into Zebra when all that is wanted are authority > records and not statistics about their use > >This patch also adds the following sysprefs: >* AutoCreateAuthorities - When this and BiblioAddsAuthorities are both turned > on, automatically create authority records for headings that don't have > any authority link when cataloging. When BiblioAddsAuthorities is on and > AutoCreateAuthorities is turned off, do not automatically generate authority > records, but allow the user to enter headings that don't match an existing > authority. When BiblioAddsAuthorities is off, this has no effect. >* LinkerModule - Chooses which linker module to use for matching headings > (current options are as described above in the section on linker options: > "Default," "FirstMatch," and "LastMatch") >* LinkerRelink - When turned on, the linker will confirm the links for headings > that have previously been linked to an authority record when it runs. When > turned off, any heading with an existing link will be ignored. >* LinkerKeepStale - When turned on, the linker will never *delete* a link to an > authority record, though, depending on the value of LinkerRelink, it may > change the link. > >This patch also modifies the authority indexing to remove trailing punctuation >from Match indexes. >--- > C4/AuthoritiesMarc.pm | 85 ++-- > C4/Biblio.pm | 23 +- > C4/Heading.pm | 80 ++-- > C4/Heading/MARC21.pm | 21 +- > C4/Linker.pm | 110 +++++ > C4/Linker/Default.pm | 73 +++ > C4/Linker/FirstMatch.pm | 58 +++ > C4/Linker/LastMatch.pm | 58 +++ > .../authorities/authority-koha-indexdefs.xml | 32 +- > .../authorities/authority-zebra-indexdefs.xsl | 505 ++++++++++++++++++-- > .../marc21/authorities/koha-indexdefs-to-zebra.xsl | 21 +- > .../atomicupdate/bug_7284_authority_linking_pt1 | 9 + > installer/data/mysql/sysprefs.sql | 5 +- > .../en/modules/admin/preferences/authorities.pref | 81 ++-- > misc/link_bibs_to_authorities.pl | 221 +++++++-- > t/db_dependent/Linker_FirstMatch.t | 46 ++ > 16 files changed, 1232 insertions(+), 196 deletions(-) > create mode 100644 C4/Linker.pm > create mode 100644 C4/Linker/Default.pm > create mode 100644 C4/Linker/FirstMatch.pm > create mode 100644 C4/Linker/LastMatch.pm > create mode 100644 installer/data/mysql/atomicupdate/bug_7284_authority_linking_pt1 > create mode 100755 t/db_dependent/Linker_FirstMatch.t > >diff --git a/C4/AuthoritiesMarc.pm b/C4/AuthoritiesMarc.pm >index 49c20b6..c341b02 100644 >--- a/C4/AuthoritiesMarc.pm >+++ b/C4/AuthoritiesMarc.pm >@@ -96,15 +96,16 @@ sub GetAuthMARCFromKohaField { > =head2 SearchAuthorities > > (\@finalresult, $nbresults)= &SearchAuthorities($tags, $and_or, >- $excluding, $operator, $value, $offset,$length,$authtypecode,$sortby) >+ $excluding, $operator, $value, $offset,$length,$authtypecode, >+ $sortby[, $skipmetadata]) > > returns ref to array result and count of results returned > > =cut > > sub SearchAuthorities { >- my ($tags, $and_or, $excluding, $operator, $value, $offset,$length,$authtypecode,$sortby) = @_; >-# warn "CALL : $tags, $and_or, $excluding, $operator, $value, $offset,$length,$authtypecode,$sortby"; >+ my ($tags, $and_or, $excluding, $operator, $value, $offset,$length,$authtypecode,$sortby,$skipmetadata) = @_; >+ # warn Dumper($tags, $and_or, $excluding, $operator, $value, $offset,$length,$authtypecode,$sortby); > my $dbh=C4::Context->dbh; > if (C4::Context->preference('NoZebra')) { > >@@ -224,22 +225,28 @@ sub SearchAuthorities { > { > if (@$value[$i]){ > if (@$tags[$i] eq "mainmainentry") { >- >- $attr =" \@attr 1=Heading-Main "; >- >- }elsif (@$tags[$i] eq "mainentry") { >- $attr =" \@attr 1=Heading "; >- }elsif (@$tags[$i] eq "any") { >- $attr =" \@attr 1=Any "; >- }else { >- $attr =" \@attr 1=Match "; >+ $attr =" \@attr 1=Heading-Main "; >+ } elsif (@$tags[$i] eq "mainentry") { >+ $attr =" \@attr 1=Heading "; >+ } elsif (@$tags[$i] eq "any") { >+ $attr =" \@attr 1=Any "; >+ } elsif (@$tags[$i] eq "match") { >+ $attr =" \@attr 1=Match "; >+ } elsif (@$tags[$i] eq "match-heading") { >+ $attr =" \@attr 1=Match-heading "; >+ } elsif (@$tags[$i] eq "see-from") { >+ $attr =" \@attr 1=Match-heading-see-from "; >+ } elsif (@$tags[$i] eq "thesaurus") { >+ $attr =" \@attr 1=Subject-heading-thesaurus "; > } > if (@$operator[$i] eq 'is') { > $attr.=" \@attr 4=1 \@attr 5=100 ";##Phrase, No truncation,all of subfield field must match >- }elsif (@$operator[$i] eq "="){ >+ } elsif (@$operator[$i] eq "="){ > $attr.=" \@attr 4=107 "; #Number Exact match >- }elsif (@$operator[$i] eq "start"){ >+ } elsif (@$operator[$i] eq "start"){ > $attr.=" \@attr 3=2 \@attr 4=1 \@attr 5=1 ";#Firstinfield Phrase, Right truncated >+ } elsif (@$operator[$i] eq "exact"){ >+ $attr.=" \@attr 4=1 \@attr 5=100 \@attr 6=3 ";##Phrase, No truncation,all of subfield field must match > } else { > $attr .=" \@attr 5=1 \@attr 4=6 ";## Word list, right truncated, anywhere > } >@@ -252,7 +259,7 @@ sub SearchAuthorities { > } > ##Add how many queries generated > if (defined $query && $query=~/\S+/){ >- $query= $and x $attr_cnt . $query . $q2; >+ $query= $and x $attr_cnt . $query . (defined $q2 ? $q2 : ''); > } else { > $query= $q2; > } >@@ -309,35 +316,39 @@ sub SearchAuthorities { > my $separator=C4::Context->preference('authoritysep'); > $authrecord = MARC::File::USMARC::decode($marcdata); > my $authid=$authrecord->field('001')->data(); >- my $summary=BuildSummary($authrecord,$authid,$authtypecode); >- my $query_auth_tag = "SELECT auth_tag_to_report FROM auth_types WHERE authtypecode=?"; >- my $sth = $dbh->prepare($query_auth_tag); >- $sth->execute($authtypecode); >- my $auth_tag_to_report = $sth->fetchrow; >- my $reported_tag; >- my $mainentry = $authrecord->field($auth_tag_to_report); >- if ($mainentry) { >- foreach ($mainentry->subfields()) { >- $reported_tag .='$'.$_->[0].$_->[1]; >- } >- } > my %newline; >- $newline{summary} = $summary; > $newline{authid} = $authid; >- $newline{even} = $counter % 2; >- $newline{reported_tag} = $reported_tag; >+ if (! $skipmetadata) { >+ my $summary=BuildSummary($authrecord,$authid,$authtypecode); >+ my $query_auth_tag = "SELECT auth_tag_to_report FROM auth_types WHERE authtypecode=?"; >+ my $sth = $dbh->prepare($query_auth_tag); >+ $sth->execute($authtypecode); >+ my $auth_tag_to_report = $sth->fetchrow; >+ my $reported_tag; >+ my $mainentry = $authrecord->field($auth_tag_to_report); >+ if ($mainentry) { >+ foreach ($mainentry->subfields()) { >+ $reported_tag .='$'.$_->[0].$_->[1]; >+ } >+ } >+ $newline{summary} = $summary; >+ $newline{even} = $counter % 2; >+ $newline{reported_tag} = $reported_tag; >+ } > $counter++; > push @finalresult, \%newline; > }## while counter >- ### >- for (my $z=0; $z<@finalresult; $z++){ >- my $count=CountUsage($finalresult[$z]{authid}); >- $finalresult[$z]{used}=$count; >- }# all $z's >- >+ ### >+ if (! $skipmetadata) { >+ for (my $z=0; $z<@finalresult; $z++){ >+ my $count=CountUsage($finalresult[$z]{authid}); >+ $finalresult[$z]{used}=$count; >+ }# all $z's >+ } >+ > }## if nbresult > NOLUCK: >- # $oAResult->destroy(); >+ $oAResult->destroy(); > # $oAuth[0]->destroy(); > > return (\@finalresult, $nbresults); >diff --git a/C4/Biblio.pm b/C4/Biblio.pm >index ac78ae3..7015e0a 100644 >--- a/C4/Biblio.pm >+++ b/C4/Biblio.pm >@@ -35,6 +35,7 @@ use C4::Dates qw/format_date/; > use C4::Log; # logaction > use C4::ClassSource; > use C4::Charset; >+use C4::Linker; > require C4::Heading; > require C4::Serials; > require C4::Items; >@@ -477,7 +478,9 @@ MARC record. > =cut > > sub LinkBibHeadingsToAuthorities { >+ my $linker = shift; > my $bib = shift; >+ my %results; > > my $num_headings_changed = 0; > foreach my $field ( $bib->fields() ) { >@@ -487,27 +490,31 @@ sub LinkBibHeadingsToAuthorities { > # check existing $9 > my $current_link = $field->subfield('9'); > >- # look for matching authorities >- my $authorities = $heading->authorities(); >+ if (defined $current_link && !C4::Context->preference('LinkerRelink')) { >+ $results{'linked'}->{$heading->display_form()}++; >+ next; >+ } > >- # want only one exact match >- if ( $#{$authorities} == 0 ) { >- my $authority = MARC::Record->new_from_usmarc( $authorities->[0] ); >- my $authid = $authority->field('001')->data(); >+ my ($authid, $fuzzy) = $linker->get_link($heading); >+ if ($authid) { >+ $results{$fuzzy ? 'fuzzy' : 'linked'}->{$heading->display_form()}++; > next if defined $current_link and $current_link eq $authid; > > $field->delete_subfield( code => '9' ) if defined $current_link; > $field->add_subfields( '9', $authid ); > $num_headings_changed++; > } else { >- if ( defined $current_link ) { >+ if ( defined $current_link && C4::Context->preference('LinkerKeepStale')) { >+ $results{'fuzzy'}->{$heading->display_form()}++; >+ } else { > $field->delete_subfield( code => '9' ); > $num_headings_changed++; >+ $results{'unlinked'}->{$heading->display_form()}++; > } > } > > } >- return $num_headings_changed; >+ return $num_headings_changed, \%results; > } > > =head2 GetRecordValue >diff --git a/C4/Heading.pm b/C4/Heading.pm >index ff7a198..f9f3156 100644 >--- a/C4/Heading.pm >+++ b/C4/Heading.pm >@@ -23,7 +23,7 @@ use MARC::Record; > use MARC::Field; > use C4::Context; > use C4::Heading::MARC21; >-use C4::Search; >+use C4::AuthoritiesMarc; > use Carp; > > our $VERSION = 3.00; >@@ -75,14 +75,28 @@ sub new_from_bib_field { > return unless $marc_handler->valid_bib_heading_tag($tag); > my $self = {}; > >- ($self->{'auth_type'}, $self->{'subject_added_entry'}, $self->{'series_added_entry'}, $self->{'main_entry'}, >- $self->{'thesaurus'}, $self->{'search_form'}, $self->{'display_form'}) = >+ $self->{'field'} = $field; >+ ($self->{'auth_type'}, $self->{'thesaurus'}, $self->{'search_form'}, >+ $self->{'display_form'}, $self->{'match_type'}) = > $marc_handler->parse_heading($field); > > bless $self, $class; > return $self; > } > >+=head2 field >+ >+ my $field = $heading->field(); >+ >+Return the MARC::Field the heading is based on. >+ >+=cut >+ >+sub field { >+ my $self = shift; >+ return $self->{'field'}; >+} >+ > =head2 display_form > > my $display = $heading->display_form(); >@@ -111,21 +125,18 @@ sub search_form { > > =head2 authorities > >- my $authorities = $heading->authorities; >+ my $authorities = $heading->authorities([$skipmetadata]); > > Return a list of authority records for this >-heading. >+heading. If passed a true value for $skipmetadata, >+SearchAuthorities will return only authids. > > =cut > > sub authorities { > my $self = shift; >- my $query = qq(Match-heading,do-not-truncate,ext="$self->{'search_form'}"); >- $query .= $self->_query_limiters(); >- my ($error, $results, $total_hits) = SimpleSearch( $query, undef, undef, [ "authorityserver" ] ); >- if (defined $error) { >- carp "Error:$error from search $query"; >- } >+ my $skipmetadata = shift; >+ my ( $results, $total ) = _search($self, 'match-heading', $skipmetadata); > return $results; > } > >@@ -140,36 +151,43 @@ that are a preferred form of the heading. > > sub preferred_authorities { > my $self = shift; >- my $query = "Match-heading-see-from,do-not-truncate,ext='$self->{'search_form'}'"; >- $query .= $self->_query_limiters(); >- my ($error, $results, $total_hits) = SimpleSearch( $query, undef, undef, [ "authorityserver" ] ); >- if (defined $error) { >- carp "Error:$error from search $query"; >- } >+ my $skipmetadata = shift || undef; >+ my ( $results, $total ) = _search('see-from', $skipmetadata); > return $results; > } > > =head1 INTERNAL METHODS > >-=head2 _query_limiters >+=head2 _search > > =cut > >-sub _query_limiters { >+sub _search { > my $self = shift; >- >- my $limiters = " AND at='$self->{'auth_type'}'"; >- if ($self->{'subject_added_entry'}) { >- $limiters .= " AND Heading-use-subject-added-entry=a"; # FIXME -- is this properly in C4::Heading::MARC21? >- $limiters .= " AND Subject-heading-thesaurus=$self->{'thesaurus'}"; >+ my $index = shift || undef; >+ my $skipmetadata = shift || undef; >+ my @marclist; >+ my @and_or; >+ my @excluding = []; >+ my @operator; >+ my @value; >+ >+ if ($index) { >+ push @marclist, $index; >+ push @and_or, 'and'; >+ push @operator, $self->{'match_type'}; >+ push @value, $self->{'search_form'}; > } >- if ($self->{'series_added_entry'}) { >- $limiters .= " AND Heading-use-series-added-entry=a"; # FIXME -- is this properly in C4::Heading::MARC21? >- } >- if (not $self->{'subject_added_entry'} and not $self->{'series_added_entry'}) { >- $limiters .= " AND Heading-use-main-or-added-entry=a" # FIXME -- is this properly in C4::Heading::MARC21? >- } >- return $limiters; >+ >+# if ($self->{'thesaurus'}) { >+# push @marclist, 'thesaurus'; >+# push @and_or, 'and'; >+# push @excluding, ''; >+# push @operator, 'is'; >+# push @value, $self->{'thesaurus'}; >+# } >+ return SearchAuthorities(\@marclist, \@and_or, \@excluding, \@operator, \@value, >+ 0, 20, $self->{'auth_type'}, '', $skipmetadata); > } > > =head1 INTERNAL FUNCTIONS >diff --git a/C4/Heading/MARC21.pm b/C4/Heading/MARC21.pm >index 77cdac2..6e54698 100644 >--- a/C4/Heading/MARC21.pm >+++ b/C4/Heading/MARC21.pm >@@ -18,7 +18,7 @@ package C4::Heading::MARC21; > # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. > > use strict; >-#use warnings; FIXME - Bug 2505 >+use warnings; > use MARC::Record; > use MARC::Field; > >@@ -124,14 +124,11 @@ sub parse_heading { > my $field_info = $bib_heading_fields->{$tag}; > > my $auth_type = $field_info->{'auth_type'}; >- my $subject = $field_info->{'subject'} ? 1 : 0; >- my $series = $field_info->{'series'} ? 1 : 0; >- my $main_entry = $field_info->{'main_entry'} ? 1 : 0; >- my $thesaurus = $subject ? _get_subject_thesaurus($field) : "lcsh"; # use 'lcsh' for names, UT, etc. >+ my $thesaurus = $tag =~ m/6../ ? _get_subject_thesaurus($field) : "lcsh"; # use 'lcsh' for names, UT, etc. > my $search_heading = _get_search_heading($field, $field_info->{'subfields'}); > my $display_heading = _get_display_heading($field, $field_info->{'subfields'}); > >- return ($auth_type, $subject, $series, $main_entry, $thesaurus, $search_heading, $display_heading); >+ return ($auth_type, $thesaurus, $search_heading, $display_heading, 'exact'); > } > > =head1 INTERNAL FUNCTIONS >@@ -182,6 +179,7 @@ sub _get_search_heading { > my $code = $subfields[$i]->[0]; > my $code_re = quotemeta $code; > my $value = $subfields[$i]->[1]; >+ $value =~ s/[-,.:=;!%\/]$//; > next unless $subfields =~ qr/$code_re/; > if ($first) { > $first = 0; >@@ -231,6 +229,17 @@ sub _get_display_heading { > return $heading; > } > >+# Additional limiters that we aren't using: >+# if ($self->{'subject_added_entry'}) { >+# $limiters .= " AND Heading-use-subject-added-entry=a"; >+# } >+# if ($self->{'series_added_entry'}) { >+# $limiters .= " AND Heading-use-series-added-entry=a"; >+# } >+# if (not $self->{'subject_added_entry'} and not $self->{'series_added_entry'}) { >+# $limiters .= " AND Heading-use-main-or-added-entry=a" >+# } >+ > =head1 AUTHOR > > Koha Development Team <http://koha-community.org/> >diff --git a/C4/Linker.pm b/C4/Linker.pm >new file mode 100644 >index 0000000..8766a9a >--- /dev/null >+++ b/C4/Linker.pm >@@ -0,0 +1,110 @@ >+package C4::Linker; >+ >+# Copyright 2011 C & P Bibliography Services >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 2 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+=head1 NAME >+ >+C4::Linker - Base class for linking authorities to bibliographic records >+ >+=head1 SYNOPSIS >+ >+ use C4::Linker (%params ); >+ >+=head1 DESCRIPTION >+ >+Base class for C4::Linker::X. Subclasses need to provide the following methods >+ >+B<get_link ($field)> - return the authid for the authority that should be >+linked to the provided MARC::Field object, and a boolean to indicate whether >+the match is "fuzzy" (the semantics of "fuzzy" are up to the individual plugin). >+In order to handle authority limits, get_link should always end with: >+ return $self->SUPER::_handle_auth_limit($authid), $fuzzy; >+ >+B<flip_heading ($field)> - return a MARC::Field object with the heading flipped >+to the preferred form. >+ >+=head1 FUNCTIONS >+ >+=cut >+ >+use strict; >+use warnings; >+use Carp; >+use C4::Context; >+ >+use base qw(Class::Accessor); >+ >+__PACKAGE__->mk_accessors( qw( ) ); >+ >+sub new { >+ my $class = shift; >+ my $param = shift; >+ >+ my $self = { }; >+ >+ if (defined $param->{'auth_limit'} && $param->{'auth_limit'}) { >+ my $dbh = C4::Context->dbh; >+ my $sql = "SELECT authid FROM auth_header WHERE $param->{'auth_limit'} ORDER BY authid ASC"; >+ my $sth = $dbh->prepare($sql); >+ $sth->execute(); >+ while (my ($authid) = $sth->fetchrow_array()) { >+ push @{$self->{'auths_to_link'}}, $authid; >+ } >+ } >+ >+ bless $self, $class; >+ return $self; >+} >+ >+=head2 _handle_auth_limit >+ >+ return $self->SUPER::_handle_auth_limit($authid), $fuzzy; >+ >+=back >+ >+Function to be called by subclasses to handle authority record limits. >+ >+=cut >+ >+sub _handle_auth_limit { >+ my $self = shift; >+ my $authid = shift; >+ >+ if (defined $self->{'auths_to_link'} && !grep {$_ eq $authid} @{$self->{'auths_to_link'}}) { >+ undef $authid; >+ } >+ return $authid; >+} >+ >+=head2 EXPORT >+ >+None by default. >+ >+=head1 SEE ALSO >+ >+C4::Linker::Default >+ >+=head1 AUTHOR >+ >+Jared Camins-Esakov, C & P Bibliography Services, E<lt>jcamins@cpbibliography.comE<gt> >+ >+=cut >+ >+1; >+ >+__END__ >diff --git a/C4/Linker/Default.pm b/C4/Linker/Default.pm >new file mode 100644 >index 0000000..f39e477 >--- /dev/null >+++ b/C4/Linker/Default.pm >@@ -0,0 +1,73 @@ >+package C4::Linker::Default; >+ >+# Copyright 2011 C & P Bibliography Services >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 2 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use strict; >+use warnings; >+use Carp; >+use C4::Heading; >+ >+use base qw(C4::Linker); >+ >+sub get_link { >+ my $self = shift; >+ my $heading = shift; >+ my $behavior = shift || 'default'; >+ my $search_form = $heading->search_form(); >+ my $authid; >+ my $fuzzy = 0; >+ >+ if ($self->{'cache'}->{$search_form}->{'cached'}) { >+ $authid = $self->{'cache'}->{$search_form}->{'authid'}; >+ $fuzzy = $self->{'cache'}->{$search_form}->{'fuzzy'}; >+ } else { >+ # look for matching authorities >+ my $authorities = $heading->authorities(1); # $skipmetadata = true >+ >+ if ( $behavior eq 'default' && $#{$authorities} == 0 ) { >+ $authid = $authorities->[0]->{'authid'}; >+ } elsif ($behavior eq 'first' && $#{$authorities} >= 0 ) { >+ $authid = $authorities->[0]->{'authid'}; >+ $fuzzy = $#{$authorities} > 0; >+ } elsif ($behavior eq 'last' && $#{$authorities} >= 0 ) { >+ $authid = $authorities->[$#{$authorities}]->{'authid'}; >+ $fuzzy = $#{$authorities} > 0; >+ } >+ >+ $self->{'cache'}->{$search_form}->{'cached'} = 1; >+ $self->{'cache'}->{$search_form}->{'authid'} = $authid; >+ $self->{'cache'}->{$search_form}->{'fuzzy'} = $fuzzy; >+ } >+ return $self->SUPER::_handle_auth_limit($authid), $fuzzy; >+} >+ >+sub flip_heading { >+ my $self = shift; >+ my $heading = shift; >+ >+ # TODO: implement >+} >+ >+1; >+__END__ >+ >+=head1 NAME >+ >+C4::Linker::Default - match only if there is a single matching auth >+ >+=cut >diff --git a/C4/Linker/FirstMatch.pm b/C4/Linker/FirstMatch.pm >new file mode 100644 >index 0000000..88a7915 >--- /dev/null >+++ b/C4/Linker/FirstMatch.pm >@@ -0,0 +1,58 @@ >+package C4::Linker::FirstMatch; >+ >+# Copyright 2011 C & P Bibliography Services >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 2 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use strict; >+use warnings; >+use Carp; >+use C4::Heading; >+use C4::Linker::Default; # Use Default for flipping >+ >+use base qw(C4::Linker); >+ >+sub new { >+ my $class = shift; >+ my $param = shift; >+ >+ my $self = $class->SUPER::new($param); >+ $self->{'default_linker'} = C4::Linker::Default->new($param); >+ bless $self, $class; >+ return $self; >+} >+ >+sub get_link { >+ my $self = shift; >+ my $heading = shift; >+ return $self->{'default_linker'}->get_link($heading, 'first'); >+} >+ >+sub flip_heading { >+ my $self = shift; >+ my $heading = shift; >+ >+ return $self->{'default_linker'}->flip($heading); >+} >+ >+1; >+__END__ >+ >+=head1 NAME >+ >+C4::Linker::FirstMatch - match against the first authority record >+ >+=cut >diff --git a/C4/Linker/LastMatch.pm b/C4/Linker/LastMatch.pm >new file mode 100644 >index 0000000..9c92ba1 >--- /dev/null >+++ b/C4/Linker/LastMatch.pm >@@ -0,0 +1,58 @@ >+package C4::Linker::LastMatch; >+ >+# Copyright 2011 C & P Bibliography Services >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 2 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use strict; >+use warnings; >+use Carp; >+use C4::Heading; >+use C4::Linker::Default; # Use Default for flipping >+ >+use base qw(C4::Linker); >+ >+sub new { >+ my $class = shift; >+ my $param = shift; >+ >+ my $self = $class->SUPER::new($param); >+ $self->{'default_linker'} = C4::Linker::Default->new($param); >+ bless $self, $class; >+ return $self; >+} >+ >+sub get_link { >+ my $self = shift; >+ my $heading = shift; >+ return $self->{'default_linker'}->get_link($heading, 'last'); >+} >+ >+sub flip_heading { >+ my $self = shift; >+ my $heading = shift; >+ >+ return $self->{'default_linker'}->flip($heading); >+} >+ >+1; >+__END__ >+ >+=head1 NAME >+ >+C4::Linker::LastMatch - match against the last authority record >+ >+=cut >diff --git a/etc/zebradb/marc_defs/marc21/authorities/authority-koha-indexdefs.xml b/etc/zebradb/marc_defs/marc21/authorities/authority-koha-indexdefs.xml >index 42cd3a7..67b234f 100644 >--- a/etc/zebradb/marc_defs/marc21/authorities/authority-koha-indexdefs.xml >+++ b/etc/zebradb/marc_defs/marc21/authorities/authority-koha-indexdefs.xml >@@ -93,9 +93,11 @@ authority-zebra-indexdefs.xsl` > <kohaidx:target_index>See-also-from:w</kohaidx:target_index> > <kohaidx:target_index>See-also-from:p</kohaidx:target_index> > <kohaidx:target_index>See-also-from:s</kohaidx:target_index> >+ </kohaidx:index_heading> >+ <kohaidx:index_match_heading tag="500" subfields="abcdefghjklmnopqrstvxyz" subdivisions="vxyz"> > <kohaidx:target_index>Match:w</kohaidx:target_index> > <kohaidx:target_index>Match:p</kohaidx:target_index> >- </kohaidx:index_heading> >+ </kohaidx:index_match_heading> > > <!-- Corporate name headings --> > <kohaidx:index_subfields tag="110" subfields="abcdefghklmnoprstvxyz"> >@@ -144,9 +146,11 @@ authority-zebra-indexdefs.xsl` > <kohaidx:target_index>See-also-from:w</kohaidx:target_index> > <kohaidx:target_index>See-also-from:p</kohaidx:target_index> > <kohaidx:target_index>See-also-from:s</kohaidx:target_index> >+ </kohaidx:index_heading> >+ <kohaidx:index_match_heading tag="510" subfields="abcdefghklmnoprstvxyz" subdivisions="vxyz"> > <kohaidx:target_index>Match:w</kohaidx:target_index> > <kohaidx:target_index>Match:p</kohaidx:target_index> >- </kohaidx:index_heading> >+ </kohaidx:index_match_heading> > > <!-- Meeting name --> > <kohaidx:index_subfields tag="111" subfields="acdefghjklnpqstvxyz"> >@@ -195,9 +199,11 @@ authority-zebra-indexdefs.xsl` > <kohaidx:target_index>See-also-from:w</kohaidx:target_index> > <kohaidx:target_index>See-also-from:p</kohaidx:target_index> > <kohaidx:target_index>See-also-from:s</kohaidx:target_index> >+ </kohaidx:index_heading> >+ <kohaidx:index_match_heading tag="511" subfields="acdefghjklnpqstvxyz" subdivisions="vxyz"> > <kohaidx:target_index>Match:w</kohaidx:target_index> > <kohaidx:target_index>Match:p</kohaidx:target_index> >- </kohaidx:index_heading> >+ </kohaidx:index_match_heading> > > <!-- Uniform title --> > <kohaidx:index_subfields tag="130" subfields="adfghklmnoprstvxyz"> >@@ -246,9 +252,11 @@ authority-zebra-indexdefs.xsl` > <kohaidx:target_index>See-also-from:w</kohaidx:target_index> > <kohaidx:target_index>See-also-from:p</kohaidx:target_index> > <kohaidx:target_index>See-also-from:s</kohaidx:target_index> >+ </kohaidx:index_heading> >+ <kohaidx:index_match_heading tag="530" subfields="adfghklmnoprstvxyz" subdivisions="vxyz"> > <kohaidx:target_index>Match:w</kohaidx:target_index> > <kohaidx:target_index>Match:p</kohaidx:target_index> >- </kohaidx:index_heading> >+ </kohaidx:index_match_heading> > > <!-- Chronological term --> > <kohaidx:index_subfields tag="148" subfields="avxyz"> >@@ -297,9 +305,11 @@ authority-zebra-indexdefs.xsl` > <kohaidx:target_index>See-also-from:w</kohaidx:target_index> > <kohaidx:target_index>See-also-from:p</kohaidx:target_index> > <kohaidx:target_index>See-also-from:s</kohaidx:target_index> >+ </kohaidx:index_heading> >+ <kohaidx:index_match_heading tag="548" subfields="avxyz" subdivisions="vxyz"> > <kohaidx:target_index>Match:w</kohaidx:target_index> > <kohaidx:target_index>Match:p</kohaidx:target_index> >- </kohaidx:index_heading> >+ </kohaidx:index_match_heading> > > > <!-- Topical term --> >@@ -349,9 +359,11 @@ authority-zebra-indexdefs.xsl` > <kohaidx:target_index>See-also-from:w</kohaidx:target_index> > <kohaidx:target_index>See-also-from:p</kohaidx:target_index> > <kohaidx:target_index>See-also-from:s</kohaidx:target_index> >+ </kohaidx:index_heading> >+ <kohaidx:index_match_heading tag="550" subfields="abvxyz" subdivisions="vxyz"> > <kohaidx:target_index>Match:w</kohaidx:target_index> > <kohaidx:target_index>Match:p</kohaidx:target_index> >- </kohaidx:index_heading> >+ </kohaidx:index_match_heading> > > <!-- Geographic name --> > <kohaidx:index_subfields tag="151" subfields="avxyz"> >@@ -400,9 +412,11 @@ authority-zebra-indexdefs.xsl` > <kohaidx:target_index>See-also-from:w</kohaidx:target_index> > <kohaidx:target_index>See-also-from:p</kohaidx:target_index> > <kohaidx:target_index>See-also-from:s</kohaidx:target_index> >+ </kohaidx:index_heading> >+ <kohaidx:index_match_heading tag="551" subfields="avxyz" subdivisions="vxyz"> > <kohaidx:target_index>Match:w</kohaidx:target_index> > <kohaidx:target_index>Match:p</kohaidx:target_index> >- </kohaidx:index_heading> >+ </kohaidx:index_match_heading> > > <!-- Genre/form term --> > <kohaidx:index_subfields tag="155" subfields="avxyz"> >@@ -451,9 +465,11 @@ authority-zebra-indexdefs.xsl` > <kohaidx:target_index>See-also-from:w</kohaidx:target_index> > <kohaidx:target_index>See-also-from:p</kohaidx:target_index> > <kohaidx:target_index>See-also-from:s</kohaidx:target_index> >+ </kohaidx:index_heading> >+ <kohaidx:index_match_heading tag="555" subfields="avxyz" subdivisions="vxyz"> > <kohaidx:target_index>Match:w</kohaidx:target_index> > <kohaidx:target_index>Match:p</kohaidx:target_index> >- </kohaidx:index_heading> >+ </kohaidx:index_match_heading> > > <!-- General subdivision --> > <kohaidx:index_heading tag="180" subfields="vxyz" subdivisions="vxyz"> >diff --git a/etc/zebradb/marc_defs/marc21/authorities/authority-zebra-indexdefs.xsl b/etc/zebradb/marc_defs/marc21/authorities/authority-zebra-indexdefs.xsl >index 079cb51..3ee5d6c 100644 >--- a/etc/zebradb/marc_defs/marc21/authorities/authority-zebra-indexdefs.xsl >+++ b/etc/zebradb/marc_defs/marc21/authorities/authority-zebra-indexdefs.xsl >@@ -213,10 +213,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </z:index> > </xslo:template> > <xslo:template mode="index_heading" match="marc:datafield[@tag='500']"> >- <z:index name="Personal-name-see-also-from:w Personal-name-see-also-from:p Personal-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <z:index name="Personal-name-see-also-from:w Personal-name-see-also-from:p Personal-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:variable name="raw_heading"> > <xslo:for-each select="marc:subfield"> >- <xslo:if test="contains('abcdefghjklmnopqrstvxyz', @code)" name="Personal-name-see-also-from:w Personal-name-see-also-from:p Personal-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <xslo:if test="contains('abcdefghjklmnopqrstvxyz', @code)" name="Personal-name-see-also-from:w Personal-name-see-also-from:p Personal-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:if test="position() > 1"> > <xslo:choose> > <xslo:when test="contains('vxyz', @code)"> >@@ -299,10 +299,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </z:index> > </xslo:template> > <xslo:template mode="index_heading" match="marc:datafield[@tag='510']"> >- <z:index name="Corporate-name-see-also-from:w Corporate-name-see-also-from:p Corporate-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <z:index name="Corporate-name-see-also-from:w Corporate-name-see-also-from:p Corporate-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:variable name="raw_heading"> > <xslo:for-each select="marc:subfield"> >- <xslo:if test="contains('abcdefghklmnoprstvxyz', @code)" name="Corporate-name-see-also-from:w Corporate-name-see-also-from:p Corporate-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <xslo:if test="contains('abcdefghklmnoprstvxyz', @code)" name="Corporate-name-see-also-from:w Corporate-name-see-also-from:p Corporate-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:if test="position() > 1"> > <xslo:choose> > <xslo:when test="contains('vxyz', @code)"> >@@ -385,10 +385,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </z:index> > </xslo:template> > <xslo:template mode="index_heading" match="marc:datafield[@tag='511']"> >- <z:index name="Meeting-name-see-also-from:w Meeting-name-see-also-from:p Meeting-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <z:index name="Meeting-name-see-also-from:w Meeting-name-see-also-from:p Meeting-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:variable name="raw_heading"> > <xslo:for-each select="marc:subfield"> >- <xslo:if test="contains('acdefghjklnpqstvxyz', @code)" name="Meeting-name-see-also-from:w Meeting-name-see-also-from:p Meeting-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <xslo:if test="contains('acdefghjklnpqstvxyz', @code)" name="Meeting-name-see-also-from:w Meeting-name-see-also-from:p Meeting-name-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:if test="position() > 1"> > <xslo:choose> > <xslo:when test="contains('vxyz', @code)"> >@@ -471,10 +471,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </z:index> > </xslo:template> > <xslo:template mode="index_heading" match="marc:datafield[@tag='530']"> >- <z:index name="Title-uniform-see-also-from:w Title-uniform-see-also-from:p Title-uniform-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <z:index name="Title-uniform-see-also-from:w Title-uniform-see-also-from:p Title-uniform-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:variable name="raw_heading"> > <xslo:for-each select="marc:subfield"> >- <xslo:if test="contains('adfghklmnoprstvxyz', @code)" name="Title-uniform-see-also-from:w Title-uniform-see-also-from:p Title-uniform-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <xslo:if test="contains('adfghklmnoprstvxyz', @code)" name="Title-uniform-see-also-from:w Title-uniform-see-also-from:p Title-uniform-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:if test="position() > 1"> > <xslo:choose> > <xslo:when test="contains('vxyz', @code)"> >@@ -557,10 +557,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </z:index> > </xslo:template> > <xslo:template mode="index_heading" match="marc:datafield[@tag='548']"> >- <z:index name="Chronological-term-see-also-from:w Chronological-term-see-also-from:p Chronological-term-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <z:index name="Chronological-term-see-also-from:w Chronological-term-see-also-from:p Chronological-term-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:variable name="raw_heading"> > <xslo:for-each select="marc:subfield"> >- <xslo:if test="contains('avxyz', @code)" name="Chronological-term-see-also-from:w Chronological-term-see-also-from:p Chronological-term-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <xslo:if test="contains('avxyz', @code)" name="Chronological-term-see-also-from:w Chronological-term-see-also-from:p Chronological-term-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:if test="position() > 1"> > <xslo:choose> > <xslo:when test="contains('vxyz', @code)"> >@@ -643,10 +643,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </z:index> > </xslo:template> > <xslo:template mode="index_heading" match="marc:datafield[@tag='550']"> >- <z:index name="Subject-topical-see-also-from:w Subject-topical-see-also-from:p Subject-topical-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <z:index name="Subject-topical-see-also-from:w Subject-topical-see-also-from:p Subject-topical-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:variable name="raw_heading"> > <xslo:for-each select="marc:subfield"> >- <xslo:if test="contains('abvxyz', @code)" name="Subject-topical-see-also-from:w Subject-topical-see-also-from:p Subject-topical-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <xslo:if test="contains('abvxyz', @code)" name="Subject-topical-see-also-from:w Subject-topical-see-also-from:p Subject-topical-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:if test="position() > 1"> > <xslo:choose> > <xslo:when test="contains('vxyz', @code)"> >@@ -729,10 +729,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </z:index> > </xslo:template> > <xslo:template mode="index_heading" match="marc:datafield[@tag='551']"> >- <z:index name="Name-geographic-see-also-from:w Name-geographic-see-also-from:p Name-geographic-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <z:index name="Name-geographic-see-also-from:w Name-geographic-see-also-from:p Name-geographic-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:variable name="raw_heading"> > <xslo:for-each select="marc:subfield"> >- <xslo:if test="contains('avxyz', @code)" name="Name-geographic-see-also-from:w Name-geographic-see-also-from:p Name-geographic-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <xslo:if test="contains('avxyz', @code)" name="Name-geographic-see-also-from:w Name-geographic-see-also-from:p Name-geographic-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:if test="position() > 1"> > <xslo:choose> > <xslo:when test="contains('vxyz', @code)"> >@@ -815,10 +815,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </z:index> > </xslo:template> > <xslo:template mode="index_heading" match="marc:datafield[@tag='555']"> >- <z:index name="Term-genre-form-see-also-from:w Term-genre-form-see-also-from:p Term-genre-form-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <z:index name="Term-genre-form-see-also-from:w Term-genre-form-see-also-from:p Term-genre-form-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:variable name="raw_heading"> > <xslo:for-each select="marc:subfield"> >- <xslo:if test="contains('avxyz', @code)" name="Term-genre-form-see-also-from:w Term-genre-form-see-also-from:p Term-genre-form-see-also-from:s See-also-from:w See-also-from:p See-also-from:s Match:w Match:p"> >+ <xslo:if test="contains('avxyz', @code)" name="Term-genre-form-see-also-from:w Term-genre-form-see-also-from:p Term-genre-form-see-also-from:s See-also-from:w See-also-from:p See-also-from:s"> > <xslo:if test="position() > 1"> > <xslo:choose> > <xslo:when test="contains('vxyz', @code)"> >@@ -1128,7 +1128,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1163,7 +1167,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> >+ </xslo:if> >+ </xslo:for-each> >+ </xslo:variable> >+ <xslo:value-of select="normalize-space($raw_heading)"/> >+ </z:index> >+ </xslo:template> >+ <xslo:template mode="index_match_heading" match="marc:datafield[@tag='500']"> >+ <z:index name="Match:w Match:p"> >+ <xslo:variable name="raw_heading"> >+ <xslo:for-each select="marc:subfield"> >+ <xslo:if test="contains('abcdefghjklmnopqrstvxyz', @code)" name="Match:w Match:p"> >+ <xslo:if test="position() > 1"> >+ <xslo:choose> >+ <xslo:when test="contains('vxyz', @code)"> >+ <xslo:choose> >+ <xslo:when test="@code = $general_subdivision_subfield"> >+ <xslo:text> generalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $form_subdivision_subfield"> >+ <xslo:text> formsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $chronological_subdivision_subfield"> >+ <xslo:text> chronologicalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $geographic_subdivision_subfield"> >+ <xslo:text> geographicsubdiv </xslo:text> >+ </xslo:when> >+ </xslo:choose> >+ </xslo:when> >+ <xslo:otherwise> >+ <xslo:value-of select="substring(' ', 1, 1)"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ </xslo:if> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1198,7 +1245,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1233,7 +1284,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> >+ </xslo:if> >+ </xslo:for-each> >+ </xslo:variable> >+ <xslo:value-of select="normalize-space($raw_heading)"/> >+ </z:index> >+ </xslo:template> >+ <xslo:template mode="index_match_heading" match="marc:datafield[@tag='510']"> >+ <z:index name="Match:w Match:p"> >+ <xslo:variable name="raw_heading"> >+ <xslo:for-each select="marc:subfield"> >+ <xslo:if test="contains('abcdefghklmnoprstvxyz', @code)" name="Match:w Match:p"> >+ <xslo:if test="position() > 1"> >+ <xslo:choose> >+ <xslo:when test="contains('vxyz', @code)"> >+ <xslo:choose> >+ <xslo:when test="@code = $general_subdivision_subfield"> >+ <xslo:text> generalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $form_subdivision_subfield"> >+ <xslo:text> formsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $chronological_subdivision_subfield"> >+ <xslo:text> chronologicalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $geographic_subdivision_subfield"> >+ <xslo:text> geographicsubdiv </xslo:text> >+ </xslo:when> >+ </xslo:choose> >+ </xslo:when> >+ <xslo:otherwise> >+ <xslo:value-of select="substring(' ', 1, 1)"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ </xslo:if> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1268,7 +1362,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1303,7 +1401,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> >+ </xslo:if> >+ </xslo:for-each> >+ </xslo:variable> >+ <xslo:value-of select="normalize-space($raw_heading)"/> >+ </z:index> >+ </xslo:template> >+ <xslo:template mode="index_match_heading" match="marc:datafield[@tag='511']"> >+ <z:index name="Match:w Match:p"> >+ <xslo:variable name="raw_heading"> >+ <xslo:for-each select="marc:subfield"> >+ <xslo:if test="contains('acdefghjklnpqstvxyz', @code)" name="Match:w Match:p"> >+ <xslo:if test="position() > 1"> >+ <xslo:choose> >+ <xslo:when test="contains('vxyz', @code)"> >+ <xslo:choose> >+ <xslo:when test="@code = $general_subdivision_subfield"> >+ <xslo:text> generalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $form_subdivision_subfield"> >+ <xslo:text> formsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $chronological_subdivision_subfield"> >+ <xslo:text> chronologicalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $geographic_subdivision_subfield"> >+ <xslo:text> geographicsubdiv </xslo:text> >+ </xslo:when> >+ </xslo:choose> >+ </xslo:when> >+ <xslo:otherwise> >+ <xslo:value-of select="substring(' ', 1, 1)"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ </xslo:if> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1338,7 +1479,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1373,7 +1518,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> >+ </xslo:if> >+ </xslo:for-each> >+ </xslo:variable> >+ <xslo:value-of select="normalize-space($raw_heading)"/> >+ </z:index> >+ </xslo:template> >+ <xslo:template mode="index_match_heading" match="marc:datafield[@tag='530']"> >+ <z:index name="Match:w Match:p"> >+ <xslo:variable name="raw_heading"> >+ <xslo:for-each select="marc:subfield"> >+ <xslo:if test="contains('adfghklmnoprstvxyz', @code)" name="Match:w Match:p"> >+ <xslo:if test="position() > 1"> >+ <xslo:choose> >+ <xslo:when test="contains('vxyz', @code)"> >+ <xslo:choose> >+ <xslo:when test="@code = $general_subdivision_subfield"> >+ <xslo:text> generalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $form_subdivision_subfield"> >+ <xslo:text> formsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $chronological_subdivision_subfield"> >+ <xslo:text> chronologicalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $geographic_subdivision_subfield"> >+ <xslo:text> geographicsubdiv </xslo:text> >+ </xslo:when> >+ </xslo:choose> >+ </xslo:when> >+ <xslo:otherwise> >+ <xslo:value-of select="substring(' ', 1, 1)"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ </xslo:if> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1408,7 +1596,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1443,7 +1635,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> >+ </xslo:if> >+ </xslo:for-each> >+ </xslo:variable> >+ <xslo:value-of select="normalize-space($raw_heading)"/> >+ </z:index> >+ </xslo:template> >+ <xslo:template mode="index_match_heading" match="marc:datafield[@tag='548']"> >+ <z:index name="Match:w Match:p"> >+ <xslo:variable name="raw_heading"> >+ <xslo:for-each select="marc:subfield"> >+ <xslo:if test="contains('avxyz', @code)" name="Match:w Match:p"> >+ <xslo:if test="position() > 1"> >+ <xslo:choose> >+ <xslo:when test="contains('vxyz', @code)"> >+ <xslo:choose> >+ <xslo:when test="@code = $general_subdivision_subfield"> >+ <xslo:text> generalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $form_subdivision_subfield"> >+ <xslo:text> formsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $chronological_subdivision_subfield"> >+ <xslo:text> chronologicalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $geographic_subdivision_subfield"> >+ <xslo:text> geographicsubdiv </xslo:text> >+ </xslo:when> >+ </xslo:choose> >+ </xslo:when> >+ <xslo:otherwise> >+ <xslo:value-of select="substring(' ', 1, 1)"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ </xslo:if> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1478,7 +1713,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1513,7 +1752,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> >+ </xslo:if> >+ </xslo:for-each> >+ </xslo:variable> >+ <xslo:value-of select="normalize-space($raw_heading)"/> >+ </z:index> >+ </xslo:template> >+ <xslo:template mode="index_match_heading" match="marc:datafield[@tag='550']"> >+ <z:index name="Match:w Match:p"> >+ <xslo:variable name="raw_heading"> >+ <xslo:for-each select="marc:subfield"> >+ <xslo:if test="contains('abvxyz', @code)" name="Match:w Match:p"> >+ <xslo:if test="position() > 1"> >+ <xslo:choose> >+ <xslo:when test="contains('vxyz', @code)"> >+ <xslo:choose> >+ <xslo:when test="@code = $general_subdivision_subfield"> >+ <xslo:text> generalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $form_subdivision_subfield"> >+ <xslo:text> formsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $chronological_subdivision_subfield"> >+ <xslo:text> chronologicalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $geographic_subdivision_subfield"> >+ <xslo:text> geographicsubdiv </xslo:text> >+ </xslo:when> >+ </xslo:choose> >+ </xslo:when> >+ <xslo:otherwise> >+ <xslo:value-of select="substring(' ', 1, 1)"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ </xslo:if> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1548,7 +1830,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1583,7 +1869,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> >+ </xslo:if> >+ </xslo:for-each> >+ </xslo:variable> >+ <xslo:value-of select="normalize-space($raw_heading)"/> >+ </z:index> >+ </xslo:template> >+ <xslo:template mode="index_match_heading" match="marc:datafield[@tag='551']"> >+ <z:index name="Match:w Match:p"> >+ <xslo:variable name="raw_heading"> >+ <xslo:for-each select="marc:subfield"> >+ <xslo:if test="contains('avxyz', @code)" name="Match:w Match:p"> >+ <xslo:if test="position() > 1"> >+ <xslo:choose> >+ <xslo:when test="contains('vxyz', @code)"> >+ <xslo:choose> >+ <xslo:when test="@code = $general_subdivision_subfield"> >+ <xslo:text> generalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $form_subdivision_subfield"> >+ <xslo:text> formsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $chronological_subdivision_subfield"> >+ <xslo:text> chronologicalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $geographic_subdivision_subfield"> >+ <xslo:text> geographicsubdiv </xslo:text> >+ </xslo:when> >+ </xslo:choose> >+ </xslo:when> >+ <xslo:otherwise> >+ <xslo:value-of select="substring(' ', 1, 1)"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ </xslo:if> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1618,7 +1947,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1653,7 +1986,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> >+ </xslo:if> >+ </xslo:for-each> >+ </xslo:variable> >+ <xslo:value-of select="normalize-space($raw_heading)"/> >+ </z:index> >+ </xslo:template> >+ <xslo:template mode="index_match_heading" match="marc:datafield[@tag='555']"> >+ <z:index name="Match:w Match:p"> >+ <xslo:variable name="raw_heading"> >+ <xslo:for-each select="marc:subfield"> >+ <xslo:if test="contains('avxyz', @code)" name="Match:w Match:p"> >+ <xslo:if test="position() > 1"> >+ <xslo:choose> >+ <xslo:when test="contains('vxyz', @code)"> >+ <xslo:choose> >+ <xslo:when test="@code = $general_subdivision_subfield"> >+ <xslo:text> generalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $form_subdivision_subfield"> >+ <xslo:text> formsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $chronological_subdivision_subfield"> >+ <xslo:text> chronologicalsubdiv </xslo:text> >+ </xslo:when> >+ <xslo:when test="@code = $geographic_subdivision_subfield"> >+ <xslo:text> geographicsubdiv </xslo:text> >+ </xslo:when> >+ </xslo:choose> >+ </xslo:when> >+ <xslo:otherwise> >+ <xslo:value-of select="substring(' ', 1, 1)"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ </xslo:if> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1688,7 +2064,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1723,7 +2103,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1758,7 +2142,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1793,7 +2181,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1828,7 +2220,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1863,7 +2259,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1898,7 +2298,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1933,7 +2337,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >@@ -1999,4 +2407,21 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > <xslo:value-of select="."/> > </z:index> > </xslo:template> >+ <xslo:template name="chopPunctuation"> >+ <xslo:param name="chopString"/> >+ <xslo:variable name="length" select="string-length($chopString)"/> >+ <xslo:choose> >+ <xslo:when test="$length=0"/> >+ <xslo:when test="contains('-,.:=;!%/', substring($chopString,$length,1))"> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString" select="substring($chopString,1,$length - 1)"/> >+ </xslo:call-template> >+ </xslo:when> >+ <xslo:when test="not($chopString)"/> >+ <xslo:otherwise> >+ <xslo:value-of select="$chopString"/> >+ </xslo:otherwise> >+ </xslo:choose> >+ <xslo:text/> >+ </xslo:template> > </xslo:stylesheet> >diff --git a/etc/zebradb/marc_defs/marc21/authorities/koha-indexdefs-to-zebra.xsl b/etc/zebradb/marc_defs/marc21/authorities/koha-indexdefs-to-zebra.xsl >index 66bf816..bfb79b4 100644 >--- a/etc/zebradb/marc_defs/marc21/authorities/koha-indexdefs-to-zebra.xsl >+++ b/etc/zebradb/marc_defs/marc21/authorities/koha-indexdefs-to-zebra.xsl >@@ -67,6 +67,21 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > <xslo:value-of select="."/> > </z:index> > </xslo:template> >+ <xslo:template name="chopPunctuation"> >+ <xslo:param name="chopString"/> >+ <xslo:variable name="length" select="string-length($chopString)"/> >+ <xslo:choose> >+ <xslo:when test="$length=0"/> >+ <xslo:when test="contains('-,.:=;!%/', substring($chopString,$length,1))"> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString" select="substring($chopString,1,$length - 1)"/> >+ </xslo:call-template> >+ </xslo:when> >+ <xslo:when test="not($chopString)"/> >+ <xslo:otherwise><xslo:value-of select="$chopString"/></xslo:otherwise> >+ </xslo:choose> >+ <xslo:text> </xslo:text> >+ </xslo:template> > </xslo:stylesheet> > </xsl:template> > >@@ -368,7 +383,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). > </xslo:otherwise> > </xslo:choose> > </xslo:if> >- <xslo:value-of select="."/> >+ <xslo:call-template name="chopPunctuation"> >+ <xslo:with-param name="chopString"> >+ <xslo:value-of select="."/> >+ </xslo:with-param> >+ </xslo:call-template> > </xslo:if> > </xslo:for-each> > </xslo:variable> >diff --git a/installer/data/mysql/atomicupdate/bug_7284_authority_linking_pt1 b/installer/data/mysql/atomicupdate/bug_7284_authority_linking_pt1 >new file mode 100644 >index 0000000..ca9a940 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_7284_authority_linking_pt1 >@@ -0,0 +1,9 @@ >+#! /usr/bin/perl >+use strict; >+use warnings; >+use C4::Context; >+my $dbh=C4::Context->dbh; >+$dbh->do("INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('LinkerModule','Default','Chooses which linker module to use (see documentation).','Default|FirstMatchLastMatch','Choice');"); >+$dbh->do("INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('LinkerRelink',1,'If ON the authority linker will relink headings that have previously been linked every time it runs.',NULL,'YesNo');"); >+$dbh->do("INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('LinkerKeepStale',0,'If ON the authority linker will keep existing authority links for headings where it is unable to find a match.',NULL,'YesNo');"); >+print "Upgrade done (Configured bug 7284, improved authority matching)\n"; >diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql >index 11d4ade..49a62e1 100755 >--- a/installer/data/mysql/sysprefs.sql >+++ b/installer/data/mysql/sysprefs.sql >@@ -329,4 +329,7 @@ INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES > INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('OpacKohaUrl','1',"Show 'Powered by Koha' text on OPAC footer.",NULL,NULL); > INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('EasyAnalyticalRecords','0','If on, display in the catalogue screens tools to easily setup analytical record relationships','','YesNo'); > INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacShowRecentComments',0,'If ON a link to recent comments will appear in the OPAC masthead',NULL,'YesNo'); >-INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ('CircAutoPrintQuickSlip', '1', 'Choose what should happen when an empty barcode field is submitted in circulation: Display a print quick slip window or Clear the screen.',NULL,'YesNo'); >\ No newline at end of file >+INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ('CircAutoPrintQuickSlip', '1', 'Choose what should happen when an empty barcode field is submitted in circulation: Display a print quick slip window or Clear the screen.',NULL,'YesNo'); >+INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('LinkerModule','Default','Chooses which linker module to use (see documentation).','Default|FirstMatch|LastMatch','Choice'); >+INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('LinkerRelink',1,'If ON the authority linker will relink headings that have previously been linked every time it runs.',NULL,'YesNo'); >+INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('LinkerKeepStale',0,'If ON the authority linker will keep existing authority links for headings where it is unable to find a match.',NULL,'YesNo'); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/authorities.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/authorities.pref >index 2b05faa..60a6b5d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/authorities.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/authorities.pref >@@ -1,29 +1,54 @@ > Authorities: >- - >- - When editing records, >- - pref: BiblioAddsAuthorities >- default: no >- choices: >- yes: allow >- no: "don't allow" >- - them to automatically create new authority records if needed, rather than having to reference existing authorities. >- - >- - pref: dontmerge >- default: yes >- choices: >- yes: "Don't" >- no: Do >- - automatically update attached biblios when changing an authority record. If this is off, please ask your administrator to enable the merge_authorities.pl cronjob. >- - >- - Use the following text for the contents of MARC authority control field 008 position 06-39 (fixed length data elements). Do NOT include the date (position 00-05). >- - pref: MARCAuthorityControlField008 >- default: "|| aca||aabn | a|a d" >- type: textarea >- class: code >- - >- - pref: UseAuthoritiesForTracings >- default: yes >- choices: >- yes: Use >- no: "Don't use" >- - authority record numbers instead of text strings for searches from subject tracings. >+ General: >+ - >+ - When editing records, >+ - pref: BiblioAddsAuthorities >+ default: no >+ choices: >+ yes: allow >+ no: "don't allow" >+ - them to automatically create new authority records if needed, rather than having to reference existing authorities. >+ - >+ - pref: dontmerge >+ default: yes >+ choices: >+ yes: "Don't" >+ no: Do >+ - automatically update attached biblios when changing an authority record. If this is off, please ask your administrator to enable the merge_authorities.pl cronjob. >+ - >+ - Use the following text for the contents of MARC authority control field 008 position 06-39 (fixed length data elements). Do NOT include the date (position 00-05). >+ - pref: MARCAuthorityControlField008 >+ default: "|| aca||aabn | a|a d" >+ type: textarea >+ class: code >+ - >+ - pref: UseAuthoritiesForTracings >+ default: yes >+ choices: >+ yes: Use >+ no: "Don't use" >+ - authority record numbers instead of text strings for searches from subject tracings. >+ Linker: >+ - >+ - Use the >+ - pref: LinkerModule >+ default: Default >+ choices: >+ Default: Default >+ FirstMatch: "First Match" >+ LastMatch: "Last Match" >+ - linker module for matching headings to authority records. >+ - >+ - pref: LinkerRelink >+ default: yes >+ choices: >+ yes: Do >+ no: "Do not" >+ - relink headings that have previously been linked to authority records. >+ - >+ - pref: LinkerKeepStale >+ default: no >+ choices: >+ yes: Do >+ no: "Do not" >+ - keep existing links to authority records for headings where the linker is unable to find a match. >diff --git a/misc/link_bibs_to_authorities.pl b/misc/link_bibs_to_authorities.pl >index 0fc9c26..691fe39 100755 >--- a/misc/link_bibs_to_authorities.pl >+++ b/misc/link_bibs_to_authorities.pl >@@ -12,44 +12,82 @@ BEGIN { > use C4::Context; > use C4::Biblio; > use Getopt::Long; >+use Pod::Usage; >+use Data::Dumper; >+ >+sub usage { >+ pod2usage( -verbose => 2 ); >+ exit; >+} > > $| = 1; > > # command-line parameters > my $verbose = 0; >+my $link_report = 0; > my $test_only = 0; > my $want_help = 0; >+my $auth_limit; >+my $bib_limit; >+my $commit = 100; > > my $result = GetOptions( >- 'verbose' => \$verbose, >- 'test' => \$test_only, >- 'h|help' => \$want_help >+ 'v|verbose' => \$verbose, >+ 't|test' => \$test_only, >+ 'l|link-report' => \$link_report, >+ 'a|auth-limit=s' => \$auth_limit, >+ 'b|bib-limit=s' => \$bib_limit, >+ 'c|commit=i' => \$commit, >+ 'h|help' => \$want_help > ); > >+binmode(STDOUT, ":utf8"); >+ > if (not $result or $want_help) { >- print_usage(); >- exit 0; >+ usage(); > } > >+my $linker_module = "C4::Linker::" . (C4::Context->preference("LinkerModule") || 'Default'); >+eval { >+ eval "require $linker_module"; >+}; >+if ($@){ >+ $linker_module = 'C4::Linker::Default'; >+ eval "require $linker_module"; >+} >+if ($@) { >+ die "Unable to load linker module. Aborting."; >+} >+ >+my $linker = $linker_module->new( {'auth_limit' => $auth_limit} ); >+ > my $num_bibs_processed = 0; > my $num_bibs_modified = 0; > my $num_bad_bibs = 0; >+my %unlinked_headings; >+my %linked_headings; >+my %fuzzy_headings; > my $dbh = C4::Context->dbh; > $dbh->{AutoCommit} = 0; >-process_bibs(); >+process_bibs($linker, $bib_limit, $auth_limit, $commit); > $dbh->commit(); > > exit 0; > > sub process_bibs { >- my $sql = "SELECT biblionumber FROM biblio ORDER BY biblionumber ASC"; >+ my ($linker, $bib_limit, $auth_limit, $commit) = @_; >+ my $bib_where = ''; >+ if ($bib_limit) { >+ $bib_where = "WHERE $bib_limit"; >+ } >+ my $sql = "SELECT biblionumber FROM biblio $bib_where ORDER BY biblionumber ASC"; > my $sth = $dbh->prepare($sql); > $sth->execute(); > while (my ($biblionumber) = $sth->fetchrow_array()) { > $num_bibs_processed++; >- process_bib($biblionumber); >+ process_bib($linker, $biblionumber); > >- if (not $test_only and ($num_bibs_processed % 100) == 0) { >+ if (not $test_only and ($num_bibs_processed % $commit) == 0) { > print_progress_and_commit($num_bibs_processed); > } > } >@@ -58,17 +96,86 @@ sub process_bibs { > $dbh->commit; > } > >+ my $headings_linked = 0; >+ my $headings_unlinked = 0; >+ my $headings_fuzzy = 0; >+ for (values %linked_headings) { $headings_linked += $_; }; >+ for (values %unlinked_headings) { $headings_unlinked += $_; }; >+ for (values %fuzzy_headings) { $headings_fuzzy += $_; }; >+ > print <<_SUMMARY_; > > Bib authority heading linking report >------------------------------------- >-Number of bibs checked: $num_bibs_processed >-Number of bibs modified: $num_bibs_modified >-Number of bibs with errors: $num_bad_bibs >+======================================================= >+Linker module: $linker_module >+Number of bibs checked: $num_bibs_processed >+Number of bibs modified: $num_bibs_modified >+Number of bibs with errors: $num_bad_bibs >+Number of headings linked: $headings_linked >+Number of headings unlinked: $headings_unlinked >+Number of headings fuzzily linked: $headings_fuzzy > _SUMMARY_ >+ print "**** Ran in test mode only ****\n" if $test_only; >+ >+ if ($link_report) { >+ my @keys; >+ print <<_LINKED_HEADER_; >+ >+Linked headings (from most frequent to least): >+------------------------------------------------------- >+ >+_LINKED_HEADER_ >+ >+ @keys = sort { $linked_headings{$b} <=> $linked_headings{$a} or "\L$a" cmp "\L$b" } keys %linked_headings; >+ foreach my $key (@keys) { >+ print "$key:\t" . $linked_headings{$key} . " occurrences\n" >+ } >+ >+ print <<_UNLINKED_HEADER_; >+ >+Unlinked headings (from most frequent to least): >+------------------------------------------------------- >+ >+_UNLINKED_HEADER_ >+ >+ @keys = sort { $unlinked_headings{$b} <=> $unlinked_headings{$a} or "\L$a" cmp "\L$b" } keys %unlinked_headings; >+ foreach my $key (@keys) { >+ print "$key:\t" . $unlinked_headings{$key} . " occurrences\n" >+ } >+ >+ print <<_FUZZY_HEADER_; >+ >+Fuzzily-matched headings (from most frequent to least): >+------------------------------------------------------- >+ >+_FUZZY_HEADER_ >+ >+ @keys = sort { $fuzzy_headings{$b} <=> $fuzzy_headings{$a} or "\L$a" cmp "\L$b" } keys %fuzzy_headings; >+ foreach my $key (@keys) { >+ print "$key:\t" . $fuzzy_headings{$key} . " occurrences\n" >+ } >+ print <<_SUMMARY2_; >+ >+ >+Printing out summary report again since your link >+report may have been very long... >+ >+Bib authority heading linking report (duplicate) >+======================================================= >+Linker module: $linker_module >+Number of bibs checked: $num_bibs_processed >+Number of bibs modified: $num_bibs_modified >+Number of bibs with errors: $num_bad_bibs >+Number of headings linked: $headings_linked >+Number of headings unlinked: $headings_unlinked >+Number of headings fuzzily linked: $headings_fuzzy >+_SUMMARY2_ >+ print "**** Ran in test mode only ****\n" if $test_only; >+ } > } > > sub process_bib { >+ my $linker = shift; > my $biblionumber = shift; > > my $bib = GetMarcBiblio($biblionumber); >@@ -78,7 +185,16 @@ sub process_bib { > return; > } > >- my $headings_changed = LinkBibHeadingsToAuthorities($bib); >+ my ($headings_changed, $results) = LinkBibHeadingsToAuthorities($linker, $bib); >+ foreach my $key (keys %{$results->{'unlinked'}}) { >+ $unlinked_headings{$key} += $results->{'unlinked'}->{$key}; >+ } >+ foreach my $key (keys %{$results->{'linked'}}) { >+ $linked_headings{$key} += $results->{'linked'}->{$key}; >+ } >+ foreach my $key (keys %{$results->{'fuzzy'}}) { >+ $fuzzy_headings{$key} += $results->{'fuzzy'}->{$key}; >+ } > > if ($headings_changed) { > if ($verbose) { >@@ -86,11 +202,6 @@ sub process_bib { > print "Bib $biblionumber ($title): $headings_changed headings changed\n"; > } > if (not $test_only) { >- # delete any item tags >- my ($itemtag, $itemsubfield) = GetMarcFromKohaField("items.itemnumber", ''); >- foreach my $field ($bib->field($itemtag)) { >- $bib->delete_field($field); >- } > ModBiblio($bib, $biblionumber, GetFrameworkCode($biblionumber)); > $num_bibs_modified++; > } >@@ -103,20 +214,58 @@ sub print_progress_and_commit { > print "... processed $recs records\n"; > } > >-sub print_usage { >- print <<_USAGE_; >-$0: link headings in bib records to authorities. >- >-This batch job checks each bib record in the Koha >-database and attempts to link each of its headings >-to the matching authority record. >- >-Parameters: >- --verbose print the number of headings changed >- for each bib >- --test only test the authority linking >- and report the results; do not >- change the bib records. >- --help or -h show this message. >-_USAGE_ >-} >+=head1 NAME >+ >+link_bibs_to_authorities.pl >+ >+=head1 SYNOPSIS >+ >+ link_bibs_to_authorities.pl >+ link_bibs_to_authorities.pl -v >+ link_bibs_to_authorities.pl -l >+ link_bibs_to_authorities.pl --commit=1000 >+ link_bibs_to_authorities.pl --auth-limit=STRING >+ link_bibs_to_authorities.pl --bib-limit=STRING >+ >+=head1 DESCRIPTION >+ >+This batch job checks each bib record in the Koha database and attempts to link >+each of its headings to the matching authority record. >+ >+=over 8 >+ >+=item B<--help> >+ >+Prints this help >+ >+=item B<-v|--verbose> >+ >+Provide verbose log information (print the number of headings changed for each >+bib record). >+ >+=item B<-l|--link-report> >+ >+Provide a report of all the headings that were processed: which were matched, >+which were not, etc. >+ >+=item B<--auth-limit=S> >+ >+Only process those headings which match an authority record that matches the >+user-specified WHERE clause. >+ >+=item B<--bib-limit=S> >+ >+Only process those bib records that match the user-specified WHERE clause. >+ >+=item B<--commit=N> >+ >+Commit the results to the database after every N records are processed. >+ >+=item B<--test> >+ >+Only test the authority linking and report the results; do not change the bib >+records. >+ >+=back >+ >+=cut >diff --git a/t/db_dependent/Linker_FirstMatch.t b/t/db_dependent/Linker_FirstMatch.t >new file mode 100755 >index 0000000..4a0ed5a >--- /dev/null >+++ b/t/db_dependent/Linker_FirstMatch.t >@@ -0,0 +1,46 @@ >+#!/usr/bin/perl >+# >+# This Koha test module is a stub! >+# Add more tests here!!! >+ >+use strict; >+use warnings; >+use Test::More tests => 3; >+use C4::Context; >+use C4::Heading; >+use MARC::Record; >+use MARC::Field; >+use C4::Linker::FirstMatch; >+ >+ >+BEGIN { >+ use_ok('C4::Linker'); >+} >+my $dbh = C4::Context->dbh; >+ >+my $query = "SELECT authid, marc FROM auth_header LIMIT 1;"; >+my $sth = $dbh->prepare($query); >+$sth->execute(); >+my ($authid, $marc) = $sth->fetchrow_array(); >+SKIP: { >+ skip "No authorities", 2 unless defined $authid; >+ my $linker = C4::Linker::FirstMatch->new(); >+ my $auth = MARC::Record->new_from_usmarc($marc); >+ my $fieldmatch; >+ if (C4::Context->preference('MARCFlavour') eq 'UNIMARC') { >+ $fieldmatch = '2..'; >+ } else { >+ $fieldmatch = '1..'; >+ } >+ my $bibfield = $auth->field($fieldmatch); >+ my $tag = $bibfield->tag(); >+ $tag =~ s/^./6/; >+ $bibfield->update(tag => $tag); >+ my $heading; >+ ok(defined ($heading = C4::Heading->new_from_bib_field($bibfield)), "Creating heading from bib field"); >+ >+ my $authmatch; >+ my $fuzzy; >+ ($authmatch, $fuzzy) = $linker->get_link($heading); >+ is($authmatch, $authid, "Matched existing heading"); >+} >-- >1.7.2.5
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 7284
:
7083
|
7084
|
7085
|
7086
|
7087
|
7091
|
7266
|
7323
|
7457
|
7548
|
7688
|
7714
|
7788
|
7829
|
7938
|
8060
|
8066
|
8137
|
8862
|
8864