DelBiblio
BiblioAutoLink
LinkBibHeadingsToAuthorities
- ApplyMarcMergeRules
+ ApplyMarcOverlayRules
TransformMarcToKoha
TransformHtmlToMarc
TransformHtmlToXml
use Koha::Biblio::Metadatas;
use Koha::Holds;
use Koha::ItemTypes;
+use Koha::MarcOverlayRules;
use Koha::Plugins;
use Koha::SearchEngine;
use Koha::SearchEngine::Indexer;
use Koha::Libraries;
use Koha::Util::MARC;
-use Koha::MarcMergeRules;
=head1 NAME
=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.
+This parameter is forwarded to L</ApplyMarcOverlayRules> where it is used for
+selecting the current rule set if MARCOverlayRules is enabled.
+See L</ApplyMarcOverlayRules> for more details.
=item C<disable_autolink>
_strip_item_fields($record, $frameworkcode);
- # apply merge rules
- if (C4::Context->preference('MARCMergeRules') && $biblionumber && defined $options && exists $options->{'context'}) {
- $record = ApplyMarcMergeRules({
+ # apply overlay rules
+ if ( C4::Context->preference('MARCOverlayRules')
+ && $biblionumber
+ && defined $options
+ && exists $options->{'context'} )
+ {
+ $record = ApplyMarcOverlayRules(
+ {
biblionumber => $biblionumber,
- record => $record,
- context => $options->{'context'},
+ record => $record,
+ context => $options->{'context'},
}
);
}
return $record;
}
-=head2 ApplyMarcMergeRules
+=head2 ApplyMarcOverlayRules
- my $record = ApplyMarcMergeRules($params)
+ my $record = ApplyMarcOverlayRules($params)
Applies marc merge rules to a record.
=cut
-sub ApplyMarcMergeRules {
+sub ApplyMarcOverlayRules {
my ($params) = @_;
my $biblionumber = $params->{biblionumber};
my $incoming_record = $params->{record};
if (!$biblionumber) {
- carp 'ApplyMarcMergeRules called on undefined biblionumber';
+ carp 'ApplyMarcOverlayRules called on undefined biblionumber';
return;
}
if (!$incoming_record) {
- carp 'ApplyMarcMergeRules called on undefined record';
+ carp 'ApplyMarcOverlayRules called on undefined record';
return;
}
my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
- # Skip merge rules if called with no context
+ # Skip overlay rules if called with no context
if ($old_record && defined $params->{context}) {
- return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
+ return Koha::MarcOverlayRules->merge_records($old_record, $incoming_record, $params->{context});
}
return $incoming_record;
}
-1;
-
=head2 _after_biblio_action_hooks
Helper method that takes care of calling all plugin hooks
);
}
+1;
+
__END__
=head1 AUTHOR
+++ /dev/null
-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;
--- /dev/null
+package Koha::Exceptions::MarcOverlayRule;
+
+# 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::MarcOverlayRule' => {
+ description => 'Something went wrong!',
+ },
+ 'Koha::Exceptions::MarcOverlayRule::InvalidTagRegExp' => {
+ isa => 'Koha::Exceptions::MarcOverlayRule',
+ description => 'Invalid regular expression for tag'
+ },
+ 'Koha::Exceptions::MarcOverlayRule::InvalidControlFieldActions' => {
+ isa => 'Koha::Exceptions::MarcOverlayRule',
+ description => 'Invalid control field actions'
+ }
+);
+
+=head1 NAME
+
+Koha::Exceptions::MarcOverlayRule - Base class for MarcOverlayRule exceptions
+
+=head1 Exceptions
+
+=head2 Koha::Exceptions::MarcOverlayRule
+
+Generic MarcOverlayRule exception
+
+=head2 Koha::Exceptions::MarcOverlayRule::InvalidTagRegExp
+
+Exception for rule validation when rule tag is an invalid regular expression
+
+=head2 Koha::Exceptions::MarcOverlayRule::InvalidControlFieldActions
+
+Exception for rule validation for control field rules with invalid combination of actions
+
+=cut
+
+1;
+++ /dev/null
-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;
+++ /dev/null
-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;
--- /dev/null
+package Koha::MarcOverlayRule;
+
+# 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 parent qw(Koha::Object);
+
+my $cache = Koha::Caches->get_instance();
+
+=head1 NAME
+
+Koha::MarcOverlayRule - Koha MarcOverlayRule 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_overlay_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_overlay_rules');
+ $self->SUPER::delete(@_);
+}
+
+sub _type {
+ return 'MarcOverlayRule';
+}
+
+1;
--- /dev/null
+package Koha::MarcOverlayRules;
+
+# 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 List::Util qw(first);
+use Koha::MarcOverlayRule;
+use Carp;
+
+use Koha::Exceptions::MarcOverlayRule;
+use Try::Tiny;
+use Scalar::Util qw(looks_like_number);
+
+use parent qw(Koha::Objects);
+
+my $cache = Koha::Caches->get_instance();
+
+=head1 NAME
+
+Koha::MarcOverlayRules - Koha MarcOverlayRules 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::MarcOverlayRules->context_rules($context);
+
+Gets all MARC overlay 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_overlay_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_overlay_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::MarcOverlayRules->merge_records($old_record, $incoming_record, $context);
+
+Overlay C<$old_record> with C<$incoming_record> applying overlay 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_overlay_rules');
+}
+
+=head2 find_or_create
+
+Override C<find_or_create> to clear marc overlay 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 overlay rules cache.
+
+=cut
+
+sub update {
+ my $self = shift @_;
+ $self->_clear_caches();
+ return $self->SUPER::update(@_);
+}
+
+=head2 delete
+
+Override C<delete> to clear marc overlay rules cache.
+
+=cut
+
+sub delete {
+ my $self = shift @_;
+ $self->_clear_caches();
+ return $self->SUPER::delete(@_);
+}
+
+=head2 validate
+
+ Koha::MarcOverlayRules->validate($rule_data);
+
+Validates C<$rule_data>. Throws C<Koha::Exceptions::MarcOverlayRule::InvalidTagRegExp>
+if C<$rule_data->{tag}> contains an invalid regular expression. Throws
+C<Koha::Exceptions::MarcOverlayRule::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::MarcOverlayRule::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::MarcOverlayRule::InvalidControlFieldActions->throw(
+ "Combination of allow append and skip remove not permitted for control fields"
+ );
+ }
+ }
+ return 1;
+}
+
+sub _type {
+ return 'MarcOverlayRule';
+}
+
+=head3 object_class
+
+=cut
+
+sub object_class {
+ return 'Koha::MarcOverlayRule';
+}
+
+1;
+++ /dev/null
-#!/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;
--- /dev/null
+#!/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::Auth qw( get_template_and_user );
+use C4::Output qw( output_html_with_http_headers );
+use C4::ImportBatch;
+use C4::Matcher;
+use C4::BackgroundJob;
+use C4::Labels::Batch;
+use Koha::MarcOverlayRules;
+use Koha::Patron::Categories;
+
+my $script_name = "/cgi-bin/koha/admin/marc-overlay-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-overlay-rules.tt",
+ query => $input,
+ type => "intranet",
+ authnotrequired => 0,
+ flagsrequired => { parameters => 'manage_marc_overlay_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::MarcOverlayRules->_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::MarcOverlayRules->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::MarcOverlayRules->validate($rule_data);
+ }
+ catch {
+ die $_ unless blessed $_ && $_->can('rethrow');
+
+ if ($_->isa('Koha::Exceptions::MarcOverlayRule::InvalidTagRegExp')) {
+ push @{$errors}, {
+ type => 'error',
+ code => 'invalid_tag_regexp',
+ tag => $rule_data->{tag},
+ };
+ }
+ elsif ($_->isa('Koha::Exceptions::MarcOverlayRule::InvalidControlFieldActions')) {
+ push @{$errors}, {
+ type => 'error',
+ code => 'invalid_control_field_actions',
+ tag => $rule_data->{tag},
+ };
+ }
+ else {
+ $_->rethrow;
+ }
+ };
+ if (!@{$errors}) {
+ my $rule = Koha::MarcOverlayRules->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;
-$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 );
+$DBversion = 'XXX'; # will be replaced by the RM
+if ( CheckVersion($DBversion) ) {
+
+ unless (TableExists('marc_overlay_rules_modules')) {
+ $dbh->do(q{
+ CREATE TABLE `marc_overlay_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;
+ });
+ }
+
+ unless ( TableExists('marc_overlay_rules') ) {
+ $dbh->do(q{
+ CREATE TABLE IF NOT EXISTS `marc_overlay_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(1) NOT NULL DEFAULT 0,
+ `append` TINYINT(1) NOT NULL DEFAULT 0,
+ `remove` TINYINT(1) NOT NULL DEFAULT 0,
+ `delete` TINYINT(1) NOT NULL DEFAULT 0,
+ PRIMARY KEY(`id`),
+ CONSTRAINT `marc_overlay_rules_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_overlay_rules_modules` (`name`) ON DELETE CASCADE ON UPDATE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+ });
+ }
- $sql = q{
+ $dbh->do(q{
INSERT IGNORE INTO systempreferences (`variable`, `value`, `options`, `explanation`, `type`) VALUES (
- 'MARCMergeRules',
+ 'MARCOverlayRules',
'0',
NULL,
- 'Use the MARC merge rules system to decide what actions to take for each field when modifying records.',
+ 'Use the MARC record overlay rules system to decide what actions to take for each field when modifying records.',
'YesNo'
);
- };
- $dbh->do( $sql );
+ });
- $sql = q{
+ $dbh->do(q{
INSERT IGNORE INTO permissions (module_bit, code, description) VALUES (
3,
- 'manage_marc_merge_rules',
- 'Manage MARC merge rules configuration'
+ 'manage_marc_overlay_rules',
+ 'Manage MARC overlay 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";
+ NewVersion( $DBversion, 14957, "Add a way to define overlay rules for incoming MARC records)\n" );
}
/*!40101 SET character_set_client = @saved_cs_client */;
--
--- Table structure for table `marc_merge_rules_modules`
+-- Table structure for table `marc_overlay_rules_modules`
--
-DROP TABLE IF EXISTS `marc_merge_rules_modules`;
-CREATE TABLE `marc_merge_rules_modules` (
+DROP TABLE IF EXISTS `marc_overlay_rules_modules`;
+CREATE TABLE `marc_overlay_rules_modules` (
`name` varchar(127) NOT NULL,
`description` varchar(255),
`specificity` int(11) NOT NULL UNIQUE,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
--- Table structure for table `marc_merge_rules`
+-- Table structure for table `marc_overlay_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
+DROP TABLE IF EXISTS `marc_overlay_rules`;
+CREATE TABLE IF NOT EXISTS `marc_overlay_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,
+ `add` TINYINT(1) NOT NULL DEFAULT 0,
+ `append` TINYINT(1) NOT NULL DEFAULT 0,
+ `remove` TINYINT(1) NOT NULL DEFAULT 0,
+ `delete` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY(`id`),
- CONSTRAINT `marc_merge_rules_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_merge_rules_modules` (`name`) ON DELETE CASCADE ON UPDATE CASCADE
-);
+ CONSTRAINT `marc_overlay_rules_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_overlay_rules_modules` (`name`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `marc_modification_template_actions`
('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'),
+('MARCOverlayRules','0',NULL,'Use the MARC record overlay rules system to decide what actions to take for each field when modifying records.','YesNo'),
('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'),
('MaxItemsToDisplayForBatchDel','1000',NULL,'Display up to a given number of items in a single item deletionbatch.','Integer'),
('MaxItemsToDisplayForBatchMod','1000',NULL,'Display up to a given number of items in a single item modification batch.','Integer'),
( 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_marc_overlay_rules', 'Manage MARC overlay 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'),
[% 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>
+ [% IF ( CAN_user_parameters_manage_marc_overlay_rules ) %]
+ <li><a href="/cgi-bin/koha/admin/marc-overlay-rules.pl">MARC overlay rules</a></li>
[% END %]
</ul>
[% END %]
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
+ [%- CASE 'manage_marc_overlay_rules' -%]
+ <span class="sub_permission manage_marc_overlay_rules_subpermission">
+ Manage MARC overlay rules configuration
</span>
<span class="permissioncode">([% name | html %])</span>
[%- CASE 'manage_search_targets' -%]
<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>
+ <dt><a href="/cgi-bin/koha/admin/marc-overlay-rules.pl">MARC overlay rules</a></dt>
+ <dd>Managed MARC field overlay rules</dd>
</dl>
[% END %]
+++ /dev/null
-[% USE Koha %]
-[% INCLUDE 'doc-head-open.inc' %]
-<title>Koha › Administration › 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> › <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
- › 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 <i id="info_added" data-toggle="tooltip" title="If a field matching the rule tag only exists in the incoming record" data-placement="right" class="fa fa-info-circle"></i></th>
- <th>Appended <i id="info_appended" data-toggle="tooltip" title="If the original record has one or more fields matching with the rule tag, but one or more fields matching the rule tag differ in the incoming record" data-placement="right" class="fa fa-info-circle"></i></th>
- <th>Removed <i id="info_removed" data-toggle="tooltip" title="If the original record has a field matching the rule tag, but the matching field is not in the incoming record" data-placement="right" class="fa fa-info-circle"></i></th>
- <th>Deleted <i id="info_deleted" data-toggle="tooltip" title="If the original record has fields matching the rule tag, but no fields with this are found in the incoming record" data-placement="right" class="fa fa-info-circle"></i></th>
- <th>Actions</th>
- <th> </th>
- </tr></thead>
- [% UNLESS edit %]
- <tfoot>
- <tr class="rule-new">
- <th> </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-default btn-xs" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
- <th><button id="btn_batchremove" disabled="disabled" class="btn btn-default btn-xs" 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-default btn-xs" title="Save" id="doedit" value="[% rule.id | html %]"><i class="fa fa-check"></i> Save</button>
- <button type="submit" class="btn btn-default btn-xs" 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-default btn-xs"><i class="fa fa-trash"></i> Delete</a>
- <a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-default btn-xs"><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' %]
--- /dev/null
+[% USE Koha %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha › Administration › MARC overlay 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-overlay-rules-form');
+
+ if(id) {
+ $('<input type="hidden"/>')
+ .attr('name', 'id')
+ .attr('value', id)
+ .appendTo('#marc-overlay-rules-form');
+ }
+
+ var valid = true;
+ if (op == 'add' || op == 'edit') {
+ var validate = [
+ $('#marc-overlay-rules-form input[name="filter"]'),
+ $('#marc-overlay-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-overlay-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-overlay-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 overlay_rules_presets = {};
+ overlay_rules_presets[_("Protect")] = {
+ 'add': 0,
+ 'append': 0,
+ 'remove': 0,
+ 'delete': 0
+ };
+ overlay_rules_presets[_("Overwrite")] = {
+ 'add': 1,
+ 'append': 1,
+ 'remove': 1,
+ 'delete': 1
+ };
+ overlay_rules_presets[_("Add new")] = {
+ 'add': 1,
+ 'append': 0,
+ 'remove': 0,
+ 'delete': 0
+ };
+ overlay_rules_presets[_("Add and append")] = {
+ 'add': 1,
+ 'append': 1,
+ 'remove': 0,
+ 'delete': 0
+ };
+ overlay_rules_presets[_("Protect from deletion")] = {
+ 'add': 1,
+ 'append': 1,
+ 'remove': 1,
+ 'delete': 0
+ };
+
+ var overlay_rules_label_to_value = {};
+ overlay_rules_label_to_value[_("Add")] = 1;
+ overlay_rules_label_to_value[_("Append")] = 1;
+ overlay_rules_label_to_value[_("Remove")] = 1;
+ overlay_rules_label_to_value[_("Delete")] = 1;
+ overlay_rules_label_to_value[_("Skip")] = 0;
+
+ function hash_config(config) {
+ return JSON.stringify(config, Object.keys(config).sort());
+ }
+
+ var overlay_rules_preset_map = {};
+ $.each(overlay_rules_presets, function(preset, config) {
+ overlay_rules_preset_map[hash_config(config)] = preset;
+ });
+
+ function operations_config_overlay_rule_preset(config) {
+ return overlay_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')] = overlay_rules_label_to_value[$operation.text()];
+ });
+ $('.rule-preset', $this).text(
+ operations_config_overlay_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_overlay_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(overlay_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-overlay-rules" class="admin">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'cat-search.inc' %]
+
+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
+ › MARC overlay rules
+</div>
+
+<div class="main container-fluid">
+<div class="row">
+<div class="col-sm-10 col-sm-push-2">
+
+<h1>Manage MARC overlay 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( 'MARCOverlayRules' ) %]
+ <div class="dialog message">
+ The <b>MARCOverlayRules</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-overlay-rules-form">
+<table id="marc-overlay-rules">
+ <thead><tr>
+ <th>Rule</th>
+ <th>Module</th>
+ <th>Filter</th>
+ <th>Tag</th>
+ <th>Preset</th>
+ <th>Added <i id="info_added" data-toggle="tooltip" title="If a field matching the rule tag only exists in the incoming record" data-placement="right" class="fa fa-info-circle"></i></th>
+ <th>Appended <i id="info_appended" data-toggle="tooltip" title="If the original record has one or more fields matching with the rule tag, but one or more fields matching the rule tag differ in the incoming record" data-placement="right" class="fa fa-info-circle"></i></th>
+ <th>Removed <i id="info_removed" data-toggle="tooltip" title="If the original record has a field matching the rule tag, but the matching field is not in the incoming record" data-placement="right" class="fa fa-info-circle"></i></th>
+ <th>Deleted <i id="info_deleted" data-toggle="tooltip" title="If the original record has fields matching the rule tag, but no fields with this are found in the incoming record" data-placement="right" class="fa fa-info-circle"></i></th>
+ <th>Actions</th>
+ <th> </th>
+ </tr></thead>
+ [% UNLESS edit %]
+ <tfoot>
+ <tr class="rule-new">
+ <th> </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-default btn-xs" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
+ <th><button id="btn_batchremove" disabled="disabled" class="btn btn-default btn-xs" 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-default btn-xs" title="Save" id="doedit" value="[% rule.id | html %]"><i class="fa fa-check"></i> Save</button>
+ <button type="submit" class="btn btn-default btn-xs" 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-default btn-xs"><i class="fa fa-trash"></i> Delete</a>
+ <a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-default btn-xs"><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' %]
1: "do"
0: "don't"
- attempt to match aggressively by trying all variations of the ISSNs in the imported record as a phrase in the ISSN fields of already cataloged records.
-
+ -
+ - pref: MARCOverlayRules
+ choices:
+ 1: "Use"
+ 0: "Don't use"
+ - MARC overlay rules for incoming records, to decide which action to take for each field.
Exporting:
-
- "Include the following fields when exporting BibTeX:"
- "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.
+++ /dev/null
-#!/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;
--- /dev/null
+#!/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 qw( AddBiblio ModBiblio DelBiblio GetMarcBiblio );
+use Koha::Database;
+
+use Test::More tests => 23;
+use Test::MockModule;
+
+use Koha::MarcOverlayRules;
+
+use t::lib::Mocks;
+
+my $schema = Koha::Database->schema;
+$schema->storage->txn_begin;
+
+t::lib::Mocks::mock_preference('MARCOverlayRules', '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 MARCOverlayRules 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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->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::MarcOverlayRules->validate({
+ 'tag' => '008',
+ 'append' => 1,
+ 'remove' => 0,
+ });
+ }
+ catch {
+ return $_;
+ };
+ ok(defined $exception, "Exception was caught");
+ ok($exception->isa('Koha::Exceptions::MarcOverlayRule::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::MarcOverlayRules->validate({
+ 'tag' => '**'
+ });
+ }
+ catch {
+ return $_;
+ };
+ ok(defined $exception, "Exception was caught");
+ ok($exception->isa('Koha::Exceptions::MarcOverlayRule::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;