From: Jared Camins-Esakov Date: Sat, 11 May 2013 20:11:38 +0000 (-0400) Subject: Bug 5202: merge authorities from the authority file and reservoir X-Git-Url: http://git.rot13.org/?a=commitdiff_plain;h=b5bd2b75865da040a44b2b48874da35b92e74cd5;p=koha.git Bug 5202: merge authorities from the authority file and reservoir This patch gives Koha the ability to merge authority records using the same interface used by bibliographic records, though slightly different methods for selecting which records to merge. The two ways to select records are as follows: 1) Records can be selected from authority search results by clicking the "Merge" link for two records. 2) Authority records can be merged from the reservoir by clicking the merge-related links in the Manage staged MARC batch screen. To test: 1) Apply patch. 2) Do a search for an authority record that will turn up multiple identical records (or at least two records that you don't mind merging). 3) Click the "Merge" link for the first record. 4) Click the "Merge" link for the second record. 5) Choose which fields from which record you want to appear in the resulting record. 6) Confirm that those are the fields that exist in the resulting record. 7) Stage an authority record (for example, an authority record you saved from your catalog. 8) Search for a record to merge with it using the "Search for a record to merge in a new window" link. 9) Merge these records, confirming that the resulting record (after going through the entire merging process) matches your expectations. 10) Set up a matching rule for authorities, and export an authority from your catalog that will match based on that rule. For MARC21, the following is a good choice for a rule: Matching rule code: AUTHPER Description: Personal name main entry Match threshold: 999 Record type: Authority record [Match point 1:] Search index: mainmainentry Score: 1000 Tag: 100 Subfields: a 11) Stage the record you just exported, choosing the matching rule you just created. 12) Merge the record using the "Merge" link, confirming that the resulting record (after going through the entire merging process) matches your expectations. Signed-off-by: Chris Cormack Signed-off-by: Katrin Fischer Testing notes on last patch in series. Signed-off-by: Galen Charlton --- diff --git a/Koha/Authority.pm b/Koha/Authority.pm index 509fe3fad9..7405936fd3 100644 --- a/Koha/Authority.pm +++ b/Koha/Authority.pm @@ -37,6 +37,7 @@ use C4::Context; use MARC::Record; use MARC::File::XML; use C4::Charset; +use Koha::Util::MARC; use base qw(Koha::MetadataRecord); @@ -53,7 +54,12 @@ sub new { my $class = shift; my $record = shift; - my $self = $class->SUPER::new( { record => $record }); + my $self = $class->SUPER::new( + { + 'record' => $record, + 'schema' => lc C4::Context->preference("marcflavour") + } + ); bless $self, $class; return $self; @@ -84,6 +90,13 @@ sub get_from_authid { return if ($@); $record->encoding('UTF-8'); + # NOTE: GuessAuthTypeCode has no business in Koha::Authority, which is an + # object-oriented class. Eventually perhaps there will be utility + # classes in the Koha:: namespace, but there are not at the moment, + # so this shim seems like the best option all-around. + require C4::AuthoritiesMarc; + $authtypecode ||= C4::AuthoritiesMarc::GuessAuthTypeCode($record); + my $self = $class->SUPER::new( { authid => $authid, authtype => $authtypecode, schema => $marcflavour, @@ -93,4 +106,49 @@ sub get_from_authid { return $self; } +=head2 get_from_breeding + + my $auth = Koha::Authority->get_from_authid($authid); + +Create the Koha::Authority object associated with the provided authid. + +=cut +sub get_from_breeding { + my $class = shift; + my $import_record_id = shift; + my $marcflavour = lc C4::Context->preference("marcflavour"); + + my $dbh=C4::Context->dbh; + my $sth=$dbh->prepare("select marcxml from import_records where import_record_id=? and record_type='auth';"); + $sth->execute($import_record_id); + my $marcxml = $sth->fetchrow; + my $record=eval {MARC::Record->new_from_xml(StripNonXmlChars($marcxml),'UTF-8', + (C4::Context->preference("marcflavour") eq "UNIMARC"?"UNIMARCAUTH":C4::Context->preference("marcflavour")))}; + return if ($@); + $record->encoding('UTF-8'); + + # NOTE: GuessAuthTypeCode has no business in Koha::Authority, which is an + # object-oriented class. Eventually perhaps there will be utility + # classes in the Koha:: namespace, but there are not at the moment, + # so this shim seems like the best option all-around. + require C4::AuthoritiesMarc; + my $authtypecode = C4::AuthoritiesMarc::GuessAuthTypeCode($record); + + my $self = $class->SUPER::new( { + schema => $marcflavour, + authtype => $authtypecode, + record => $record }); + + bless $self, $class; + return $self; +} + +sub authorized_heading { + my ($self) = @_; + if ($self->schema =~ m/marc/) { + return Koha::Util::MARC::getAuthorityAuthorizedHeading($self->record, $self->schema); + } + return; +} + 1; diff --git a/Koha/Util/MARC.pm b/Koha/Util/MARC.pm index fc3c9f247c..500ba53a2c 100644 --- a/Koha/Util/MARC.pm +++ b/Koha/Util/MARC.pm @@ -106,4 +106,73 @@ sub _createKey { return int(rand(1000000)); } +=head2 getAuthorityAuthorizedHeading + +Retrieve the authorized heading from a MARC authority record + +=cut + +sub getAuthorityAuthorizedHeading { + my ( $record, $schema ) = @_; + return unless ( ref $record eq 'MARC::Record' ); + if ( $schema eq 'unimarc' ) { + + # construct UNIMARC summary, that is quite different from MARC21 one + # accepted form + foreach my $field ( $record->field('2..') ) { + return $field->as_string('abcdefghijlmnopqrstuvwxyz'); + } + } + else { + foreach my $field ( $record->field('1..') ) { + my $tag = $field->tag(); + next if "152" eq $tag; + + # FIXME - 152 is not a good tag to use + # in MARC21 -- purely local tags really ought to be + # 9XX + if ( $tag eq '100' ) { + return $field->as_string('abcdefghjklmnopqrstvxyz68'); + } + elsif ( $tag eq '110' ) { + return $field->as_string('abcdefghklmnoprstvxyz68'); + } + elsif ( $tag eq '111' ) { + return $field->as_string('acdefghklnpqstvxyz68'); + } + elsif ( $tag eq '130' ) { + return $field->as_string('adfghklmnoprstvxyz68'); + } + elsif ( $tag eq '148' ) { + return $field->as_string('abvxyz68'); + } + elsif ( $tag eq '150' ) { + return $field->as_string('abvxyz68'); + } + elsif ( $tag eq '151' ) { + return $field->as_string('avxyz68'); + } + elsif ( $tag eq '155' ) { + return $field->as_string('abvxyz68'); + } + elsif ( $tag eq '180' ) { + return $field->as_string('vxyz68'); + } + elsif ( $tag eq '181' ) { + return $field->as_string('vxyz68'); + } + elsif ( $tag eq '182' ) { + return $field->as_string('vxyz68'); + } + elsif ( $tag eq '185' ) { + return $field->as_string('vxyz68'); + } + else { + return $field->as_string(); + } + } + } + return; +} + 1; diff --git a/authorities/merge.pl b/authorities/merge.pl new file mode 100755 index 0000000000..0e3fb6df84 --- /dev/null +++ b/authorities/merge.pl @@ -0,0 +1,186 @@ +#!/usr/bin/perl + +# Copyright 2013 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 CGI; +use C4::Output; +use C4::Auth; +use C4::AuthoritiesMarc; +use Koha::Authority; +use C4::Koha; +use C4::Biblio; + +my $input = new CGI; +my @authid = $input->param('authid'); +my $merge = $input->param('merge'); + +my @errors; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "authorities/merge.tt", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { editauthorities => 1 }, + } +); + +#------------------------ +# Merging +#------------------------ +if ($merge) { + + # Creating a new record from the html code + my $record = TransformHtmlToMarc($input); + my $recordid1 = $input->param('recordid1'); + my $recordid2 = $input->param('recordid2'); + my $typecode = $input->param('frameworkcode'); + + # Rewriting the leader + $record->leader( GetAuthority($recordid1)->leader() ); + + # Modifying the reference record + ModAuthority( $recordid1, $record, $typecode ); + + # Deleting the other record + if ( scalar(@errors) == 0 ) { + + my $error; + if ($input->param('mergereference') eq 'breeding') { + require C4::ImportBatch; + C4::ImportBatch::SetImportRecordStatus( $recordid2, 'imported' ); + } else { + $error = (DelAuthority($recordid2) == 0); + } + push @errors, $error if ($error); + } + + # Parameters + $template->param( + result => 1, + recordid1 => $recordid1 + ); + + #------------------------- + # Show records to merge + #------------------------- +} +else { + my $mergereference = $input->param('mergereference'); + $template->{'VARS'}->{'mergereference'} = $mergereference; + + if ( scalar(@authid) != 2 ) { + push @errors, { code => "WRONG_COUNT", value => scalar(@authid) }; + } + else { + my $recordObj1 = Koha::Authority->get_from_authid($authid[0]) || Koha::Authority->new(); + my $recordObj2; + + if ($mergereference eq 'breeding') { + $recordObj2 = Koha::Authority->get_from_breeding($authid[1]) || Koha::Authority->new(); + $mergereference = $authid[0]; + } else { + $recordObj2 = Koha::Authority->get_from_authid($authid[1]) || Koha::Authority->new(); + } + + if ($mergereference) { + + my $framework; + if ( $recordObj1->authtype ne $recordObj2->authtype && $mergereference ne 'breeding' ) { + $framework = $input->param('frameworkcode') + or push @errors, "Framework not selected."; + } + else { + $framework = $recordObj1->authtype; + } + + # Getting MARC Structure + my $tagslib = GetTagsLabels( 1, $framework ); + + my $notreference = + ( $authid[0] == $mergereference ) + ? $authid[1] + : $authid[0]; + + # Creating a loop for display + + my @record1 = $recordObj1->createMergeHash($tagslib); + my @record2 = $recordObj2->createMergeHash($tagslib); + + # Parameters + $template->param( + recordid1 => $mergereference, + recordid2 => $notreference, + record1 => @record1, + record2 => @record2, + framework => $framework, + ); + } + else { + + # Ask the user to choose which record will be the kept + $template->param( + choosereference => 1, + recordid1 => $authid[0], + recordid2 => $authid[1], + title1 => $recordObj1->authorized_heading, + title2 => $recordObj2->authorized_heading, + ); + if ( $recordObj1->authtype ne $recordObj2->authtype ) { + my $frameworks = getauthtypes; + my @frameworkselect; + foreach my $thisframeworkcode ( keys %$frameworks ) { + my %row = ( + value => $thisframeworkcode, + frameworktext => + $frameworks->{$thisframeworkcode}->{'authtypetext'}, + ); + if ( $recordObj1->authtype eq $thisframeworkcode ) { + $row{'selected'} = 1; + } + push @frameworkselect, \%row; + } + $template->param( + frameworkselect => \@frameworkselect, + frameworkcode1 => $recordObj1->authtype, + frameworkcode2 => $recordObj2->authtype, + ); + } + } + } +} + +if (@errors) { + + # Errors + $template->param( errors => \@errors ); +} + +output_html_with_http_headers $input, $cookie, $template->output; +exit; + +=head1 FUNCTIONS + +=cut + +# ------------------------ +# Functions +# ------------------------ diff --git a/authorities/merge_ajax.pl b/authorities/merge_ajax.pl new file mode 100755 index 0000000000..dd716a72b3 --- /dev/null +++ b/authorities/merge_ajax.pl @@ -0,0 +1,27 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use CGI; +use CGI::Session; +use C4::Context; +use C4::Auth qw/check_cookie_auth/; +use C4::AuthoritiesMarc; +use JSON; +use CGI::Cookie; # need to check cookies before + # having CGI parse the POST request + +my %cookies = fetch CGI::Cookie; +my ($auth_status, $sessionID) = check_cookie_auth($cookies{'CGISESSID'}->value, { editcatalogue => 'edit_catalogue' }); +if ($auth_status ne "ok") { + my $reply = CGI->new(""); + print $reply->header(-type => 'text/html'); + exit 0; +} + +my $reply = new CGI; +my $framework = $reply->param('frameworkcode'); +my $tagslib = GetTagsLabels(1, $framework); +print $reply->header(-type => 'text/html'); +print encode_json $tagslib; diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/authorities_js.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/authorities_js.inc new file mode 100644 index 0000000000..d1964d1d3d --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/authorities_js.inc @@ -0,0 +1,38 @@ +function mergeAuth(authid, summary) { + var alreadySelected = $.cookie('auth_to_merge'); + if (alreadySelected !== null) { + alreadySelected = JSON.parse(alreadySelected); + $.cookie('auth_to_merge', '', { 'path': '/', 'expires': -1 }); + var refstring = ""; + if (typeof alreadySelected.mergereference !== 'undefined') { + refstring = "&mergereference=" + alreadySelected.mergereference; + } + window.location.href = "/cgi-bin/koha/authorities/merge.pl?authid=" + authid + "&authid=" + alreadySelected.authid + refstring; + } else { + $.cookie('auth_to_merge', JSON.stringify({ 'authid': authid, 'summary': summary }), { 'path' : '/' }); + showMergingInProgress(); + } +} + +function showMergingInProgress() { + var alreadySelected = $.cookie('auth_to_merge'); + if (alreadySelected !== null) { + alreadySelected = JSON.parse(alreadySelected); + $('#merge_in_progress').html(_("Merging with authority: ") + "" + alreadySelected.summary + " (" + alreadySelected.authid + ") " + _("Cancel merge") + ""); + $('#cancel_merge').click(function(event) { + event.preventDefault(); + $.cookie('auth_to_merge', '', { 'path': '/', 'expires': -1 }); + $('#merge_in_progress').empty(); + }); + } else { + $('#merge_in_progress').empty(); + } +} + +$(document).ready(function () { + showMergingInProgress(); + $('.merge_auth').click(function (event) { + event.preventDefault(); + mergeAuth($(this).parents('tr').attr('data-authid'), $(this).parents('tr').find('div.authorizedheading').text()); + }); +}); diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities-home.tt index 7d138ff1d7..558746e6ce 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities-home.tt @@ -19,6 +19,7 @@ function searchauthority() { function confirm_deletion() { // not really implemented, but required by phantom delAuthButton code in authorities-toolbar.inc return true; } +[% INCLUDE 'authorities_js.inc' %] //]]> @@ -35,6 +36,8 @@ function confirm_deletion() { // not really implemented, but required by phant [% INCLUDE 'authorities-toolbar.inc' %] +
+ diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/merge.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/merge.tt new file mode 100644 index 0000000000..d201a2297c --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/merge.tt @@ -0,0 +1,143 @@ +[% PROCESS 'merge-record.inc' %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Cataloging › Merging records +[% INCLUDE 'greybox.inc' %] +[% INCLUDE 'doc-head-close.inc' %] + + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'authorities-search.inc' %] + + +
+ +
+
+ + +

