From 6dd563383c624e8fa3b8bcd185a37546428bd97a Mon Sep 17 00:00:00 2001 From: Jared Camins-Esakov 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 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 - 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 - 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, Ejcamins@cpbibliography.comE + +=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` See-also-from:w See-also-from:p See-also-from:s + + Match:w Match:p - + @@ -144,9 +146,11 @@ authority-zebra-indexdefs.xsl` See-also-from:w See-also-from:p See-also-from:s + + Match:w Match:p - + @@ -195,9 +199,11 @@ authority-zebra-indexdefs.xsl` See-also-from:w See-also-from:p See-also-from:s + + Match:w Match:p - + @@ -246,9 +252,11 @@ authority-zebra-indexdefs.xsl` See-also-from:w See-also-from:p See-also-from:s + + Match:w Match:p - + @@ -297,9 +305,11 @@ authority-zebra-indexdefs.xsl` See-also-from:w See-also-from:p See-also-from:s + + Match:w Match:p - + @@ -349,9 +359,11 @@ authority-zebra-indexdefs.xsl` See-also-from:w See-also-from:p See-also-from:s + + Match:w Match:p - + @@ -400,9 +412,11 @@ authority-zebra-indexdefs.xsl` See-also-from:w See-also-from:p See-also-from:s + + Match:w Match:p - + @@ -451,9 +465,11 @@ authority-zebra-indexdefs.xsl` See-also-from:w See-also-from:p See-also-from:s + + Match:w Match:p - + 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). - + - + @@ -299,10 +299,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + - + @@ -385,10 +385,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + - + @@ -471,10 +471,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + - + @@ -557,10 +557,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + - + @@ -643,10 +643,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + - + @@ -729,10 +729,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + - + @@ -815,10 +815,10 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + - + @@ -1128,7 +1128,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1163,7 +1167,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + + + + + + + + + + + + + + + + + + generalsubdiv + + + formsubdiv + + + chronologicalsubdiv + + + geographicsubdiv + + + + + + + + + + + + + @@ -1198,7 +1245,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1233,7 +1284,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + + + + + + + + + + + + + + + + + + generalsubdiv + + + formsubdiv + + + chronologicalsubdiv + + + geographicsubdiv + + + + + + + + + + + + + @@ -1268,7 +1362,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1303,7 +1401,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + + + + + + + + + + + + + + + + + + generalsubdiv + + + formsubdiv + + + chronologicalsubdiv + + + geographicsubdiv + + + + + + + + + + + + + @@ -1338,7 +1479,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1373,7 +1518,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + + + + + + + + + + + + + + + + + + generalsubdiv + + + formsubdiv + + + chronologicalsubdiv + + + geographicsubdiv + + + + + + + + + + + + + @@ -1408,7 +1596,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1443,7 +1635,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + + + + + + + + + + + + + + + + + + generalsubdiv + + + formsubdiv + + + chronologicalsubdiv + + + geographicsubdiv + + + + + + + + + + + + + @@ -1478,7 +1713,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1513,7 +1752,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + + + + + + + + + + + + + + + + + + generalsubdiv + + + formsubdiv + + + chronologicalsubdiv + + + geographicsubdiv + + + + + + + + + + + + + @@ -1548,7 +1830,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1583,7 +1869,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + + + + + + + + + + + + + + + + + + generalsubdiv + + + formsubdiv + + + chronologicalsubdiv + + + geographicsubdiv + + + + + + + + + + + + + @@ -1618,7 +1947,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1653,7 +1986,50 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + + + + + + + + + + + + + + + + + + generalsubdiv + + + formsubdiv + + + chronologicalsubdiv + + + geographicsubdiv + + + + + + + + + + + + + @@ -1688,7 +2064,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1723,7 +2103,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1758,7 +2142,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1793,7 +2181,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1828,7 +2220,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1863,7 +2259,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1898,7 +2298,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1933,7 +2337,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + @@ -1999,4 +2407,21 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). + + + + + + + + + + + + + + + + + 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). + + + + + + + + + + + + + + + @@ -368,7 +383,11 @@ authority-zebra-indexdefs.xsl` (substituting the appropriate file names). - + + + + + 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