@@ -, +, @@ reservoir the "Merge" link for two records. merge-related links in the Manage staged MARC batch screen. identical records (or at least two records that you don't mind merging). resulting record. saved from your catalog. to merge in a new window" link. going through the entire merging process) matches your expectations. 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 just created. resulting record (after going through the entire merging process) matches your expectations. --- Koha/Authority.pm | 60 ++++++- Koha/Util/MARC.pm | 69 ++++++++ authorities/merge.pl | 187 ++++++++++++++++++++ authorities/merge_ajax.pl | 27 +++ .../prog/en/includes/authorities_js.inc | 39 ++++ .../en/modules/authorities/authorities-home.tt | 3 + .../prog/en/modules/authorities/merge.tt | 143 +++++++++++++++ .../en/modules/authorities/searchresultlist.tt | 9 +- .../prog/en/modules/tools/manage-marc-import.tt | 20 ++- t/Koha_Util_MARC.t | 3 +- t/db_dependent/Koha_Authority.t | 26 +++ 11 files changed, 579 insertions(+), 7 deletions(-) create mode 100755 authorities/merge.pl create mode 100755 authorities/merge_ajax.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/authorities_js.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/authorities/merge.tt --- a/Koha/Authority.pm +++ a/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::Record); @@ -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; --- a/Koha/Util/MARC.pm +++ a/Koha/Util/MARC.pm @@ -107,4 +107,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; --- a/authorities/merge.pl +++ a/authorities/merge.pl @@ -0,0 +1,187 @@ +#!/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 Koha::Record; +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.tmpl", + 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); + } + 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 +# ------------------------ --- a/authorities/merge_ajax.pl +++ a/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; --- a/koha-tmpl/intranet-tmpl/prog/en/includes/authorities_js.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/authorities_js.inc @@ -0,0 +1,39 @@ +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()); + }); +}); + --- a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities-home.tt +++ a/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' %] +
+ --- a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/merge.tt +++ a/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' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/searchresultlist.tt +++ a/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 %]
@@ -67,13 +70,14 @@ function searchauthority() { [% UNLESS ( isEDITORS ) %] Used in [% END %] + Merge Delete [% 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 @@ -82,6 +86,7 @@ function searchauthority() { [% resul.used %] biblio(s) [% END %] + Merge [% UNLESS ( resul.used ) %] Delete --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/manage-marc-import.tt +++ a/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"); + }); }); //]]> @@ -339,9 +353,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 %] [% record_lis.status %] [% record_lis.overlay_status %] [% IF ( record_lis.final_match_id ) %] --- a/t/Koha_Util_MARC.t +++ a/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'); @@ -96,3 +96,4 @@ 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'); --- a/t/db_dependent/Koha_Authority.t +++ a/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(); --