Bug 14957: Merge rules system for merging of MARC records

Add a rule based system for merging MARC records to for example
prevent field data from being overwritten.

To test:
    1. Apply this patch.
    2. Log in to staff client.
    3. Enable new syspref MARCMergeRules.
    4. Click the new link "MARC merge rules" in the "Catalog"
       section of the Koha administration page.
    5. Create a new rule:
       Module: source, Filter: *, Tag: 245, Preset: Protect.
    6. Clicking "Edit" should allow you to edit corresponding rule.
    7. Clicking "Delete" should remove corresponding rule after confirmation.
    8. Selecting one or more rules followed by clicking "Delete
       selected" should remove all selected rules after confirmation.
    9. Try creating a rule with tag set to "**", the other options does
       not matter. Verify that saving this rule produces an error
       message complaining about invalid tag regular expression.
    10. Try creating a rule with tag set to "008" (or other control
        field) and set Appended: Append and Removed: Skip, the other
        options does not matter. Verify that saving this rule produces
        an error message complaining about invalid combination of actions
        for control field.
    11. With the 245 rule in step 5 in place, edit a bibliographic record,
        change 245a for example (which should be Title for MARC21) and save.
    12. Verify that the changes has not been saved.
    13. Create a new rule:
        Module: source, Filter: intranet, Tag: 245, Preset: Overwrite.
    14. Repeat step 12, and verify that the changes has now been saved.
    15. Run tests in t/db_dependent/Biblio/MarcMergeRules.t and very
        that all tests pass.

Sponsored-by: Halland County Library
Sponsored-by: Catalyst IT
Sponsored-by: Gothenburg University Library
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Christian Stelzenmüller <christian.stelzenmueller@bsz-bw.de>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
This commit is contained in:
David Gustafsson 2017-02-02 13:36:38 +01:00 committed by Jonathan Druart
parent c0a03d34ca
commit 139e6c30d6
24 changed files with 2164 additions and 15 deletions

View file