Merging records

+[% IF ( result ) %] + [% IF ( errors ) %] + + [% FOREACH error IN errors %] +
+ + [% IF error.code == 'CANNOT_MOVE' %] + The following items could not be moved from the old record to the new one: [% error.value %] + [% ELSE %] + [% error %] + [% END %] + +
Therefore, the record to be merged has not been deleted.
+ [% END %] + + [% ELSE %] + +

The merging was successful. Click here to see the merged record.

+ [% END %] + +[% ELSE %] + +[% IF ( choosereference ) %] +

Please choose which record will be the reference for the merge. The record chosen as reference will be kept, and the other will be deleted.

+
+
+ Merge reference +
    +
  1. +
  2. + + [% IF frameworkselect %] +
  3. +
  4. + [% END %] +
+ + + +
+
+
+[% ELSE %] +[% IF ( errors ) %] +
+ [% FOREACH error IN errors %] +

+ [% IF error.code == 'WRONG_COUNT' %] + Number of records provided for merging: [% error.value %]. Currently only 2 records can be merged at a time. + [% ELSE %] + [% error %] + [% END %] + +

+ [% END %] +
+[% ELSE %] +
+ +
+
+[% PROCESS mergesource %] +
+
+[% PROCESS mergetarget %] +
+ + + + + + +
+
+
+[% END %] +[% END %] +[% END %] + +
+
+
+ +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/searchresultlist.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/searchresultlist.tt index ea16505594..f73b8190af 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/searchresultlist.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/searchresultlist.tt @@ -35,6 +35,8 @@ function searchauthority() { Y = document.forms[0].value.value; window.location="/cgi-bin/koha/authorities/authorities-home.pl?op=do_search&type=intranet&authtypecode="+X+"&value="+Y+"&marclist=&and_or=and&excluding=&operator=contains"; } + +[% INCLUDE 'authorities_js.inc' %] //]]> @@ -53,6 +55,7 @@ function searchauthority() { [% INCLUDE 'authorities-toolbar.inc' %]

