@@ -, +, @@ authority links on a Z39.50 server. --- C4/Biblio.pm | 21 +- C4/Biblio.pm.rej | 20 ++ C4/Linker/Z3950Server.pm | 316 ++++++++++++++++++ .../Bug11300_LinkerZ3950Server_syspef.sql | 9 + installer/data/mysql/mandatory/sysprefs.sql | 1 + .../admin/preferences/authorities.pref | 5 + .../prog/en/modules/cataloguing/addbiblio.tt | 21 +- svc/cataloguing/automatic_linker.pl | 17 +- 8 files changed, 396 insertions(+), 14 deletions(-) create mode 100644 C4/Biblio.pm.rej create mode 100644 C4/Linker/Z3950Server.pm create mode 100644 installer/data/mysql/atomicupdate/Bug11300_LinkerZ3950Server_syspef.sql --- a/C4/Biblio.pm +++ a/C4/Biblio.pm @@ -115,6 +115,8 @@ use Koha::SearchEngine; use Koha::SearchEngine::Indexer; use Koha::Libraries; use Koha::Util::MARC; +use Koha::MetadataRecord; +use C4::Linker::Default; =head1 NAME @@ -618,7 +620,7 @@ sub BiblioAutoLink { =head2 LinkBibHeadingsToAuthorities - my $num_headings_changed, %results = LinkBibHeadingsToAuthorities($linker, $marc, $frameworkcode, [$allowrelink, $tagtolink, $verbose]); + my $num_headings_changed, %results = LinkBibHeadingsToAuthorities($linker, $marc, $frameworkcode, [$allowrelink, $tagtolink, $dont_auto_create, $verbose]); Links bib headings to authority records by checking each authority-controlled field in the C @@ -642,7 +644,9 @@ sub LinkBibHeadingsToAuthorities { my $allowrelink = shift; my $tagtolink = shift; my $verbose = shift; + my $dont_auto_create = shift; my %results; + my $linker_default = C4::Linker::Default->new(); if (!$bib) { carp 'LinkBibHeadingsToAuthorities called on undefined bib record'; return ( 0, {}); @@ -669,7 +673,15 @@ sub LinkBibHeadingsToAuthorities { next; } - my ( $authid, $fuzzy, $match_count ) = $linker->get_link($heading); + my ( $authid, $fuzzy, $match_count, $status ) = $linker->get_link($heading); + if ( defined $status ) { + if ($status eq 'NO_CONNECTION' or $status eq 'SERVER_NOT_FOUND') { + return 0, { error => $status }; + } + elsif ($status eq 'Z3950_SEARCH_EMPTY') { + ( $authid, $fuzzy, $match_count, $status ) = $linker_default->get_link($heading); + } + } if ($authid) { $results{ $fuzzy ? 'fuzzy' : 'linked' } ->{ $heading->display_form() }++; @@ -681,7 +693,7 @@ sub LinkBibHeadingsToAuthorities { $field->delete_subfield( code => '9' ) if defined $current_link; $field->add_subfields( '9', $authid ); $num_headings_changed++; - push(@{$results{'details'}}, { tag => $field->tag(), authid => $authid, status => 'LOCAL_FOUND'}) if $verbose; + push(@{$results{'details'}}, { tag => $field->tag(), authid => $authid, status => $status || 'LOCAL_FOUND'}) if $verbose; } else { my $authority_type = Koha::Authority::Types->find( $heading->auth_type() ); @@ -691,9 +703,10 @@ sub LinkBibHeadingsToAuthorities { $results{'fuzzy'}->{ $heading->display_form() }++; push(@{$results{'details'}}, { tag => $field->tag(), authid => $current_link, status => 'UNCHANGED'}) if $verbose; } - elsif ( C4::Context->preference('AutoCreateAuthorities') ) { + elsif ( !$dont_auto_create && C4::Context->preference('AutoCreateAuthorities') ) { if ( _check_valid_auth_link( $current_link, $field ) ) { $results{'linked'}->{ $heading->display_form() }++; + push(@{$results{'details'}}, { tag => $field->tag(), authid => $authid, status => 'UNCHANGED'}) if $verbose; } elsif ( $match_count > 1 ) { $results{'unlinked'}->{ $heading->display_form() }++; push(@{$results{'details'}}, { tag => $field->tag(), authid => undef, status => 'MULTIPLE_MATCH', auth_type => $heading->auth_type(), tag_to_report => $authority_type->auth_tag_to_report}) if $verbose; --- a/C4/Biblio.pm.rej +++ a/C4/Biblio.pm.rej @@ -0,0 +1,20 @@ +diff a/C4/Biblio.pm b/C4/Biblio.pm (rejected hunks) +@@ -461,7 +462,7 @@ sub BiblioAutoLink { + + =head2 LinkBibHeadingsToAuthorities + +- my $num_headings_changed, %results = LinkBibHeadingsToAuthorities($linker, $marc, $frameworkcode, [$allowrelink, $verbose]); ++ my $num_headings_changed, %results = LinkBibHeadingsToAuthorities($linker, $marc, $frameworkcode, [$allowrelink, $verbose, $dont_auto_create]); + + Links bib headings to authority records by checking + each authority-controlled field in the C +@@ -484,7 +485,9 @@ sub LinkBibHeadingsToAuthorities { + my $frameworkcode = shift; + my $allowrelink = shift; + my $verbose = shift; ++ my $dont_auto_create = shift; + my %results; ++ my $linker_default=C4::Linker::Default->new(); + if (!$bib) { + carp 'LinkBibHeadingsToAuthorities called on undefined bib record'; + return ( 0, {}); --- a/C4/Linker/Z3950Server.pm +++ a/C4/Linker/Z3950Server.pm @@ -0,0 +1,316 @@ +package C4::Linker::Z3950Server; + +# Copyright 2012 Frédérick Capovilla - Libéo +# +# 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 3 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, see . + +use strict; +use warnings; +use Carp; +use MARC::Field; +use MARC::Record; +use C4::Context; +use C4::Heading; +use ZOOM; # For Z39.50 Searches +use C4::AuthoritiesMarc; # For Authority addition +use C4::Log; # logaction +use Koha::SearchEngine::Indexer; + +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; + my $match_count = 0; + my $status = ''; + + if ( $self->{'cache'}->{$search_form}->{'cached'} ) { + $authid = $self->{'cache'}->{$search_form}->{'authid'}; + $fuzzy = $self->{'cache'}->{$search_form}->{'fuzzy'}; + }else { + # Look for matching authorities on the Z39.50 server + unless($self->{'local_first'}) { + ($authid, undef, $status) = $self->getZ3950Authority($heading); + if($status eq 'Z3950_SEARCH_ERROR' or + $status eq 'Z3950_QUERY_ERROR' or + $status eq 'NO_CONNECTION' or + $status eq 'SERVER_NOT_FOUND') { + return $authid, $fuzzy, 0, $status; + } + } + if(!$authid) { + # look for matching authorities + my $authorities = $heading->authorities(1); # $skipmetadata = true + $match_count = scalar @$authorities; + + 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; + } + + if ( !defined $authid && $self->{'broader_headings'} ) { + my $field = $heading->field(); + my @subfields = $field->subfields(); + if ( scalar @subfields > 1 ) { + pop @subfields; + $field->replace_with( + MARC::Field->new( + $field->tag, + $field->indicator(1), + $field->indicator(2), + map { $_[0] => $_[1] } @subfields + ) + ); + ( $authid, $fuzzy ) = + $self->get_link( C4::Heading->new_from_bib_field($field), + $behavior ); + } + } + } + if($self->{'local_first'} && !$authid) { + ($authid, undef, $status) = $self->getZ3950Authority($heading); + } + + $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, $match_count, $status; +} + +sub update_cache { + my $self = shift; + my $heading = shift; + my $authid = shift; + my $search_form = $heading->search_form(); + my $fuzzy = 0; + + $self->{'cache'}->{$search_form}->{'cached'} = 1; + $self->{'cache'}->{$search_form}->{'authid'} = $authid; + $self->{'cache'}->{$search_form}->{'fuzzy'} = $fuzzy; +} + +sub flip_heading { + my $self = shift; + my $heading = shift; + + # TODO: implement +} + + +=head1 getZ3950Authority + + ($authid, $record, $status) = $self->getZ3950Authority($heading); + + Do a Z39.50 search for the heading using the $conn ZOOM::Connection object and the $heading Heading. + The column origincode is used to check for duplicates. + FIXME: Use thesaurus in search? As of Koha 3.8, the community stopped using them. + + RETURNS : + $authid = the ID of the local copy of the authority record that was found. undef if nothing was found. + $record = the MARC record of the found authority. + $status = A string with additional informations on the search status (Z3950_CREATED, Z3950_UPDATED, Z3950_QUERY_ERROR, Z3950_SEARCH_ERROR) + +=cut + +sub getZ3950Authority { + my $self = shift; + my $heading = shift; + + # Try to find a match on the Z39.50 server if LinkerZ3950Server is set + if(C4::Context->preference('LinkerZ3950Server')) { + unless($self->{'conn'}) { + my $sth = C4::Context->dbh->prepare("select * from z3950servers where servername=?"); + $sth->execute(C4::Context->preference('LinkerZ3950Server')); + my $server = $sth->fetchrow_hashref or undef; + $sth->finish; + + if($server) { + my $options = ZOOM::Options->new(); + $options->option('cclfile' => C4::Context->new()->{"serverinfo"}->{"authorityserver"}->{"ccl2rpn"}); + $options->option('elementSetName', 'F'); + $options->option('async', 0); + $options->option('databaseName', $server->{db}); + $options->option('user', $server->{userid} ) if $server->{userid}; + $options->option('password', $server->{password}) if $server->{password}; + $options->option('preferredRecordSyntax', $server->{syntax}); + $self->{'conn'} = create ZOOM::Connection($options); + eval{ $self->{'conn'}->connect( $server->{host}, $server->{port} ) }; + if($@) { + return (undef, undef, 'NO_CONNECTION'); + }} + else { + return (undef, undef, 'SERVER_NOT_FOUND'); + } + } + } + else { + return; + } + my $query =qq(Match-Heading,do-not-truncate,ext="$heading->{'search_form'}"); + my $zquery = eval{ ZOOM::Query::CCL2RPN->new($query, $self->{'conn'}) }; + if($@) { + warn $query . "\n" . $@; + return (undef, undef, 'Z3950_QUERY_ERROR'); + } + + # Try to send the search query to the server. + my $rs = eval{ $self->{'conn'}->search($zquery) }; + if($@){ + warn $query . "\n" . $@; + return (undef, undef, 'Z3950_SEARCH_EMPTY'); + } + + # If authorities are found, select the first valid one for addition in the local authority table. + my $record; + if($rs->size() != 0) { + $record = MARC::Record::new_from_usmarc($rs->record(0)->raw()); + }else{ + return (undef, undef, 'Z3950_SEARCH_ERROR'); + } + $rs->destroy(); + $zquery->destroy(); + + # If a valid authority was found, add it in the local authority database. + if($record) { + my $dbh=C4::Context->dbh; + + my $authtypecode = C4::AuthoritiesMarc::GuessAuthTypeCode($record); + my $authId; + + # Use the control number to prevent the creation of duplicate authorities. + my $controlNumber = $record->field('970')->subfield('0'); + my $sthExist=$dbh->prepare("SELECT authid FROM auth_header WHERE origincode =?"); + $sthExist->execute($controlNumber) or die $sthExist->errstr; + ($authId) = $sthExist->fetchrow; + $sthExist->finish; + + #------------------------------------------------------------------------------------------ + # Corrections and verifications before insertion + my $format; + my $leader=' nz a22 o 4500';# Leader for incomplete MARC21 record + + if (uc(C4::Context->preference('marcflavour')) eq 'UNIMARC') { + $format= 'UNIMARCAUTH'; + } + else { + $format= 'MARC21'; + } + + if ($format eq "MARC21") { + if (!$record->leader) { + $record->leader($leader); + } + if (!$record->field('003')) { + $record->insert_fields_ordered( + MARC::Field->new('003',C4::Context->preference('MARCOrgCode')) + ); + } + my $time=POSIX::strftime("%Y%m%d%H%M%S",localtime); + if (!$record->field('005')) { + $record->insert_fields_ordered( + MARC::Field->new('005',$time.".0") + ); + } + my $date=POSIX::strftime("%y%m%d",localtime); + if (!$record->field('008')) { + # Get a valid default value for field 008 + my $default_008 = C4::Context->preference('MARCAuthorityControlField008'); + if(!$default_008 or length($default_008)<34) { + $default_008 = '|| aca||aabn | a|a d'; + } + else { + $default_008 = substr($default_008,0,34); + } + $record->insert_fields_ordered( MARC::Field->new('008',$date.$default_008) ); + } + if (!$record->field('040')) { + $record->insert_fields_ordered( + MARC::Field->new('040','','', + 'a' => C4::Context->preference('MARCOrgCode'), + 'c' => C4::Context->preference('MARCOrgCode') + ) + ); + } + } + if ($format eq "UNIMARCAUTH") { + $record->leader(" nx j22 ") unless ($record->leader()); + my $date=POSIX::strftime("%Y%m%d",localtime); + if (my $string=$record->subfield('100',"a")){ + $string=~s/fre50/frey50/; + $record->field('100')->update('a'=>$string); + } + elsif ($record->field('100')){ + $record->field('100')->update('a'=>$date."afrey50 ba0"); + } else { + $record->append_fields( + MARC::Field->new('100',' ',' ' + ,'a'=>$date."afrey50 ba0") + ); + } + } + my ($auth_type_tag, $auth_type_subfield) = C4::AuthoritiesMarc::get_auth_type_location($authtypecode); + if (!$authId and $format eq "MARC21") { + # only need to do this fix when modifying an existing authority + C4::AuthoritiesMarc::MARC21::fix_marc21_auth_type_location($record, $auth_type_tag, $auth_type_subfield); + } + if (my $field=$record->field($auth_type_tag)){ + $field->update($auth_type_subfield=>$authtypecode); + } + else { + $record->add_fields($auth_type_tag,'','', $auth_type_subfield=>$authtypecode); + } + #------------------------------------------------------------------------------------------ + if ($authId) { + my $sth=$dbh->prepare("UPDATE auth_header SET authtypecode=?,marc=?,marcxml=? WHERE origincode =?"); + eval { $sth->execute($authtypecode,$record->as_usmarc,$record->as_xml_record($format),$controlNumber) or die $sth->errstr; }; + $sth->finish; + warn "Problem with authority $controlNumber : Cannot update" if $@; + $dbh->commit unless $dbh->{AutoCommit}; + return if $@; + + my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::AUTHORITIES_INDEX }); + $indexer->index_records($authId,'specialUpdate',"authorityserver",$record); + return ($authId, $record, 'Z3950_UPDATED'); + } + else { + my $sth=$dbh->prepare("INSERT INTO auth_header (datecreated,authtypecode,marc,marcxml,origincode) VALUES (NOW(),?,?,?,?)"); + eval { $sth->execute($authtypecode,$record->as_usmarc,$record->as_xml_record($format),$controlNumber) or die $sth->errstr; }; + $sth->finish; + warn "Problem with authority $controlNumber : Cannot insert" if $@; + my $id = $dbh->{'mysql_insertid'}; + $dbh->commit unless $dbh->{AutoCommit}; + return if $@; + + logaction( "AUTHORITIES", "ADD", $id, "authority" ) if C4::Context->preference("AuthoritiesLog"); + my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::AUTHORITIES_INDEX }); + $indexer->index_records($id,'specialUpdate',"authorityserver",$record); + return ($id, $record, 'Z3950_CREATED'); + } + } + return ; +} + +1; --- a/installer/data/mysql/atomicupdate/Bug11300_LinkerZ3950Server_syspef.sql +++ a/installer/data/mysql/atomicupdate/Bug11300_LinkerZ3950Server_syspef.sql @@ -0,0 +1,9 @@ +$DBversion = 'XXX'; +if ( CheckVersion( $DBversion ) ) { + $dbh->do(q{ + INSERT IGNORE INTO systempreferences (`variable`, `value`, `options`, `explanation`, `type`) + VALUES ('LinkerZ3950Server', '', NULL, 'Import authorities from this Z39.50 server when searching for authority links with the Z39.50 Server linker module', 'free') + ­}); + + NewVersion( $DBversion, 12446, "Add new system preference LinkerZ3950Server"); +} --- a/installer/data/mysql/mandatory/sysprefs.sql +++ a/installer/data/mysql/mandatory/sysprefs.sql @@ -366,6 +366,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('LinkerModule','Default','Default|FirstMatch|LastMatch','Chooses which linker module to use (see documentation).','Choice'), ('LinkerOptions','','','A pipe-separated list of options for the linker.','free'), ('LinkerRelink','1',NULL,'If ON the authority linker will relink headings that have previously been linked every time it runs.','YesNo'), +('LinkerZ3950Server','',NULL,'Import authorities from this Z39.50 server when searching for authority links with the Z39.50 Server linker module',''), ('ListOwnerDesignated', NULL, NULL, 'Designated list owner at patron deletion', 'Free'), ('ListOwnershipUponPatronDeletion', 'delete', 'delete|transfer', 'Defines the action on their public or shared lists when patron is deleted', 'Choice'), ('LoadCheckoutsTableDelay','0','','Delay before auto-loading checkouts table on checkouts screen','Integer'), --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/authorities.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/authorities.pref @@ -105,6 +105,7 @@ Authorities: Default: default FirstMatch: "first match" LastMatch: "last match" + Z3950Server: "Z39.50 Server" - linker module for matching headings to authority records. - - "Set the following options for the authority linker:" @@ -146,3 +147,7 @@ Authorities: 1: Do 0: "Don't" - compare the source for 6XX headings to the thesaurus source for authority records when linking. Enabling this preference may require a reindex, and may generate new authority records if AutoCreateAuthorities is enabled. + - + - pref: LinkerZ3950Server + - class: multi + - Import authorities from this Z39.50 server when searching for authority links with the Z39.50 Server linker module : --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt @@ -363,6 +363,16 @@ function updateHeadingLinks(links) { var message = ''; var field_class = 'no_matching_authority_field'; switch(heading.status) { + case 'Z3950_CREATED': + image = ' '; + message = _("A matching authority record was found on the Z39.50 server and was imported locally."); + field_class = 'matching_authority_field'; + break; + case 'Z3950_UPDATED': + image = ' '; + message = _("A matching authority record was found on the Z39.50 server and a local authority was updated."); + field_class = 'matching_authority_field'; + break; case 'LOCAL_FOUND': image = ' '; message = _("A matching authority was found in the local database."); @@ -416,7 +426,7 @@ function AutomaticLinker() { $('#f').find('.tag').each(function() { var empty = true; $(this).find('.input_marceditor').each(function() { - if($(this).val() != '') { + if(this.value != '') { empty = false; return false; } @@ -428,8 +438,9 @@ function AutomaticLinker() { // Get all the form values to post via AJAX var form_data = {}; + var i=0; $('#f').find(':input').each(function(){ - form_data[this.name] = $(this).val(); + form_data[this.name] = this.value; }); delete form_data['']; @@ -447,6 +458,12 @@ function AutomaticLinker() { case 'UNAUTHORIZED': alert(_("Error : You do not have the permissions necessary to use this functionality.")); break; + case 'SERVER_NOT_FOUND': + alert(_("Error : The Z39.50 server configured in the 'LinkerZ3950Server' preference was not found in the Z39.50 servers list.")); + break; + case 'NO_CONNECTION': + alert(_("Error : Could not connect to the Z39.50 server.")); + break; case 'OK': updateHeadingLinks(json.links); break; --- a/svc/cataloguing/automatic_linker.pl +++ a/svc/cataloguing/automatic_linker.pl @@ -33,17 +33,13 @@ my ( $auth_status ) = editauthorities => 1, editcatalogue => 'edit_catalogue' }); -if ( $auth_status ne "ok" ) { - print to_json( { status => 'UNAUTHORIZED' } ); - exit 0; -} # Link the biblio headings to authorities and return a json containing the status of all the links. # Example : {"status":"OK","links":[{"authid":"123","status":"LINK_CHANGED","tag":"650"}]} # # tag = the tag number of the field # authid = the value of the $9 subfield for this tag -# status = The status of the link (LOCAL_FOUND, NONE_FOUND, MULTIPLE_MATCH, UNCHANGED, CREATED) +# status = The status of the link (LOCAL_FOUND, NONE_FOUND, MULTIPLE_MATCH, UNCHANGED, CREATED, 3950_CREATED, Z3950_UPDATED) my $json; @@ -54,8 +50,13 @@ my ( $headings_changed, $results ) = BiblioAutoLink ( scalar $input->param('frameworkcode'), 1 ); - -$json->{status} = 'OK'; -$json->{links} = $results->{details} || ''; +if(defined $results->{error}) { + $json->{links} = $results->{details} || ''; + $json->{links} = $results->{details} || ''; +} +else{ + $json->{status} = 'OK'; + $json->{links} = $results->{details} || ''; +} print to_json($json); --