@ -61,6 +61,7 @@ BEGIN {
DelBiblio
BiblioAutoLink
LinkBibHeadingsToAuthorities
ApplyMarcMergeRules
TransformMarcToKoha
TransformHtmlToMarc
TransformHtmlToXml
@ -114,6 +115,7 @@ use Koha::SearchEngine;
use Koha::SearchEngine::Indexer;
use Koha::Libraries;
use Koha::Util::MARC;
use Koha::MarcMergeRules;
=head1 NAME
@ -310,7 +312,7 @@ sub AddBiblio {
=head2 ModBiblio
ModBiblio( $record,$biblionumber,$frameworkcode, $disable_autolink);
ModBiblio($record, $biblionumber, $frameworkcode, $options);
Replace an existing bib record identified by C<$biblionumber>
with one supplied by the MARC::Record object C<$record>. The embedded
@ -326,16 +328,32 @@ in the C<biblio> and C<biblioitems> tables, as well as
which fields are used to store embedded item, biblioitem,
and biblionumber data for indexing.
Unless C<$disable_autolink> is passed ModBiblio will relink record headings
The C<$options> argument is a hashref with additional parameters:
=over 4
=item C<context>
This parameter is forwared to L</ApplyMarcMergeRules> where it is used for
selecting the current rule set if Marc Merge Rules is enabled.
See L</ApplyMarcMergeRules> for more details.
=item C<disable_autolink>
Unless C<disable_autolink> is passed ModBiblio will relink record headings
to authorities based on settings in the system preferences. This flag allows
us to not relink records when the authority linker is saving modifications.
=back
Returns 1 on success 0 on failure
=cut
sub ModBiblio {
my ( $record, $biblionumber, $frameworkcode, $disable_autolink ) = @_;
my ( $record, $biblionumber, $frameworkcode, $options ) = @_;
$options //= {};
if (!$record) {
carp 'No record passed to ModBiblio';
return 0;
@ -346,7 +364,7 @@ sub ModBiblio {
logaction( "CATALOGUING", "MODIFY", $biblionumber, "biblio BEFORE=>" . $newrecord->as_formatted );
}
if ( !$disable_autolink && C4::Context->preference('BiblioAddsAuthorities') ) {
if ( !$options->{disable_autolink} && C4::Context->preference('BiblioAddsAuthorities') ) {
BiblioAutoLink( $record, $frameworkcode );
}
@ -367,6 +385,16 @@ sub ModBiblio {
_strip_item_fields($record, $frameworkcode);
# apply merge rules
if (C4::Context->preference('MARCMergeRules') && $biblionumber && defined $options && exists $options->{'context'}) {
$record = ApplyMarcMergeRules({
biblionumber => $biblionumber,
record => $record,
context => $options->{'context'},
}
);
}
# update biblionumber and biblioitemnumber in MARC
# FIXME - this is assuming a 1 to 1 relationship between
# biblios and biblioitems
@ -383,7 +411,7 @@ sub ModBiblio {
_koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
# update the MARC record (that now contains biblio and items) with the new record data
&ModBiblioMarc( $record, $biblionumber );
ModBiblioMarc( $record, $biblionumber );
# modify the other koha tables
_koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
@ -2921,7 +2949,7 @@ sub _koha_delete_biblio_metadata {
=head2 ModBiblioMarc
&ModBiblioMarc($newrec,$biblionumber);
ModBiblioMarc($newrec,$biblionumber);
Add MARC XML data for a biblio to koha
@ -3233,8 +3261,67 @@ sub RemoveAllNsb {
return $record;
}
1;
=head2 ApplyMarcMergeRules
my $record = ApplyMarcMergeRules($params)
Applies marc merge rules to a record.
C<$params> is expected to be a hashref with below keys defined.
=over 4
=item C<biblionumber>
biblionumber of old record
=item C<record>
Incoming record that will be merged with old record
=item C<context>
hashref containing at least one context module and filter value on
the form {module => filter, ...}.
=back
Returns:
=over 4
=item C<$record>
Merged MARC record based with merge rules for C<context> applied. If no old
record for C<biblionumber> can be found, C<record> is returned unchanged.
Default action when no matching context is found to return C<record> unchanged.
If no rules are found for a certain field tag the default is to overwrite with
fields with this field tag from C<record>.
=back
=cut
sub ApplyMarcMergeRules {
my ($params) = @_;
my $biblionumber = $params->{biblionumber};
my $incoming_record = $params->{record};
if (!$biblionumber) {
carp 'ApplyMarcMergeRules called on undefined biblionumber';
return;
}
if (!$incoming_record) {
carp 'ApplyMarcMergeRules called on undefined record';
return;
}
my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
# Skip merge rules if called with no context
if ($old_record && defined $params->{context}) {
return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
}
return $incoming_record;
}
1;
=head2 _after_biblio_action_hooks

View file

@ -675,7 +675,7 @@ sub BatchCommitRecords {
}
$oldxml = $old_marc->as_xml($marc_type);
ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode);
ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode, {context => {source => 'batchimport'}});
$query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?"; # FIXME call SetMatchedBiblionumber instead
if ($item_result eq 'create_new' || $item_result eq 'replace') {

View file

@ -91,7 +91,13 @@ sub process {
my $record = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
C4::MarcModificationTemplates::ModifyRecordWithTemplate( $mmtid, $record );
my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
C4::Biblio::ModBiblio( $record, $biblionumber, $frameworkcode );
C4::Biblio::ModBiblio( $record, $biblionumber, $frameworkcode,
{
source => $args->{source},
categorycode => $args->{categorycode},
userid => $args->{userid},
}
);
};
if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
push @messages, {

View file

@ -0,0 +1,55 @@
package Koha::Exceptions::MarcMergeRule;
# 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 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Exception::Class (
'Koha::Exceptions::MarcMergeRule' => {
description => 'Something went wrong!',
},
'Koha::Exceptions::MarcMergeRule::InvalidTagRegExp' => {
isa => 'Koha::Exceptions::MarcMergeRule',
description => 'Invalid regular expression for tag'
},
'Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions' => {
isa => 'Koha::Exceptions::MarcMergeRule',
description => 'Invalid control field actions'
}
);
=head1 NAME
Koha::Exceptions::MarcMergeRule - Base class for MarcMergeRule exceptions
=head1 Exceptions
=head2 Koha::Exceptions::MarcMergeRule
Generic MarcMergeRule exception
=head2 Koha::Exceptions::MarcMergeRule::InvalidTagRegExp
Exception for rule validation when rule tag is an invalid regular expression
=head2 Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions
Exception for rule validation for control field rules with invalid combination of actions
=cut
1;

58
Koha/MarcMergeRule.pm Normal file
View file

@ -0,0 +1,58 @@
package Koha::MarcMergeRule;
# 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 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use parent qw(Koha::Object);
my $cache = Koha::Caches->get_instance();
=head1 NAME
Koha::MarcMergeRule - Koha MarcMergeRule Object class
=cut
=head2 store
Override C<store> to clear marc merge rules cache.
=cut
sub store {
my $self = shift @_;
$cache->clear_from_cache('marc_merge_rules');
$self->SUPER::store(@_);
}
=head2 delete
Override C<delete> to clear marc merge rules cache.
=cut
sub delete {
my $self = shift @_;
$cache->clear_from_cache('marc_merge_rules');
$self->SUPER::delete(@_);
}
sub _type {
return 'MarcMergeRule';
}
1;

381
Koha/MarcMergeRules.pm Normal file
View file

@ -0,0 +1,381 @@
package Koha::MarcMergeRules;
# 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 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use List::Util qw(first);
use Koha::MarcMergeRule;
use Carp;
use Koha::Exceptions::MarcMergeRule;
use Try::Tiny;
use Scalar::Util qw(looks_like_number);
use parent qw(Koha::Objects);
my $cache = Koha::Caches->get_instance();
=head1 NAME
Koha::MarcMergeRules - Koha MarcMergeRules Object set class
=head1 API
=head2 Class Methods
=head3 operations
Returns a list of all valid operations.
=cut
sub operations {
return ('add', 'append', 'remove', 'delete');
}
=head3 context_rules
my $rules = Koha::MarcMergeRules->context_rules($context);
Gets all MARC merge rules for the supplied C<$context> (hashref with { module => filter, ... } values).
=cut
sub context_rules {
my ($self, $context) = @_;
return unless %{$context};
my $rules = $cache->get_from_cache('marc_merge_rules', { unsafe => 1 });
if (!$rules) {
$rules = {};
my @rules_rows = $self->_resultset()->search(
undef,
{
order_by => { -desc => [qw/id/] }
}
);
foreach my $rule_row (@rules_rows) {
my %rule = $rule_row->get_columns();
my $operations = {};
foreach my $operation ($self->operations) {
$operations->{$operation} = { allow => $rule{$operation}, rule => $rule{id} };
}
# TODO: Remove unless check and validate on saving rules?
if ($rule{tag} eq '*') {
unless (exists $rules->{$rule{module}}->{$rule{filter}}->{'*'}) {
$rules->{$rule{module}}->{$rule{filter}}->{'*'} = $operations;
}
}
elsif ($rule{tag} =~ /^(\d{3})$/) {
unless (exists $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}}) {
$rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}} = $operations;
}
}
else {
my $regexps = ($rules->{$rule{module}}->{$rule{filter}}->{regexps} //= []);
push @{$regexps}, [$rule{tag}, $operations];
}
}
$cache->set_in_cache('marc_merge_rules', $rules);
}
my $context_rules = undef;
foreach my $module_name (keys %{$context}) {
if (
exists $rules->{$module_name} &&
exists $rules->{$module_name}->{$context->{$module_name}}
) {
$context_rules = $rules->{$module_name}->{$context->{$module_name}};
last;
}
}
if (!$context_rules) {
# No perms matching specific context conditions found, try wildcard value for each active context
foreach my $module_name (keys %{$context}) {
if (exists $rules->{$module_name}->{'*'}) {
$context_rules = $rules->{$module_name}->{'*'};
last;
}
}
}
return $context_rules;
}
=head3 merge_records
my $merged_record = Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $context);
Merge C<$old_record> with C<$incoming_record> applying merge rules for C<$context>.
Returns merged record C<$merged_record>. C<$old_record>, C<$incoming_record> and
C<$merged_record> are all MARC::Record objects.
=cut
sub merge_records {
my ($self, $old_record, $incoming_record, $context) = @_;
my $rules = $self->context_rules($context);
# Default when no rules found is to overwrite with incoming record
return $incoming_record unless $rules;
my $fields_by_tag = sub {
my ($record) = @_;
my $fields = {};
foreach my $field ($record->fields()) {
$fields->{$field->tag()} //= [];
push @{$fields->{$field->tag()}}, $field;
}
return $fields;
};
my $hash_field_data = sub {
my ($field) = @_;
my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields());
};
my $diff_by_key = sub {
my ($a, $b) = @_;
my @removed;
my @intersecting;
my @added;
my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
foreach my $key (keys %keys_index) {
if ($a->{$key} && $b->{$key}) {
push @intersecting, $a->{$key};
}
elsif ($a->{$key}) {
push @removed, $a->{$key};
}
else {
push @added, $b->{$key};
}
}
return (\@removed, \@intersecting, \@added);
};
my $tag_rules = $rules->{tags} // {};
my $default_rule = $rules->{'*'} // {
add => { allow => 1, 'rule' => 0},
append => { allow => 1, 'rule' => 0},
delete => { allow => 1, 'rule' => 0},
remove => { allow => 1, 'rule' => 0},
};
# Precompile regexps
my @regexp_rules = map { { regexp => qr/^$_->[0]$/, actions => $_->[1] } } @{$rules->{regexps} // []};
my $get_matching_field_rule = sub {
my ($tag) = @_;
# Exact match takes precedence, then regexp, then wildcard/defaults
return $tag_rules->{$tag} //
%{(first { $tag =~ $_->{regexp} } @regexp_rules) // {}}{actions} //
$default_rule;
};
my %merged_record_fields;
my $current_fields = $fields_by_tag->($old_record);
my $incoming_fields = $fields_by_tag->($incoming_record);
# First we get all new incoming fields
my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
foreach my $tag (@new_field_tags) {
my $rule = $get_matching_field_rule->($tag);
if ($rule->{add}->{allow}) {
$merged_record_fields{$tag} //= [];
push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
}
}
# Then we get all fields no longer present in incoming fields
my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
foreach my $tag (@deleted_field_tags) {
my $rule = $get_matching_field_rule->($tag);
if (!$rule->{delete}->{allow}) {
$merged_record_fields{$tag} //= [];
push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
}
}
# Then we get the intersection of fields, present both in
# current and incoming record (possibly to be overwritten)
my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
foreach my $tag (@common_field_tags) {
my $rule = $get_matching_field_rule->($tag);
# Special handling for control fields
if ($tag < 10) {
if (
$rule->{append}->{allow} &&
!$rule->{remove}->{allow}
) {
# This should be highly unlikely since we have input validation to protect against this case
carp "Allowing \"append\" and skipping \"remove\" is not permitted for control fields, falling back to skipping both \"append\" and \"remove\"";
push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
}
elsif ($rule->{append}->{allow}) {
push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
}
else {
push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
}
}
else {
# Compute intersection and diff using field data
my $sort_weight = 0;
my %current_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$current_fields->{$tag}};
# Always put incoming fields after current fields
my %incoming_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$incoming_fields->{$tag}};
my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
my @merged_fields;
# First add common fields (intersection)
# Unchanged
if (@{$common_fields}) {
push @merged_fields, @{$common_fields};
}
# Removed
if (@{$current_fields_only}) {
if (!$rule->{remove}->{allow}) {
push @merged_fields, @{$current_fields_only};
}
}
# Appended
if (@{$incoming_fields_only}) {
if ($rule->{append}->{allow}) {
push @merged_fields, @{$incoming_fields_only};
}
}
$merged_record_fields{$tag} //= [];
# Sort ascending according to weight (original order)
push @{$merged_record_fields{$tag}}, map { $_->[1] } sort { $a->[0] <=> $b->[0] } @merged_fields;
}
}
my $merged_record = MARC::Record->new();
# Leader is always overwritten, or kept???
$merged_record->leader($incoming_record->leader());
if (%merged_record_fields) {
foreach my $tag (sort keys %merged_record_fields) {
$merged_record->append_fields(@{$merged_record_fields{$tag}});
}
}
return $merged_record;
}
sub _clear_caches {
$cache->clear_from_cache('marc_merge_rules');
}
=head2 find_or_create
Override C<find_or_create> to clear marc merge rules cache.
=cut
sub find_or_create {
my $self = shift @_;
$self->_clear_caches();
return $self->SUPER::find_or_create(@_);
}
=head2 update
Override C<update> to clear marc merge rules cache.
=cut
sub update {
my $self = shift @_;
$self->_clear_caches();
return $self->SUPER::update(@_);
}
=head2 delete
Override C<delete> to clear marc merge rules cache.
=cut
sub delete {
my $self = shift @_;
$self->_clear_caches();
return $self->SUPER::delete(@_);
}
=head2 validate
Koha::MarcMergeRules->validate($rule_data);
Validates C<$rule_data>. Throws C<Koha::Exceptions::MarcMergeRule::InvalidTagRegExp>
if C<$rule_data->{tag}> contains an invalid regular expression. Throws
C<Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions> if contains invalid
combination of actions for control fields. Otherwise returns true.
=cut
sub validate {
my ($self, $rule_data) = @_;
if(exists $rule_data->{tag}) {
if ($rule_data->{tag} ne '*') {
eval { qr/$rule_data->{tag}/ };
if ($@) {
Koha::Exceptions::MarcMergeRule::InvalidTagRegExp->throw(
"Invalid tag regular expression"
);
}
}
# TODO: Regexp or '*' that match controlfield not currently detected
if (
looks_like_number($rule_data->{tag}) &&
$rule_data->{tag} < 10 &&
$rule_data->{append} &&
!$rule_data->{remove}
) {
Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions->throw(
"Combination of allow append and skip remove not permitted for control fields"
);
}
}
return 1;
}
sub _type {
return 'MarcMergeRule';
}
=head3 object_class
=cut
sub object_class {
return 'Koha::MarcMergeRule';
}
1;

162
admin/marc-merge-rules.pl Executable file
View file

@ -0,0 +1,162 @@
#!/usr/bin/perl
# 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 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
# standard or CPAN modules used
use CGI qw ( -utf8 );
use CGI::Cookie;
use MARC::File::USMARC;
use Try::Tiny;
# Koha modules used
use C4::Context;
use C4::Koha;
use C4::Auth;
use C4::AuthoritiesMarc;
use C4::Output;
use C4::Biblio;
use C4::ImportBatch;
use C4::Matcher;
use C4::BackgroundJob;
use C4::Labels::Batch;
use Koha::MarcMergeRules;
use Koha::MarcMergeRule;
use Koha::Patron::Categories; # TODO: Required? Try without use
my $script_name = "/cgi-bin/koha/admin/marc-merge-rules.pl";
my $input = new CGI;
my $op = $input->param('op') || '';
my $errors = [];
my $rule_from_cgi = sub {
my ($cgi) = @_;
my %rule = map { $_ => scalar $cgi->param($_) } (
'tag',
'module',
'filter',
'add',
'append',
'remove',
'delete'
);
my $id = $cgi->param('id');
if ($id) {
$rule{id} = $id;
}
return \%rule;
};
my ($template, $loggedinuser, $cookie) = get_template_and_user(
{
template_name => "admin/marc-merge-rules.tt",
query => $input,
type => "intranet",
authnotrequired => 0,
flagsrequired => { parameters => 'manage_marc_merge_rules' },
debug => 1,
}
);
$template->param(script_name => $script_name);
my %cookies = parse CGI::Cookie($cookie);
our $sessionID = $cookies{'CGISESSID'}->value;
my $get_rules = sub {
# TODO: order?
return [map { { $_->get_columns() } } Koha::MarcMergeRules->_resultset->all];
};
my $rules;
if ($op eq 'remove' || $op eq 'doremove') {
my @remove_ids = $input->multi_param('batchremove');
push @remove_ids, scalar $input->param('id') if $input->param('id');
if ($op eq 'remove') {
$template->{VARS}->{removeConfirm} = 1;
my %remove_ids = map { $_ => undef } @remove_ids;
$rules = $get_rules->();
for my $rule (@{$rules}) {
$rule->{'removemarked'} = 1 if exists $remove_ids{$rule->{id}};
}
}
elsif ($op eq 'doremove') {
my @remove_ids = $input->multi_param('batchremove');
push @remove_ids, scalar $input->param('id') if $input->param('id');
Koha::MarcMergeRules->search({ id => { in => \@remove_ids } })->delete();
$rules = $get_rules->();
}
}
elsif ($op eq 'edit') {
$template->{VARS}->{edit} = 1;
my $id = $input->param('id');
$rules = $get_rules->();
for my $rule(@{$rules}) {
if ($rule->{id} == $id) {
$rule->{'edit'} = 1;
last;
}
}
}
elsif ($op eq 'doedit' || $op eq 'add') {
my $rule_data = $rule_from_cgi->($input);
if (!@{$errors}) {
try {
Koha::MarcMergeRules->validate($rule_data);
}
catch {
die $_ unless blessed $_ && $_->can('rethrow');
if ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp')) {
push @{$errors}, {
type => 'error',
code => 'invalid_tag_regexp',
tag => $rule_data->{tag},
};
}
elsif ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions')) {
push @{$errors}, {
type => 'error',
code => 'invalid_control_field_actions',
tag => $rule_data->{tag},
};
}
else {
$_->rethrow;
}
};
if (!@{$errors}) {
my $rule = Koha::MarcMergeRules->find_or_create($rule_data);
# Need to call set and store here in case we have an update
$rule->set($rule_data);
$rule->store();
}
$rules = $get_rules->();
}
}
else {
$rules = $get_rules->();
}
my $categorycodes = Koha::Patron::Categories->search_limited({}, {order_by => ['description']});
$template->param( rules => $rules, categorycodes => $categorycodes, messages => $errors );
output_html_with_http_headers $input, $cookie, $template->output;