Authority search results

+
[% IF ( total ) %]
[% pagination_bar %]
@@ -73,9 +76,9 @@ function searchauthority() { [% FOREACH resul IN result %] [% UNLESS ( loop.odd ) %] - + [% ELSE %] - + [% END %] [% PROCESS authresult summary=resul.summary link="/cgi-bin/koha/authorities/authorities-home.pl?op=do_search&type=intranet&marclist=any&operator=contains&orderby=HeadingAsc&value=" %] Details @@ -87,6 +90,7 @@ function searchauthority() { [% IF ( CAN_user_editauthorities ) %] Edit + | Merge [% UNLESS ( resul.used ) %] | Delete [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/manage-marc-import.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/manage-marc-import.tt index 789b02aab6..c1d7060bb9 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/manage-marc-import.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/manage-marc-import.tt @@ -12,9 +12,16 @@ [% IF ( record.record_type == 'biblio' ) %] Matches biblio [% record_lis.match_id %] (score = [% record_lis.match_score %]): [% record_lis.match_citation %] [% ELSIF ( record.record_type == 'auth' ) %] - Matches authority [% record_lis.match_id %] (score = [% record_lis.match_score %]): [% record_lis.match_citation %] + Matches authority [% record_lis.match_id %] (score = [% record_lis.match_score %]): [% record_lis.match_citation %] + Merge + [% END %] + [% ELSIF ( record.record_type == 'auth') %] + + + Search for a record to merge in a new window + [% END %] [% END %] [% INCLUDE 'doc-head-open.inc' %] @@ -41,6 +48,13 @@ $(document).ready(function(){ $("#"+str+" option[selected='selected']").attr("selected","selected"); $(this).parent().hide(); }); + + $('.merge_auth').click(function(event) { + event.preventDefault(); + var authid = $(this).parents('tr').attr('data-authid'); + $.cookie('auth_to_merge', JSON.stringify({ 'authid': authid, 'summary': $('tr[data-id="' + authid + '"] .citation').text(), 'mergereference': 'breeding' }), { 'path': '/' }); + window.open("/cgi-bin/koha/authorities/authorities-home.pl"); + }); }); //]]> @@ -383,9 +397,9 @@ Page [% FOREACH record_lis IN record_list %] - [% UNLESS ( loop.odd ) %][% ELSE %][% END %] + [% UNLESS ( loop.odd ) %][% ELSE %][% END %] [% record_lis.record_sequence %] - [% record_lis.citation %] + [% record_lis.citation %] [% IF ( record_lis.status == 'imported' ) %] Imported diff --git a/t/Koha_Util_MARC.t b/t/Koha_Util_MARC.t index 797fc3442c..daf91f0e43 100755 --- a/t/Koha_Util_MARC.t +++ b/t/Koha_Util_MARC.t @@ -23,7 +23,7 @@ use strict; use warnings; -use Test::More tests => 3; +use Test::More tests => 4; BEGIN { use_ok('Koha::Util::MARC'); @@ -95,3 +95,5 @@ foreach my $field (@$hash) { is_deeply($hash, $samplehash, 'Generated hash correctly'); my $dupkeys = grep { $_ > 1 } values %fieldkeys; is($dupkeys, 0, 'No duplicate keys'); + +is(Koha::Util::MARC::getAuthorityAuthorizedHeading($marcrecord, 'marc21'), 'Cooking', 'Routine for retrieving authorized heading works'); diff --git a/t/db_dependent/Koha_Authority.t b/t/db_dependent/Koha_Authority.t index dc577744df..6373fb75f8 100755 --- a/t/db_dependent/Koha_Authority.t +++ b/t/db_dependent/Koha_Authority.t @@ -38,6 +38,8 @@ my $authority = Koha::Authority->new($record); is(ref($authority), 'Koha::Authority', 'Created valid Koha::Authority object'); +is($authority->authorized_heading(), 'Cooking', 'Authorized heading was correct'); + is_deeply($authority->record, $record, 'Saved record'); SKIP: @@ -63,4 +65,28 @@ SKIP: is($authority, undef, 'No invalid record is retrieved'); } +SKIP: +{ + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("SELECT import_record_id FROM import_records WHERE record_type = 'auth' LIMIT 1;"); + $sth->execute(); + + my $import_record_id; + for my $row ($sth->fetchrow_hashref) { + $import_record_id = $row->{'import_record_id'}; + } + + skip 'No authorities in reservoir', 3 unless $import_record_id; + $authority = Koha::Authority->get_from_breeding($import_record_id); + + is(ref($authority), 'Koha::Authority', 'Retrieved valid Koha::Authority object'); + + is($authority->authid, undef, 'Records in reservoir do not have an authid'); + + is(ref($authority->record), 'MARC::Record', 'MARC record attached to authority'); + + $authority = Koha::Authority->get_from_breeding('alphabetsoup'); + is($authority, undef, 'No invalid record is retrieved from reservoir'); +} + done_testing();