View file

@ -51,6 +51,7 @@ use Koha::ItemTypes;
use Koha::Libraries;
use Koha::BiblioFrameworks;
use Koha::Patrons;
use MARC::File::USMARC;
use MARC::File::XML;
@ -864,7 +865,15 @@ if ( $op eq "addbiblio" ) {
if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
my $oldbibitemnum;
if ( $is_a_modif ) {
ModBiblio( $record, $biblionumber, $frameworkcode );
my $member = Koha::Patrons->find($loggedinuser);
ModBiblio( $record, $biblionumber, $frameworkcode, {
context => {
source => $z3950 ? 'z39.50' : 'intranet',
categorycode => $member->categorycode,
userid => $member->userid
}
}
);
}
else {
( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
@ -957,6 +966,7 @@ elsif ( $op eq "delete" ) {
$template->param(
biblionumberdata => $biblionumber,
op => $op,
z3950 => $z3950
);
if ( $op eq "duplicate" ) {
$biblionumber = "";

View file

@ -0,0 +1,41 @@
$DBversion = 'XXX'; # will be replaced by the RM
if( CheckVersion( $DBversion ) ) {
my $sql = q{
CREATE TABLE IF NOT EXISTS `marc_merge_rules` (
`id` int(11) NOT NULL auto_increment,
`tag` varchar(255) NOT NULL,
`module` varchar(127) NOT NULL,
`filter` varchar(255) NOT NULL,
`add` tinyint NOT NULL,
`append` tinyint NOT NULL,
`remove` tinyint NOT NULL,
`delete` tinyint NOT NULL,
PRIMARY KEY(`id`)
);
};
$dbh->do( $sql );
$sql = q{
INSERT IGNORE INTO systempreferences (`variable`, `value`, `options`, `explanation`, `type`) VALUES (
'MARCMergeRules',
'0',
NULL,
'Use the MARC merge rules system to decide what actions to take for each field when modifying records.',
'YesNo'
);
};
$dbh->do( $sql );
$sql = q{
INSERT IGNORE INTO permissions (module_bit, code, description) VALUES (
3,
'manage_marc_merge_rules',
'Manage MARC merge rules configuration'
);
};
$dbh->do( $sql );
# Always end with this (adjust the bug info)
SetVersion( $DBversion );
print "Upgrade to $DBversion done (Bug 14957 - Write protecting MARC fields based on source of import)\n";
}

View file

@ -3385,6 +3385,36 @@ CREATE TABLE `marc_matchers` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `marc_merge_rules_modules`
--
DROP TABLE IF EXISTS `marc_merge_rules_modules`;
CREATE TABLE `marc_merge_rules_modules` (
`name` varchar(127) NOT NULL,
`description` varchar(255),
`specificity` int(11) NOT NULL UNIQUE,
PRIMARY KEY(`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `marc_merge_rules`
--
DROP TABLE IF EXISTS `marc_merge_rules`;
CREATE TABLE IF NOT EXISTS `marc_merge_rules` (
`id` int(11) NOT NULL auto_increment,
`tag` varchar(255) NOT NULL, -- can be regexp, so need > 3 chars
`module` varchar(127) NOT NULL,
`filter` varchar(255) NOT NULL,
`add` tinyint NOT NULL,
`append` tinyint NOT NULL,
`remove` tinyint NOT NULL,
`delete` tinyint NOT NULL,
PRIMARY KEY(`id`),
CONSTRAINT `marc_merge_rules_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_merge_rules_modules` (`name`) ON DELETE CASCADE ON UPDATE CASCADE
);
--
-- Table structure for table `marc_modification_template_actions`
--

View file

@ -331,6 +331,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
('MarcFieldForModifierName','',NULL,'Where to store the name of the record''s last modifier','Free'),
('MarcFieldsToOrder','',NULL,'Set the mapping values for a new order line created from a MARC record in a staged file. In a YAML format.','textarea'),
('MarcItemFieldsToOrder','',NULL,'Set the mapping values for new item records created from a MARC record in a staged file. In a YAML format.','textarea'),
('MARCMergeRules','0',NULL,'Use the MARC merge rules system to decide what actions to take for each field when modifying records.','YesNo'),
('MarkLostItemsAsReturned','batchmod,moredetail,cronjob,additem,pendingreserves,onpayment','claim_returned|batchmod|moredetail|cronjob|additem|pendingreserves|onpayment','Mark items as returned when flagged as lost','multiple'),
('MARCOrgCode','OSt','','Define MARC Organization Code for MARC21 records - http://www.loc.gov/marc/organizations/orgshome.html','free'),
('MaxFine',NULL,'','Maximum fine a patron can have for all late returns at one moment. Single item caps are specified in the circulation rules matrix.','Integer'),

View file

@ -25,6 +25,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
( 3, 'manage_oai_sets', 'Manage OAI sets'),
( 3, 'manage_item_search_fields', 'Manage item search fields'),
( 3, 'manage_search_engine_config', 'Manage search engine configuration'),
( 3, 'manage_marc_merge_rules', 'Manage MARC merge rules configuration'),
( 3, 'manage_search_targets', 'Manage Z39.50 and SRU server configuration'),
( 3, 'manage_didyoumean', 'Manage Did you mean? configuration'),
( 3, 'manage_column_config', 'Manage column configuration'),

View file

@ -100,6 +100,9 @@
[% IF ( CAN_user_parameters_manage_search_engine_config ) %]
<li><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_marc_merge_rules ) %]
<li><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></li>
[% END %]
</ul>
[% END %]

View file

@ -210,6 +210,11 @@
Manage search engine configuration
</span>
<span class="permissioncode">([% name | html %])</span>
[%- CASE 'manage_marc_merge_rules' -%]
<span class="sub_permission manage_marc_merge_rules_subpermission">
Manage MARC merge rules configuration
</span>
<span class="permissioncode">([% name | html %])</span>
[%- CASE 'manage_search_targets' -%]
<span class="sub_permission manage_search_targets_subpermission">
Manage Z39.50 and SRU server configuration

View file

@ -192,6 +192,8 @@
<dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></dt>
<dd>Manage indexes, facets, and their mappings to MARC fields and subfields</dd>
[% END %]
<dt><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></dt>
<dd>Managed MARC field merge rules</dd>
</dl>
[% END %]

View file

@ -0,0 +1,519 @@
[% USE Koha %]
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Administration &rsaquo; MARC merge rules</title>
[% INCLUDE 'doc-head-close.inc' %]
[% Asset.css("css/datatables.css") %]
[% INCLUDE 'datatables.inc' %]
<style type="text/css">
.required {
background-color: #C00;
}
</style>
<script type="text/javascript">
function doSubmit(op, id) {
$('<input type="hidden"/>')
.attr('name', 'op')
.attr('value', op)
.appendTo('#marc-merge-rules-form');
if(id) {
$('<input type="hidden"/>')
.attr('name', 'id')
.attr('value', id)
.appendTo('#marc-merge-rules-form');
}
var valid = true;
if (op == 'add' || op == 'edit') {
var validate = [
$('#marc-merge-rules-form input[name="filter"]'),
$('#marc-merge-rules-form input[name="tag"]')
];
for(var i = 0; i < validate.length; i++) {
if (validate[i].length) {
if(validate[i].val().length == 0) {
validate[i].addClass('required');
valid = false;
} else {
validate[i].removeClass('required');
}
}
}
}
if (valid) {
$('#marc-merge-rules-form').submit();
}
return valid;
}
$(document).ready(function(){
$('#doremove').on('click', function(){
doSubmit('doremove');
});
$('#doedit').on('click', function(){
doSubmit('doedit', $("#doedit").attr('value'));
});
$('#add').on('click', function(){
doSubmit('add');
return false;
});
$('#btn_batchremove').on('click', function(){
doSubmit('remove');
});
/* Disable batch remove unless one or more checkboxes are checked */
$('input[name="batchremove"]').change(function() {
if($('input[name="batchremove"]:checked').length > 0) {
$('#btn_batchremove').removeAttr('disabled');
} else {
$('#btn_batchremove').attr('disabled', 'disabled');
}
});
$.fn.dataTable.ext.order['dom-input'] = function (settings, col) {
return this.api().column(col, { order: 'index' }).nodes()
.map(function (td, i) {
if($('input', td).val() != undefined) {
return $('input', td).val();
} else if($('select', td).val() != undefined) {
return $('option[selected="selected"]', td).val();
} else {
return $(td).html();
}
});
}
$('#marc-merge-rules').dataTable($.extend(true, {}, dataTablesDefaults, {
"aoColumns": [
{"bSearchable": false, "bSortable": false},
{"sSortDataType": "dom-input"},
{"sSortDataType": "dom-input"},
{"bSearchable": false, "sSortDataType": "dom-input"},
{"bSearchable": false, "sSortDataType": "dom-input"},
{"bSearchable": false, "sSortDataType": "dom-input"},
{"bSearchable": false, "sSortDataType": "dom-input"},
{"bSearchable": false, "sSortDataType": "dom-input"},
{"bSearchable": false, "sSortDataType": "dom-input"},
{"bSearchable": false, "bSortable": false},
{"bSearchable": false, "bSortable": false}
],
"pagingType": "simple"
}));
var merge_rules_presets = {};
merge_rules_presets[_("Protect")] = {
'add': 0,
'append': 0,
'remove': 0,
'delete': 0
};
merge_rules_presets[_("Overwrite")] = {
'add': 1,
'append': 1,
'remove': 1,
'delete': 1
};
merge_rules_presets[_("Add new")] = {
'add': 1,
'append': 0,
'remove': 0,
'delete': 0
};
merge_rules_presets[_("Add and append")] = {
'add': 1,
'append': 1,
'remove': 0,
'delete': 0
};
merge_rules_presets[_("Protect from deletion")] = {
'add': 1,
'append': 1,
'remove': 1,
'delete': 0
};
var merge_rules_label_to_value = {};
merge_rules_label_to_value[_("Add")] = 1;
merge_rules_label_to_value[_("Append")] = 1;
merge_rules_label_to_value[_("Remove")] = 1;
merge_rules_label_to_value[_("Delete")] = 1;
merge_rules_label_to_value[_("Skip")] = 0;
function hash_config(config) {
return JSON.stringify(config, Object.keys(config).sort());
}
var merge_rules_preset_map = {};
$.each(merge_rules_presets, function(preset, config) {
merge_rules_preset_map[hash_config(config)] = preset;
});
function operations_config_merge_rule_preset(config) {
return merge_rules_preset_map[hash_config(config)] || '';
}
/* Set preset values according to operation config */
$('.rule').each(function() {
var $this = $(this);
var operations_config = {};
$('.rule-operation-action', $this).each(function() {
var $operation = $(this);
operations_config[$operation.data('operation')] = merge_rules_label_to_value[$operation.text()];
});
$('.rule-preset', $this).text(
operations_config_merge_rule_preset(operations_config) || _("Custom")
);
});
/* Listen to operations config changes and set presets accordingly */
$('.rule-operation-action-edit select').change(function() {
var operations_config = {};
var $parent_row = $(this).closest('tr');
$('.rule-operation-action-edit select', $parent_row).each(function() {
var $this = $(this);
operations_config[$this.attr('name')] = parseInt($this.val());
});
$('select[name="preset"]', $parent_row).val(
operations_config_merge_rule_preset(operations_config)
);
});
/* Listen to preset changes and set operations config accordingly */
$('select[name="preset"]').change(function() {
var $this = $(this);
var $parent_row = $this.closest('tr');
var preset = $this.val();
if (preset) {
$.each(merge_rules_presets[preset], function(operation, action) {
$('select[name="' + operation + '"]', $parent_row).val(action);
});
}
});
var module_filter_options = {
source: {
'*': '*',
batchmod: "Batch record modification",
intranet: "Staff client MARC editor",
batchimport: "Staged MARC import",
z3950: "Z39.50",
bulkmarcimport: "bulkmarcimport.pl",
import_lexile: "import_lexile.pl"
},
categorycode: {
'*': '*',
[% FOREACH categorycode IN categorycodes %]
[% categorycode.categorycode | html %]: "[% categorycode.description | html %]",
[% END %]
}
};
//Kind of hack: Replace filter value with label when one exist
$('.rule-module').each(function() {
var $this = $(this);
var module = $this.text();
if (module in module_filter_options) {
let $filter = $this.siblings('.rule-filter');
if ($filter.text() in module_filter_options[module]) {
$filter.text(module_filter_options[module][$filter.text()]);
}
}
});
var $filter_container = $('#filter-container');
/* Listen to module changes and set filter input accordingly */
$('select[name="module"]').change(function() {
var $this = $(this);
var module_name = $this.val();
/* Remove current element if any */
$filter_container.empty();
var filter_elem = null;
if (module_name in module_filter_options) {
// Create select element
filter_elem = document.createElement('select');
for (var filter_value in module_filter_options[module_name]) {
var option = document.createElement('option');
option.value = filter_value;
option.text = module_filter_options[module_name][filter_value];
filter_elem.appendChild(option);
}
}
else {
// Create text input element
filter_elem = document.createElement('input');
filter_elem.type = 'text';
filter_elem.setAttribute('size', 5);
}
filter_elem.name = 'filter';
filter_elem.id = 'filter';
$filter_container.append(filter_elem);
}).change(); // Trigger change
// Hack: set value if editing rule
if ($filter_container.data('filter')) {
$('#filter').val($filter_container.data('filter'));
}
});
</script>
</head>
<body id="admin_marc-merge-rules" class="admin">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
&rsaquo; MARC merge rules
</div>
<div class="main container-fluid">
<div class="row">
<div class="col-sm-10 col-sm-push-2">
<h1>Manage MARC merge rules</h1>
[% FOR m IN messages %]
<div class="dialog [% m.type | html %]">
[% SWITCH m.code %]
[% CASE 'invalid_tag_regexp' %]
Invalid regular expression "[% m.tag | html %]".
[% CASE 'invalid_control_field_actions' %]
Invalid combination of actions for tag [% m.tag | html %]. Control field rules do not allow "Appended: Append" and "Removed: Skip".
[% CASE %]
[% m.code | html %]
[% END %]
</div>
[% END %]
[% UNLESS Koha.Preference( 'MARCMergeRules' ) %]
<div class="dialog message">
The <b>MARCMergeRules</b> preference is not set, don't forget to enable it for rules to take effect.
</div>
[% END %]
[% IF removeConfirm %]
<div class="dialog alert">
<h3>Remove rule?</h3>
<p>Are you sure you want to remove the selected rule(s)?</p>
<form action="[% script_name %]" method="GET">
<button type="submit" class="deny"><i class="fa fa-fw fa-remove"></i> No, do not remove</button>
</form>
<button type="button" class="approve" id="doremove"><i class="fa fa-fw fa-check"></i> Yes, remove</button>
</div>
[% END %]
<form action="[% script_name %]" method="POST" id="marc-merge-rules-form">
<table id="marc-merge-rules">
<thead><tr>
<th>Rule</th>
<th>Module</th>
<th>Filter</th>
<th>Tag</th>
<th>Preset</th>
<th>Added</th>
<th>Appended</th>
<th>Removed</th>
<th>Deleted</th>
<th>Actions</th>
<th>&nbsp;</th>
</tr></thead>
[% UNLESS edit %]
<tfoot>
<tr class="rule-new">
<th>&nbsp;</th>
<th>
<select name="module">
<option value="source">Source</option>
<option value="categorycode">User category</option>
<option value="userid">Username</option>
</select>
</th>
<th id="filter-container"></th>
<th><input type="text" size="5" name="tag"/></th>
<th>
<select name="preset">
<option value="" selected>Custom</option>
[% FOR preset IN [
'Protect',
'Overwrite',
'Add new',
'Add and append',
'Protect from deletion'
]
%]
<option value="[% preset | html %]">[% preset | html %]</option>
[% END %]
</select>
</th>
<th class="rule-operation-action-edit">
<select name="add">
<option value="0">Skip</option>
<option value="1">Add</option>
</select>
</th>
<th class="rule-operation-action-edit">
<select name="append">
<option value="0">Skip</option>
<option value="1">Append</option>
</select>
</th>
<th class="rule-operation-action-edit">
<select name="remove">
<option value="0">Skip</option>
<option value="1">Remove</option>
</select>
</th>
<th class="rule-operation-action-edit">
<select name="delete">
<option value="0">Skip</option>
<option value="1">Delete</option>
</select>
</th>
<th><button class="btn btn-sm" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
<th><button id="btn_batchremove" disabled="disabled" class="btn btn-sm" title="Batch remove"><i class="fa fa-trash"></i> Delete selected</button></th>
</tr>
</tfoot>
[% END %]
<tbody>
[% FOREACH rule IN rules %]
<tr id="[% rule.id %]" class="rule[% IF rule.edit %]-edit[% END %]">
[% IF rule.edit %]
<td>[% rule.id %]</td>
<td>
<select name="module">
[% IF rule.module == "source" %]
<option value="source" selected="selected">Source</option>
[% ELSE %]
<option value="source">Source</option>
[% END %]
[% IF rule.module == "categorycode" %]
<option value="categorycode" selected="selected">User category</option>
[% ELSE %]
<option value="categorycode">User category</option>
[% END %]
[% IF rule.module == "userid" %]
<option value="userid" selected="selected">Username</option>
[% ELSE %]
<option value="userid">Username</option>
[% END %]
</select>
</td>
<td id="filter-container" data-filter="[% rule.filter | html %]"></td>
<td><input type="text" size="3" name="tag" value="[% rule.tag | html %]"/></td>
<th>
<select name="preset">
<option value="" selected>Custom</option>
[% FOR preset IN [
'Protect',
'Overwrite',
'Add new',
'Add and append',
'Protect from deletion'
]
%]
<option value="[% preset | html %]">[% preset | html %]</option>
[% END %]
</select>
</th>
<td class="rule-operation-action-edit">
<select name="add">
[% IF rule.add %]
<option value="0">Skip</option>
<option value="1" selected="selected">Add</option>
[% ELSE %]
<option value="0" selected="selected">Skip</option>
<option value="1">Add</option>
[% END %]
</select>
</td>
<td class="rule-operation-action-edit">
<select name="append">
[% IF rule.append %]
<option value="0">Skip</option>
<option value="1" selected="selected">Append</option>
[% ELSE %]
<option value="0" selected="selected">Skip</option>
<option value="1">Append</option>
[% END %]
</select>
</td>
<td class="rule-operation-action-edit">
<select name="remove">
[% IF rule.remove %]
<option value="0">Skip</option>
<option value="1" selected="selected">Remove</option>
[% ELSE %]
<option value="0" selected="selected">Skip</option>
<option value="1">Remove</option>
[% END %]
</select>
</td>
<td class="rule-operation-action-edit">
<select name="delete">
[% IF rule.delete %]
<option value="0">Skip</option>
<option value="1" selected="selected">Delete</option>
[% ELSE %]
<option value="0" selected="selected">Skip</option>
<option value="1">Delete</option>
[% END %]
</select>
</td>
<td class="actions">
<button class="btn btn-sm" title="Save" id="doedit" value="[% rule.id | html %]"><i class="fa fa-check"></i> Save</button>
<button type="submit" class="btn btn-sm" title="Cancel" ><i class="fa fa-times"></i> Cancel</button>
</td>
<td></td>
[% ELSE %]
<td>[% rule.id | html %]</td>
<td class="rule-module">[% rule.module | html %]</td>
<td class="rule-filter">[% rule.filter | html %]</td>
<td>[% rule.tag | html %]</td>
<td class="rule-preset"></td>
<td class="rule-operation-action" data-operation="add">[% IF rule.add %]Add[% ELSE %]Skip[% END %]</td>
<td class="rule-operation-action" data-operation="append">[% IF rule.append %]Append[% ELSE %]Skip[% END %]</td>
<td class="rule-operation-action" data-operation="remove">[% IF rule.remove %]Remove[% ELSE %]Skip[% END %]</td>
<td class="rule-operation-action" data-operation="delete">[% IF rule.delete %]Delete[% ELSE %]Skip[% END %]</td>
<td class="actions">
<a href="?op=remove&id=[% rule.id %]" title="Delete" class="btn btn-sm"><i class="fa fa-trash"></i> Delete</a>
<a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-sm"><i class="fa fa-pencil"></i> Edit</a>
</td>
<td>
[% IF rule.removemarked %]
<input type="checkbox" name="batchremove" value="[% rule.id | html %]" checked="checked"/>
[% ELSE %]
<input type="checkbox" name="batchremove" value="[% rule.id | html %]"/>
[% END %]
</td>
[% END %]
</tr>
[% END %]
</tbody>
</table>
</form>
<form action="[% script_name %]" method="post">
<input type="hidden" name="op" value="redo-matching" />
</form>
</div>
<!-- /.col-sm-10.col-sm-push-2 -->
<div class="col-sm-2 col-sm-pull-10">
<aside>
[% INCLUDE 'admin-menu.inc' %]
</aside>
</div>
</div>
<!-- /.row>
</div>
<!-- /main container-fluid -->
[% INCLUDE 'intranet-bottom.inc' %]

View file

@ -325,3 +325,10 @@ Cataloging:
- "All values of repeating tags and subfields will be printed with the given RIS tag."
- "<br/>"
- "Use of TY ( record type ) as a key will <em>replace</em> the default TY with the field value of your choosing."
-
- When importing records
- pref: MARCMergeRules
choices:
yes: "use"
no: "don't use"
- MARC merge rules to decide which action to take for each field.

View file

@ -925,6 +925,7 @@ function PopupMARCFieldDoc(field) {
[% END %]
<input type="hidden" name="op" value="addbiblio" />
<input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
<input type="hidden" name="z3950" value="[% z3950 | html %]" />
<input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
<input type="hidden" name="breedingid" value="[% breedingid | html %]" />
<input type="hidden" name="changed_framework" value="" />

View file

@ -233,7 +233,7 @@ sub process_bib {
);
}
if ( not $test_only ) {
ModBiblio( $bib, $biblionumber, $frameworkcode, 1 );
ModBiblio( $bib, $biblionumber, $frameworkcode, { disable_autolink => 1 });
#Last param is to note ModBiblio was called from linking script and bib should not be linked again
$num_bibs_modified++;
}

View file

@ -450,7 +450,7 @@ RECORD: while ( ) {
$biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
};
if ($update) {
eval { ModBiblio( $record, $biblionumber, $framework ) };
eval { ModBiblio( $record, $biblionumber, $framework, { context => { source => 'bulkmarcimport' } } ) };
if ($@) {
warn "ERROR: Edit biblio $biblionumber failed: $@\n";
printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);

View file

@ -203,7 +203,7 @@ while ( my $row = $csv->getline_hr($fh) ) {
$record->append_fields($field);
}
ModBiblio( $record, $biblionumber ) unless ( $test );
ModBiblio( $record, $biblionumber, undef, { context => { source => 'import_lexile' } } ) unless ( $test );
}
}

View file

@ -731,7 +731,7 @@ subtest 'ModBiblio called from linker test' => sub {
C4::Biblio::ModBiblio($record,$biblionumber,'');
is($called,1,"We called to link bibs because not from linker");
$called = 0;
C4::Biblio::ModBiblio($record,$biblionumber,'',1);
C4::Biblio::ModBiblio($record,$biblionumber,'',{ disable_autolink => 1 });
is($called,0,"We didn't call to link bibs because from linker");
};

View file

@ -0,0 +1,779 @@
#!/usr/bin/perl
# 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 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use Try::Tiny;
use MARC::Record;
use C4::Context;
use C4::Biblio;
use Koha::Database; #??
use Test::More tests => 23;
use Test::MockModule;
use Koha::MarcMergeRules;
use t::lib::Mocks;
my $schema = Koha::Database->schema;
$schema->storage->txn_begin;
t::lib::Mocks::mock_preference('MARCMergeRules', '1');
# Create a record
my $orig_record = MARC::Record->new();
$orig_record->append_fields (
MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
MARC::Field->new('250', '','', 'a' => '256 bottles of beer on the wall'),
MARC::Field->new('500', '','', 'a' => 'One bottle of beer in the fridge'),
);
my $incoming_record = MARC::Record->new();
$incoming_record->append_fields(
MARC::Field->new('250', '', '', 'a' => '256 bottles of beer on the wall'), # Unchanged
MARC::Field->new('250', '', '', 'a' => '251 bottles of beer on the wall'), # Appended
# MARC::Field->new('250', '', '', 'a' => '250 bottles of beer on the wall'), # Removed
# MARC::Field->new('500', '', '', 'a' => 'One bottle of beer in the fridge'), # Deleted
MARC::Field->new('501', '', '', 'a' => 'One cold bottle of beer in the fridge'), # Added
MARC::Field->new('501', '', '', 'a' => 'Two cold bottles of beer in the fridge'), # Added
);
# Test default behavior when MARCMergeRules is enabled, but no rules defined (overwrite)
subtest 'Record fields has been overwritten when no merge rules are defined' => sub {
plan tests => 4;
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" fields has been appended and removed'
);
my @fields = $merged_record->field('500');
cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
my $rule = Koha::MarcMergeRules->find_or_create({
tag => '*',
module => 'source',
filter => '*',
add => 0,
append => 0,
remove => 0,
delete => 0
});
subtest 'Record fields has been protected when matched merge all rule operations are set to "0"' => sub {
plan tests => 3;
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
'"250" fields has retained their original value'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
};
subtest 'Only new fields has been added when add = 1, append = 0, remove = 0, delete = 0' => sub {
plan tests => 4;
$rule->set(
{
'add' => 1,
'append' => 0,
'remove' => 0,
'delete' => 0,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
'"250" fields retain their original value'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field retain it\'s original value'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
subtest 'Only appended fields has been added when add = 0, append = 1, remove = 0, delete = 0' => sub {
plan tests => 3;
$rule->set(
{
'add' => 0,
'append' => 1,
'remove' => 0,
'delete' => 0,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"251" field has been appended'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
};
subtest 'Appended and added fields has been added when add = 1, append = 1, remove = 0, delete = 0' => sub {
plan tests => 4;
$rule->set(
{
'add' => 1,
'append' => 1,
'remove' => 0,
'delete' => 0,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 6, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"251" field has been appended'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
subtest 'Record fields has been only removed when add = 0, append = 0, remove = 1, delete = 0' => sub {
plan tests => 3;
$rule->set(
{
'add' => 0,
'append' => 0,
'remove' => 1,
'delete' => 0,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall'],
'"250" field has been removed'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
};
subtest 'Record fields has been added and removed when add = 1, append = 0, remove = 1, delete = 0' => sub {
plan tests => 4;
$rule->set(
{
'add' => 1,
'append' => 0,
'remove' => 1,
'delete' => 0,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall'],
'"250" field has been removed'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
subtest 'Record fields has been appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub {
plan tests => 3;
$rule->set(
{
'add' => 0,
'append' => 1,
'remove' => 1,
'delete' => 0,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" fields has been appended and removed'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
};
subtest 'Record fields has been added, appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub {
plan tests => 4;
$rule->set(
{
'add' => 1,
'append' => 1,
'remove' => 1,
'delete' => 0,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" fields has been appended and removed'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
subtest 'Record fields has been deleted when add = 0, append = 0, remove = 0, delete = 1' => sub {
plan tests => 2;
$rule->set(
{
'add' => 0,
'append' => 0,
'remove' => 0,
'delete' => 1,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
'"250" fields has retained their original value'
);
};
subtest 'Record fields has been added and deleted when add = 1, append = 0, remove = 0, delete = 1' => sub {
plan tests => 3;
$rule->set(
{
'add' => 1,
'append' => 0,
'remove' => 0,
'delete' => 1,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
'"250" fields has retained their original value'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
subtest 'Record fields has been appended and deleted when add = 0, append = 1, remove = 0, delete = 1' => sub {
plan tests => 2;
$rule->set(
{
'add' => 0,
'append' => 1,
'remove' => 0,
'delete' => 1,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" field has been appended'
);
};
subtest 'Record fields has been added, appended and deleted when add = 1, append = 1, remove = 0, delete = 1' => sub {
plan tests => 3;
$rule->set(
{
'add' => 1,
'append' => 1,
'remove' => 0,
'delete' => 1,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" field has been appended'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
subtest 'Record fields has been removed and deleted when add = 0, append = 0, remove = 1, delete = 1' => sub {
plan tests => 2;
$rule->set(
{
'add' => 0,
'append' => 0,
'remove' => 1,
'delete' => 1,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 1, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall'],
'"250" field has been removed'
);
};
subtest 'Record fields has been added, removed and deleted when add = 1, append = 0, remove = 1, delete = 1' => sub {
plan tests => 3;
$rule->set(
{
'add' => 1,
'append' => 0,
'remove' => 1,
'delete' => 1,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall'],
'"250" field has been removed'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
subtest 'Record fields has been appended, removed and deleted when add = 0, append = 1, remove = 1, delete = 1' => sub {
plan tests => 2;
$rule->set(
{
'add' => 0,
'append' => 1,
'remove' => 1,
'delete' => 1,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" fields has been appended and removed'
);
};
subtest 'Record fields has been overwritten when add = 1, append = 1, remove = 1, delete = 1' => sub {
plan tests => 4;
$rule->set(
{
'add' => 1,
'append' => 1,
'remove' => 1,
'delete' => 1,
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" fields has been appended and removed'
);
my @fields = $merged_record->field('500');
cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
# Test rule tag specificity
# Protect field 500 with more specific tag value
my $skip_all_rule = Koha::MarcMergeRules->find_or_create({
tag => '500',
module => 'source',
filter => '*',
add => 0,
append => 0,
remove => 0,
delete => 0
});
subtest '"500" field has been protected when rule matching on tag "500" is add = 0, append = 0, remove = 0, delete = 0' => sub {
plan tests => 4;
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" fields has been appended and removed'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
# Test regexp matching
subtest '"5XX" fields has been protected when rule matching on regexp "5\d{2}" is add = 0, append = 0, remove = 0, delete = 0' => sub {
plan tests => 3;
$skip_all_rule->set(
{
'tag' => '5\d{2}',
}
);
$skip_all_rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" fields has been appended and removed'
);
is_deeply(
[map { $_->subfield('a') } $merged_record->field('500') ],
['One bottle of beer in the fridge'],
'"500" field has retained it\'s original value'
);
};
# Test module specificity, the 0 all rule should no longer be included in set of applied rules
subtest 'Record fields has been overwritten when non wild card rule with filter match is add = 1, append = 1, remove = 1, delete = 1' => sub {
plan tests => 4;
$rule->set(
{
'filter' => 'test',
}
);
$rule->store();
my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
my @all_fields = $merged_record->fields();
cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
is_deeply(
[map { $_->subfield('a') } $merged_record->field('250') ],
['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
'"250" fields has been appended and removed'
);
my @fields = $merged_record->field('500');
cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
is_deeply(
[map { $_->subfield('a') } $merged_record->field('501') ],
['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
'"501" fields has been added'
);
};
subtest 'An exception is thrown when append = 1, remove = 0 is set for control field rule' => sub {
plan tests => 2;
my $exception = try {
Koha::MarcMergeRules->validate({
'tag' => '008',
'append' => 1,
'remove' => 0,
});
}
catch {
return $_;
};
ok(defined $exception, "Exception was caught");
ok($exception->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions'), "Exception is of correct class");
};
subtest 'An exception is thrown when rule tag is set to invalid regexp' => sub {
plan tests => 2;
my $exception = try {
Koha::MarcMergeRules->validate({
'tag' => '**'
});
}
catch {
return $_;
};
ok(defined $exception, "Exception was caught");
ok($exception->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp'), "Exception is of correct class");
};
$skip_all_rule->delete();
subtest 'context option in ModBiblio is handled correctly' => sub {
plan tests => 6;
$rule->set(
{
tag => '250',
module => 'source',
filter => '*',
'add' => 0,
'append' => 0,
'remove' => 0,
'delete' => 0,
}
);
$rule->store();
my ($biblionumber) = AddBiblio($orig_record, '');
# Since marc merc rules are not run on save, only update
# saved record should be identical to orig_record
my $saved_record = GetMarcBiblio({ biblionumber => $biblionumber });
my @all_fields = $saved_record->fields();
# Koha also adds 999c field, therefore 4 not 3
cmp_ok(scalar @all_fields, '==', 4, 'Saved record has the expected number of fields');
is_deeply(
[map { $_->subfield('a') } $saved_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
'All "250" fields of saved record are identical to original record passed to AddBiblio'
);
is_deeply(
[map { $_->subfield('a') } $saved_record->field('500') ],
['One bottle of beer in the fridge'],
'All "500" fields of saved record are identical to original record passed to AddBiblio'
);
$saved_record->append_fields(
MARC::Field->new('250', '', '', 'a' => '251 bottles of beer on the wall'), # Appended
MARC::Field->new('500', '', '', 'a' => 'One cold bottle of beer in the fridge'), # Appended
);
ModBiblio($saved_record, $biblionumber, '', { context => { 'source' => 'test' } });
my $updated_record = GetMarcBiblio({ biblionumber => $biblionumber });
@all_fields = $updated_record->fields();
cmp_ok(scalar @all_fields, '==', 5, 'Updated record has the expected number of fields');
is_deeply(
[map { $_->subfield('a') } $updated_record->field('250') ],
['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
'"250" fields have retained their original values'
);
is_deeply(
[map { $_->subfield('a') } $updated_record->field('500') ],
['One bottle of beer in the fridge', 'One cold bottle of beer in the fridge'],
'"500" field has been appended'
);
# To trigger removal from search index etc
DelBiblio($biblionumber);
};
# Explicityly delete rule to trigger clearing of cache
$rule->delete();
$schema->storage->txn_rollback;
1;

View file

@ -162,9 +162,10 @@ if ( $op eq 'form' ) {
record_ids => \@record_ids,
};
my $patron = Koha::Patrons->find( $loggedinuser );
my $job_id =
$recordtype eq 'biblio'
? Koha::BackgroundJob::BatchUpdateBiblio->new->enqueue($params)
? Koha::BackgroundJob::BatchUpdateBiblio->new->enqueue($params, { source => 'batchmod', categorycode => $patron->categorycode, userid => $patron->userid })
: Koha::BackgroundJob::BatchUpdateAuthority->new->enqueue($params);
$template->param(