View | Details | Raw Unified | Return to bug 14957
Collapse All | Expand All

(-)a/C4/Biblio.pm (-8 / +94 lines)
Lines 59-64 BEGIN { Link Here
59
        DelBiblio
59
        DelBiblio
60
        BiblioAutoLink
60
        BiblioAutoLink
61
        LinkBibHeadingsToAuthorities
61
        LinkBibHeadingsToAuthorities
62
        ApplyMarcMergeRules
62
        TransformMarcToKoha
63
        TransformMarcToKoha
63
        TransformHtmlToMarc
64
        TransformHtmlToMarc
64
        TransformHtmlToXml
65
        TransformHtmlToXml
Lines 105-114 use Koha::SearchEngine; Link Here
105
use Koha::SearchEngine::Indexer;
106
use Koha::SearchEngine::Indexer;
106
use Koha::Libraries;
107
use Koha::Libraries;
107
use Koha::Util::MARC;
108
use Koha::Util::MARC;
109
use Koha::MarcMergeRules;
108
110
109
use vars qw($debug $cgi_debug);
111
use vars qw($debug $cgi_debug);
110
112
111
112
=head1 NAME
113
=head1 NAME
113
114
114
C4::Biblio - cataloging management functions
115
C4::Biblio - cataloging management functions
Lines 304-310 sub AddBiblio { Link Here
304
305
305
=head2 ModBiblio
306
=head2 ModBiblio
306
307
307
  ModBiblio( $record,$biblionumber,$frameworkcode, $disable_autolink);
308
  ModBiblio($record, $biblionumber, $frameworkcode, $options);
308
309
309
Replace an existing bib record identified by C<$biblionumber>
310
Replace an existing bib record identified by C<$biblionumber>
310
with one supplied by the MARC::Record object C<$record>.  The embedded
311
with one supplied by the MARC::Record object C<$record>.  The embedded
Lines 320-335 in the C<biblio> and C<biblioitems> tables, as well as Link Here
320
which fields are used to store embedded item, biblioitem,
321
which fields are used to store embedded item, biblioitem,
321
and biblionumber data for indexing.
322
and biblionumber data for indexing.
322
323
323
Unless C<$disable_autolink> is passed ModBiblio will relink record headings
324
The C<$options> argument is a hashref with additional parameters:
325
326
=over 4
327
328
=item C<context>
329
330
This parameter is forwared to L</ApplyMarcMergeRules> where it is used for
331
selecting the current rule set if Marc Merge Rules is enabled.
332
See L</ApplyMarcMergeRules> for more details.
333
334
=item C<disable_autolink>
335
336
Unless C<disable_autolink> is passed ModBiblio will relink record headings
324
to authorities based on settings in the system preferences. This flag allows
337
to authorities based on settings in the system preferences. This flag allows
325
us to not relink records when the authority linker is saving modifications.
338
us to not relink records when the authority linker is saving modifications.
326
339
340
=back
341
327
Returns 1 on success 0 on failure
342
Returns 1 on success 0 on failure
328
343
329
=cut
344
=cut
330
345
331
sub ModBiblio {
346
sub ModBiblio {
332
    my ( $record, $biblionumber, $frameworkcode, $disable_autolink ) = @_;
347
    my ( $record, $biblionumber, $frameworkcode, $options ) = @_;
348
    $options //= {};
349
333
    if (!$record) {
350
    if (!$record) {
334
        carp 'No record passed to ModBiblio';
351
        carp 'No record passed to ModBiblio';
335
        return 0;
352
        return 0;
Lines 340-346 sub ModBiblio { Link Here
340
        logaction( "CATALOGUING", "MODIFY", $biblionumber, "biblio BEFORE=>" . $newrecord->as_formatted );
357
        logaction( "CATALOGUING", "MODIFY", $biblionumber, "biblio BEFORE=>" . $newrecord->as_formatted );
341
    }
358
    }
342
359
343
    if ( !$disable_autolink && C4::Context->preference('BiblioAddsAuthorities') ) {
360
    if ( !$options->{disable_autolink} && C4::Context->preference('BiblioAddsAuthorities') ) {
344
        BiblioAutoLink( $record, $frameworkcode );
361
        BiblioAutoLink( $record, $frameworkcode );
345
    }
362
    }
346
363
Lines 361-366 sub ModBiblio { Link Here
361
378
362
    _strip_item_fields($record, $frameworkcode);
379
    _strip_item_fields($record, $frameworkcode);
363
380
381
    # apply merge rules
382
    if (C4::Context->preference('MARCMergeRules') && $biblionumber && defined $options && exists $options->{'context'}) {
383
        $record = ApplyMarcMergeRules({
384
                biblionumber => $biblionumber,
385
                record => $record,
386
                context => $options->{'context'},
387
            }
388
        );
389
    }
390
364
    # update biblionumber and biblioitemnumber in MARC
391
    # update biblionumber and biblioitemnumber in MARC
365
    # FIXME - this is assuming a 1 to 1 relationship between
392
    # FIXME - this is assuming a 1 to 1 relationship between
366
    # biblios and biblioitems
393
    # biblios and biblioitems
Lines 377-383 sub ModBiblio { Link Here
377
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
404
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
378
405
379
    # update the MARC record (that now contains biblio and items) with the new record data
406
    # update the MARC record (that now contains biblio and items) with the new record data
380
    &ModBiblioMarc( $record, $biblionumber );
407
    ModBiblioMarc( $record, $biblionumber );
381
408
382
    # modify the other koha tables
409
    # modify the other koha tables
383
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
410
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
Lines 2904-2910 sub _koha_delete_biblio_metadata { Link Here
2904
2931
2905
=head2 ModBiblioMarc
2932
=head2 ModBiblioMarc
2906
2933
2907
  &ModBiblioMarc($newrec,$biblionumber);
2934
  ModBiblioMarc($newrec,$biblionumber);
2908
2935
2909
Add MARC XML data for a biblio to koha
2936
Add MARC XML data for a biblio to koha
2910
2937
Lines 3216-3223 sub RemoveAllNsb { Link Here
3216
    return $record;
3243
    return $record;
3217
}
3244
}
3218
3245
3219
1;
3246
=head2 ApplyMarcMergeRules
3247
3248
    my $record = ApplyMarcMergeRules($params)
3220
3249
3250
Applies marc merge rules to a record.
3251
3252
C<$params> is expected to be a hashref with below keys defined.
3253
3254
=over 4
3255
3256
=item C<biblionumber>
3257
biblionumber of old record
3258
3259
=item C<record>
3260
Incoming record that will be merged with old record
3261
3262
=item C<context>
3263
hashref containing at least one context module and filter value on
3264
the form {module => filter, ...}.
3265
3266
=back
3267
3268
Returns:
3269
3270
=over 4
3271
3272
=item C<$record>
3273
3274
Merged MARC record based with merge rules for C<context> applied. If no old
3275
record for C<biblionumber> can be found, C<record> is returned unchanged.
3276
Default action when no matching context is found to return C<record> unchanged.
3277
If no rules are found for a certain field tag the default is to overwrite with
3278
fields with this field tag from C<record>.
3279
3280
=back
3281
3282
=cut
3283
3284
sub ApplyMarcMergeRules {
3285
    my ($params) = @_;
3286
    my $biblionumber = $params->{biblionumber};
3287
    my $incoming_record = $params->{record};
3288
3289
    if (!$biblionumber) {
3290
        carp 'ApplyMarcMergeRules called on undefined biblionumber';
3291
        return;
3292
    }
3293
    if (!$incoming_record) {
3294
        carp 'ApplyMarcMergeRules called on undefined record';
3295
        return;
3296
    }
3297
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3298
3299
    # Skip merge rules if called with no context
3300
    if ($old_record && defined $params->{context}) {
3301
        return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
3302
    }
3303
    return $incoming_record;
3304
}
3305
3306
1;
3221
3307
3222
=head2 _after_biblio_action_hooks
3308
=head2 _after_biblio_action_hooks
3223
3309
(-)a/C4/ImportBatch.pm (-1 / +1 lines)
Lines 664-670 sub BatchCommitRecords { Link Here
664
                }
664
                }
665
                $oldxml = $old_marc->as_xml($marc_type);
665
                $oldxml = $old_marc->as_xml($marc_type);
666
666
667
                ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode);
667
                ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode, {context => {source => 'batchimport'}});
668
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?"; # FIXME call SetMatchedBiblionumber instead
668
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?"; # FIXME call SetMatchedBiblionumber instead
669
669
670
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
670
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
(-)a/Koha/BackgroundJob/BatchUpdateBiblio.pm (-1 / +7 lines)
Lines 88-94 sub process { Link Here
88
            my $record = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
88
            my $record = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
89
            C4::MarcModificationTemplates::ModifyRecordWithTemplate( $mmtid, $record );
89
            C4::MarcModificationTemplates::ModifyRecordWithTemplate( $mmtid, $record );
90
            my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
90
            my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
91
            C4::Biblio::ModBiblio( $record, $biblionumber, $frameworkcode );
91
            C4::Biblio::ModBiblio( $record, $biblionumber, $frameworkcode,
92
                {
93
                    source => $args->{source},
94
                    categorycode => $args->{categorycode},
95
                    userid => $args->{userid},
96
                }
97
            );
92
        };
98
        };
93
        if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
99
        if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
94
            push @messages, {
100
            push @messages, {
(-)a/Koha/Exceptions/MarcMergeRule.pm (+55 lines)
Line 0 Link Here
1
package Koha::Exceptions::MarcMergeRule;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Exception::Class (
21
22
    'Koha::Exceptions::MarcMergeRule' => {
23
        description => 'Something went wrong!',
24
    },
25
    'Koha::Exceptions::MarcMergeRule::InvalidTagRegExp' => {
26
        isa => 'Koha::Exceptions::MarcMergeRule',
27
        description => 'Invalid regular expression for tag'
28
    },
29
    'Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions' => {
30
        isa => 'Koha::Exceptions::MarcMergeRule',
31
        description => 'Invalid control field actions'
32
    }
33
);
34
35
=head1 NAME
36
37
Koha::Exceptions::MarcMergeRule - Base class for MarcMergeRule exceptions
38
39
=head1 Exceptions
40
41
=head2 Koha::Exceptions::MarcMergeRule
42
43
Generic MarcMergeRule exception
44
45
=head2 Koha::Exceptions::MarcMergeRule::InvalidTagRegExp
46
47
Exception for rule validation when rule tag is an invalid regular expression
48
49
=head2 Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions
50
51
Exception for rule validation for control field rules with invalid combination of actions
52
53
=cut
54
55
1;
(-)a/Koha/MarcMergeRule.pm (+58 lines)
Line 0 Link Here
1
package Koha::MarcMergeRule;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use parent qw(Koha::Object);
21
22
my $cache = Koha::Caches->get_instance();
23
24
=head1 NAME
25
26
Koha::MarcMergeRule - Koha MarcMergeRule Object class
27
28
=cut
29
30
=head2 store
31
32
Override C<store> to clear marc merge rules cache.
33
34
=cut
35
36
sub store {
37
    my $self = shift @_;
38
    $cache->clear_from_cache('marc_merge_rules');
39
    $self->SUPER::store(@_);
40
}
41
42
=head2 delete
43
44
Override C<delete> to clear marc merge rules cache.
45
46
=cut
47
48
sub delete {
49
    my $self = shift @_;
50
    $cache->clear_from_cache('marc_merge_rules');
51
    $self->SUPER::delete(@_);
52
}
53
54
sub _type {
55
    return 'MarcMergeRule';
56
}
57
58
1;
(-)a/Koha/MarcMergeRules.pm (+381 lines)
Line 0 Link Here
1
package Koha::MarcMergeRules;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
use List::Util qw(first);
20
use Koha::MarcMergeRule;
21
use Carp;
22
23
use Koha::Exceptions::MarcMergeRule;
24
use Try::Tiny;
25
use Scalar::Util qw(looks_like_number);
26
27
use parent qw(Koha::Objects);
28
29
my $cache = Koha::Caches->get_instance();
30
31
=head1 NAME
32
33
Koha::MarcMergeRules - Koha MarcMergeRules Object set class
34
35
=head1 API
36
37
=head2 Class Methods
38
39
=head3 operations
40
41
Returns a list of all valid operations.
42
43
=cut
44
45
sub operations {
46
    return ('add', 'append', 'remove', 'delete');
47
}
48
49
=head3 context_rules
50
51
    my $rules = Koha::MarcMergeRules->context_rules($context);
52
53
Gets all MARC merge rules for the supplied C<$context> (hashref with { module => filter, ... } values).
54
55
=cut
56
57
sub context_rules {
58
    my ($self, $context) = @_;
59
60
    return unless %{$context};
61
62
    my $rules = $cache->get_from_cache('marc_merge_rules', { unsafe => 1 });
63
64
    if (!$rules) {
65
        $rules = {};
66
        my @rules_rows = $self->_resultset()->search(
67
            undef,
68
            {
69
                order_by => { -desc => [qw/id/] }
70
            }
71
        );
72
        foreach my $rule_row (@rules_rows) {
73
            my %rule = $rule_row->get_columns();
74
            my $operations = {};
75
76
            foreach my $operation ($self->operations) {
77
                $operations->{$operation} = { allow => $rule{$operation}, rule => $rule{id} };
78
            }
79
80
            # TODO: Remove unless check and validate on saving rules?
81
            if ($rule{tag} eq '*') {
82
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{'*'}) {
83
                    $rules->{$rule{module}}->{$rule{filter}}->{'*'} = $operations;
84
                }
85
            }
86
            elsif ($rule{tag} =~ /^(\d{3})$/) {
87
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}}) {
88
                    $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}} = $operations;
89
                }
90
            }
91
            else {
92
                my $regexps = ($rules->{$rule{module}}->{$rule{filter}}->{regexps} //= []);
93
                push @{$regexps}, [$rule{tag}, $operations];
94
            }
95
        }
96
        $cache->set_in_cache('marc_merge_rules', $rules);
97
    }
98
99
    my $context_rules = undef;
100
    foreach my $module_name (keys %{$context}) {
101
        if (
102
            exists $rules->{$module_name} &&
103
            exists $rules->{$module_name}->{$context->{$module_name}}
104
        ) {
105
            $context_rules = $rules->{$module_name}->{$context->{$module_name}};
106
            last;
107
        }
108
    }
109
    if (!$context_rules) {
110
        # No perms matching specific context conditions found, try wildcard value for each active context
111
        foreach my $module_name (keys %{$context}) {
112
            if (exists $rules->{$module_name}->{'*'}) {
113
                $context_rules = $rules->{$module_name}->{'*'};
114
                last;
115
            }
116
        }
117
    }
118
    return $context_rules;
119
}
120
121
=head3 merge_records
122
123
    my $merged_record = Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $context);
124
125
Merge C<$old_record> with C<$incoming_record> applying merge rules for C<$context>.
126
Returns merged record C<$merged_record>. C<$old_record>, C<$incoming_record> and
127
C<$merged_record> are all MARC::Record objects.
128
129
=cut
130
131
sub merge_records {
132
    my ($self, $old_record, $incoming_record, $context) = @_;
133
134
    my $rules = $self->context_rules($context);
135
136
    # Default when no rules found is to overwrite with incoming record
137
    return $incoming_record unless $rules;
138
139
    my $fields_by_tag = sub {
140
        my ($record) = @_;
141
        my $fields = {};
142
        foreach my $field ($record->fields()) {
143
            $fields->{$field->tag()} //= [];
144
            push @{$fields->{$field->tag()}}, $field;
145
        }
146
        return $fields;
147
    };
148
149
    my $hash_field_data = sub {
150
        my ($field) = @_;
151
        my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
152
        return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields());
153
    };
154
155
    my $diff_by_key = sub {
156
        my ($a, $b) = @_;
157
        my @removed;
158
        my @intersecting;
159
        my @added;
160
        my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
161
        foreach my $key (keys %keys_index) {
162
            if ($a->{$key} && $b->{$key}) {
163
                push @intersecting, $a->{$key};
164
            }
165
            elsif ($a->{$key}) {
166
                push @removed, $a->{$key};
167
            }
168
            else {
169
                push @added, $b->{$key};
170
            }
171
        }
172
        return (\@removed, \@intersecting, \@added);
173
    };
174
175
    my $tag_rules = $rules->{tags} // {};
176
    my $default_rule = $rules->{'*'} // {
177
        add => { allow => 1, 'rule' => 0},
178
        append => { allow => 1, 'rule' => 0},
179
        delete => { allow => 1, 'rule' => 0},
180
        remove => { allow => 1, 'rule' => 0},
181
    };
182
183
    # Precompile regexps
184
    my @regexp_rules = map { { regexp => qr/^$_->[0]$/, actions => $_->[1] } } @{$rules->{regexps} // []};
185
186
    my $get_matching_field_rule = sub {
187
        my ($tag) = @_;
188
        # Exact match takes precedence, then regexp, then wildcard/defaults
189
        return $tag_rules->{$tag} //
190
            %{(first { $tag =~ $_->{regexp} } @regexp_rules) // {}}{actions} //
191
            $default_rule;
192
    };
193
194
    my %merged_record_fields;
195
196
    my $current_fields = $fields_by_tag->($old_record);
197
    my $incoming_fields = $fields_by_tag->($incoming_record);
198
199
    # First we get all new incoming fields
200
    my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
201
    foreach my $tag (@new_field_tags) {
202
        my $rule = $get_matching_field_rule->($tag);
203
        if ($rule->{add}->{allow}) {
204
            $merged_record_fields{$tag} //= [];
205
            push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
206
        }
207
    }
208
209
    # Then we get all fields no longer present in incoming fields
210
    my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
211
    foreach my $tag (@deleted_field_tags) {
212
        my $rule = $get_matching_field_rule->($tag);
213
        if (!$rule->{delete}->{allow}) {
214
            $merged_record_fields{$tag} //= [];
215
            push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
216
        }
217
    }
218
219
    # Then we get the intersection of fields, present both in
220
    # current and incoming record (possibly to be overwritten)
221
    my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
222
    foreach my $tag (@common_field_tags) {
223
        my $rule = $get_matching_field_rule->($tag);
224
225
        # Special handling for control fields
226
        if ($tag < 10) {
227
            if (
228
                $rule->{append}->{allow} &&
229
                !$rule->{remove}->{allow}
230
            ) {
231
                # This should be highly unlikely since we have input validation to protect against this case
232
                carp "Allowing \"append\" and skipping \"remove\" is not permitted for control fields, falling back to skipping both \"append\" and \"remove\"";
233
                push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
234
            }
235
            elsif ($rule->{append}->{allow}) {
236
                push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
237
            }
238
            else {
239
                push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
240
            }
241
        }
242
        else {
243
            # Compute intersection and diff using field data
244
            my $sort_weight = 0;
245
            my %current_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$current_fields->{$tag}};
246
247
            # Always put incoming fields after current fields
248
            my %incoming_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$incoming_fields->{$tag}};
249
250
            my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
251
252
            my @merged_fields;
253
254
            # First add common fields (intersection)
255
            # Unchanged
256
            if (@{$common_fields}) {
257
                push @merged_fields, @{$common_fields};
258
            }
259
            # Removed
260
            if (@{$current_fields_only}) {
261
                if (!$rule->{remove}->{allow}) {
262
                    push @merged_fields, @{$current_fields_only};
263
                }
264
            }
265
            # Appended
266
            if (@{$incoming_fields_only}) {
267
                if ($rule->{append}->{allow}) {
268
                    push @merged_fields, @{$incoming_fields_only};
269
                }
270
            }
271
            $merged_record_fields{$tag} //= [];
272
273
            # Sort ascending according to weight (original order)
274
            push @{$merged_record_fields{$tag}}, map { $_->[1] } sort { $a->[0] <=> $b->[0] } @merged_fields;
275
        }
276
    }
277
278
    my $merged_record = MARC::Record->new();
279
280
    # Leader is always overwritten, or kept???
281
    $merged_record->leader($incoming_record->leader());
282
283
    if (%merged_record_fields) {
284
        foreach my $tag (sort keys %merged_record_fields) {
285
            $merged_record->append_fields(@{$merged_record_fields{$tag}});
286
        }
287
    }
288
    return $merged_record;
289
}
290
291
sub _clear_caches {
292
    $cache->clear_from_cache('marc_merge_rules');
293
}
294
295
=head2 find_or_create
296
297
Override C<find_or_create> to clear marc merge rules cache.
298
299
=cut
300
301
sub find_or_create {
302
    my $self = shift @_;
303
    $self->_clear_caches();
304
    return $self->SUPER::find_or_create(@_);
305
}
306
307
=head2 update
308
309
Override C<update> to clear marc merge rules cache.
310
311
=cut
312
313
sub update {
314
    my $self = shift @_;
315
    $self->_clear_caches();
316
    return $self->SUPER::update(@_);
317
}
318
319
=head2 delete
320
321
Override C<delete> to clear marc merge rules cache.
322
323
=cut
324
325
sub delete {
326
    my $self = shift @_;
327
    $self->_clear_caches();
328
    return $self->SUPER::delete(@_);
329
}
330
331
=head2 validate
332
333
    Koha::MarcMergeRules->validate($rule_data);
334
335
Validates C<$rule_data>. Throws C<Koha::Exceptions::MarcMergeRule::InvalidTagRegExp>
336
if C<$rule_data->{tag}> contains an invalid regular expression. Throws
337
C<Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions> if contains invalid
338
combination of actions for control fields. Otherwise returns true.
339
340
=cut
341
342
sub validate {
343
    my ($self, $rule_data) = @_;
344
345
    if(exists $rule_data->{tag}) {
346
        if ($rule_data->{tag} ne '*') {
347
            eval { qr/$rule_data->{tag}/ };
348
            if ($@) {
349
                Koha::Exceptions::MarcMergeRule::InvalidTagRegExp->throw(
350
                    "Invalid tag regular expression"
351
                );
352
            }
353
        }
354
        # TODO: Regexp or '*' that match controlfield not currently detected
355
        if (
356
            looks_like_number($rule_data->{tag}) &&
357
            $rule_data->{tag} < 10 &&
358
            $rule_data->{append} &&
359
            !$rule_data->{remove}
360
        ) {
361
            Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions->throw(
362
                "Combination of allow append and skip remove not permitted for control fields"
363
            );
364
        }
365
    }
366
    return 1;
367
}
368
369
sub _type {
370
    return 'MarcMergeRule';
371
}
372
373
=head3 object_class
374
375
=cut
376
377
sub object_class {
378
    return 'Koha::MarcMergeRule';
379
}
380
381
1;
(-)a/admin/marc-merge-rules.pl (+162 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
# standard or CPAN modules used
21
use CGI qw ( -utf8 );
22
use CGI::Cookie;
23
use MARC::File::USMARC;
24
use Try::Tiny;
25
26
# Koha modules used
27
use C4::Context;
28
use C4::Koha;
29
use C4::Auth;
30
use C4::AuthoritiesMarc;
31
use C4::Output;
32
use C4::Biblio;
33
use C4::ImportBatch;
34
use C4::Matcher;
35
use C4::BackgroundJob;
36
use C4::Labels::Batch;
37
use Koha::MarcMergeRules;
38
use Koha::MarcMergeRule;
39
use Koha::Patron::Categories; # TODO: Required? Try without use
40
41
my $script_name = "/cgi-bin/koha/admin/marc-merge-rules.pl";
42
43
my $input = new CGI;
44
my $op = $input->param('op') || '';
45
my $errors = [];
46
47
my $rule_from_cgi = sub {
48
    my ($cgi) = @_;
49
50
    my %rule = map { $_ => scalar $cgi->param($_) } (
51
        'tag',
52
        'module',
53
        'filter',
54
        'add',
55
        'append',
56
        'remove',
57
        'delete'
58
    );
59
60
    my $id = $cgi->param('id');
61
    if ($id) {
62
        $rule{id} = $id;
63
    }
64
65
    return \%rule;
66
};
67
68
my ($template, $loggedinuser, $cookie) = get_template_and_user(
69
    {
70
        template_name   => "admin/marc-merge-rules.tt",
71
        query           => $input,
72
        type            => "intranet",
73
        authnotrequired => 0,
74
        flagsrequired   => { parameters => 'manage_marc_merge_rules' },
75
        debug           => 1,
76
    }
77
);
78
79
$template->param(script_name => $script_name);
80
81
my %cookies = parse CGI::Cookie($cookie);
82
our $sessionID = $cookies{'CGISESSID'}->value;
83
84
my $get_rules = sub {
85
    # TODO: order?
86
    return [map { { $_->get_columns() } } Koha::MarcMergeRules->_resultset->all];
87
};
88
my $rules;
89
90
if ($op eq 'remove' || $op eq 'doremove') {
91
    my @remove_ids = $input->multi_param('batchremove');
92
    push @remove_ids, scalar $input->param('id') if $input->param('id');
93
    if ($op eq 'remove') {
94
        $template->{VARS}->{removeConfirm} = 1;
95
        my %remove_ids = map { $_ => undef } @remove_ids;
96
        $rules = $get_rules->();
97
        for my $rule (@{$rules}) {
98
            $rule->{'removemarked'} = 1 if exists $remove_ids{$rule->{id}};
99
        }
100
    }
101
    elsif ($op eq 'doremove') {
102
        my @remove_ids = $input->multi_param('batchremove');
103
        push @remove_ids, scalar $input->param('id') if $input->param('id');
104
        Koha::MarcMergeRules->search({ id => { in => \@remove_ids } })->delete();
105
        $rules = $get_rules->();
106
    }
107
}
108
elsif ($op eq 'edit') {
109
    $template->{VARS}->{edit} = 1;
110
    my $id = $input->param('id');
111
    $rules = $get_rules->();
112
    for my $rule(@{$rules}) {
113
        if ($rule->{id} == $id) {
114
            $rule->{'edit'} = 1;
115
            last;
116
        }
117
    }
118
}
119
elsif ($op eq 'doedit' || $op eq 'add') {
120
    my $rule_data = $rule_from_cgi->($input);
121
    if (!@{$errors}) {
122
        try {
123
            Koha::MarcMergeRules->validate($rule_data);
124
        }
125
        catch {
126
            die $_ unless blessed $_ && $_->can('rethrow');
127
128
            if ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp')) {
129
                push @{$errors}, {
130
                    type => 'error',
131
                    code => 'invalid_tag_regexp',
132
                    tag => $rule_data->{tag},
133
                };
134
            }
135
            elsif ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions')) {
136
                push @{$errors}, {
137
                    type => 'error',
138
                    code => 'invalid_control_field_actions',
139
                    tag => $rule_data->{tag},
140
                };
141
            }
142
            else {
143
                $_->rethrow;
144
            }
145
        };
146
        if (!@{$errors}) {
147
            my $rule = Koha::MarcMergeRules->find_or_create($rule_data);
148
            # Need to call set and store here in case we have an update
149
            $rule->set($rule_data);
150
            $rule->store();
151
        }
152
        $rules = $get_rules->();
153
    }
154
}
155
else {
156
    $rules = $get_rules->();
157
}
158
159
my $categorycodes = Koha::Patron::Categories->search_limited({}, {order_by => ['description']});
160
$template->param( rules => $rules, categorycodes => $categorycodes, messages => $errors );
161
162
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/cataloguing/addbiblio.pl (-1 / +11 lines)
Lines 41-46 use Koha::ItemTypes; Link Here
41
use Koha::Libraries;
41
use Koha::Libraries;
42
42
43
use Koha::BiblioFrameworks;
43
use Koha::BiblioFrameworks;
44
use Koha::Patrons;
44
45
45
use Date::Calc qw(Today);
46
use Date::Calc qw(Today);
46
use MARC::File::USMARC;
47
use MARC::File::USMARC;
Lines 850-856 if ( $op eq "addbiblio" ) { Link Here
850
    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
851
    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
851
        my $oldbibitemnum;
852
        my $oldbibitemnum;
852
        if ( $is_a_modif ) {
853
        if ( $is_a_modif ) {
853
            ModBiblio( $record, $biblionumber, $frameworkcode );
854
            my $member = Koha::Patrons->find($loggedinuser);
855
            ModBiblio( $record, $biblionumber, $frameworkcode, {
856
                    context => {
857
                        source => $z3950 ? 'z39.50' : 'intranet',
858
                        categorycode => $member->categorycode,
859
                        userid => $member->userid
860
                    }
861
                }
862
            );
854
        }
863
        }
855
        else {
864
        else {
856
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
865
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
Lines 943-948 elsif ( $op eq "delete" ) { Link Here
943
    $template->param(
952
    $template->param(
944
        biblionumberdata => $biblionumber,
953
        biblionumberdata => $biblionumber,
945
        op               => $op,
954
        op               => $op,
955
        z3950            => $z3950
946
    );
956
    );
947
    if ( $op eq "duplicate" ) {
957
    if ( $op eq "duplicate" ) {
948
        $biblionumber = "";
958
        $biblionumber = "";
(-)a/installer/data/mysql/atomicupdate/bug_14957-marc-merge-rules.perl (+41 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    my $sql = q{
4
      CREATE TABLE IF NOT EXISTS `marc_merge_rules` (
5
        `id` int(11) NOT NULL auto_increment,
6
        `tag` varchar(255) NOT NULL,
7
        `module` varchar(127) NOT NULL,
8
        `filter` varchar(255) NOT NULL,
9
        `add` tinyint NOT NULL,
10
        `append` tinyint NOT NULL,
11
        `remove` tinyint NOT NULL,
12
        `delete` tinyint NOT NULL,
13
        PRIMARY KEY(`id`)
14
      );
15
    };
16
    $dbh->do( $sql );
17
18
    $sql = q{
19
      INSERT IGNORE INTO systempreferences (`variable`, `value`, `options`, `explanation`, `type`) VALUES (
20
        'MARCMergeRules',
21
        '0',
22
        NULL,
23
        'Use the MARC merge rules system to decide what actions to take for each field when modifying records.',
24
        'YesNo'
25
      );
26
    };
27
    $dbh->do( $sql );
28
29
    $sql = q{
30
      INSERT IGNORE INTO permissions (module_bit, code, description) VALUES (
31
        3,
32
        'manage_marc_merge_rules',
33
        'Manage MARC merge rules configuration'
34
      );
35
    };
36
    $dbh->do( $sql );
37
38
    # Always end with this (adjust the bug info)
39
    SetVersion( $DBversion );
40
    print "Upgrade to $DBversion done (Bug 14957 - Write protecting MARC fields based on source of import)\n";
41
}
(-)a/installer/data/mysql/kohastructure.sql (+30 lines)
Lines 3382-3387 CREATE TABLE `marc_matchers` ( Link Here
3382
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3382
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3383
/*!40101 SET character_set_client = @saved_cs_client */;
3383
/*!40101 SET character_set_client = @saved_cs_client */;
3384
3384
3385
--
3386
-- Table structure for table `marc_merge_rules_modules`
3387
--
3388
3389
DROP TABLE IF EXISTS `marc_merge_rules_modules`;
3390
CREATE TABLE `marc_merge_rules_modules` (
3391
  `name` varchar(127) NOT NULL,
3392
  `description` varchar(255),
3393
  `specificity` int(11) NOT NULL UNIQUE,
3394
  PRIMARY KEY(`name`)
3395
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3396
3397
--
3398
-- Table structure for table `marc_merge_rules`
3399
--
3400
3401
DROP TABLE IF EXISTS `marc_merge_rules`;
3402
CREATE TABLE IF NOT EXISTS `marc_merge_rules` (
3403
  `id` int(11) NOT NULL auto_increment,
3404
  `tag` varchar(255) NOT NULL, -- can be regexp, so need > 3 chars
3405
  `module` varchar(127) NOT NULL,
3406
  `filter` varchar(255) NOT NULL,
3407
  `add` tinyint NOT NULL,
3408
  `append` tinyint NOT NULL,
3409
  `remove` tinyint NOT NULL,
3410
  `delete` tinyint NOT NULL,
3411
  PRIMARY KEY(`id`),
3412
  CONSTRAINT `marc_merge_rules_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_merge_rules_modules` (`name`) ON DELETE CASCADE ON UPDATE CASCADE
3413
);
3414
3385
--
3415
--
3386
-- Table structure for table `marc_modification_template_actions`
3416
-- Table structure for table `marc_modification_template_actions`
3387
--
3417
--
(-)a/installer/data/mysql/mandatory/sysprefs.sql (+1 lines)
Lines 323-328 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
323
('MarcFieldForModifierName','',NULL,'Where to store the name of the record''s last modifier','Free'),
323
('MarcFieldForModifierName','',NULL,'Where to store the name of the record''s last modifier','Free'),
324
('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'),
324
('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'),
325
('MarcItemFieldsToOrder','',NULL,'Set the mapping values for new item records created from a MARC record in a staged file. In a YAML format.','textarea'),
325
('MarcItemFieldsToOrder','',NULL,'Set the mapping values for new item records created from a MARC record in a staged file. In a YAML format.','textarea'),
326
('MARCMergeRules','0',NULL,'Use the MARC merge rules system to decide what actions to take for each field when modifying records.','YesNo'),
326
('MarkLostItemsAsReturned','batchmod,moredetail,cronjob,additem,pendingreserves,onpayment','claim_returned|batchmod|moredetail|cronjob|additem|pendingreserves|onpayment','Mark items as returned when flagged as lost','multiple'),
327
('MarkLostItemsAsReturned','batchmod,moredetail,cronjob,additem,pendingreserves,onpayment','claim_returned|batchmod|moredetail|cronjob|additem|pendingreserves|onpayment','Mark items as returned when flagged as lost','multiple'),
327
('MARCOrgCode','OSt','','Define MARC Organization Code for MARC21 records - http://www.loc.gov/marc/organizations/orgshome.html','free'),
328
('MARCOrgCode','OSt','','Define MARC Organization Code for MARC21 records - http://www.loc.gov/marc/organizations/orgshome.html','free'),
328
('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'),
329
('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'),
(-)a/installer/data/mysql/mandatory/userpermissions.sql (+1 lines)
Lines 25-30 INSERT INTO permissions (module_bit, code, description) VALUES Link Here
25
   ( 3, 'manage_oai_sets', 'Manage OAI sets'),
25
   ( 3, 'manage_oai_sets', 'Manage OAI sets'),
26
   ( 3, 'manage_item_search_fields', 'Manage item search fields'),
26
   ( 3, 'manage_item_search_fields', 'Manage item search fields'),
27
   ( 3, 'manage_search_engine_config', 'Manage search engine configuration'),
27
   ( 3, 'manage_search_engine_config', 'Manage search engine configuration'),
28
   ( 3, 'manage_marc_merge_rules', 'Manage MARC merge rules configuration'),
28
   ( 3, 'manage_search_targets', 'Manage Z39.50 and SRU server configuration'),
29
   ( 3, 'manage_search_targets', 'Manage Z39.50 and SRU server configuration'),
29
   ( 3, 'manage_didyoumean', 'Manage Did you mean? configuration'),
30
   ( 3, 'manage_didyoumean', 'Manage Did you mean? configuration'),
30
   ( 3, 'manage_column_config', 'Manage column configuration'),
31
   ( 3, 'manage_column_config', 'Manage column configuration'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc (+3 lines)
Lines 93-98 Link Here
93
            [% IF ( CAN_user_parameters_manage_search_engine_config ) %]
93
            [% IF ( CAN_user_parameters_manage_search_engine_config ) %]
94
                <li><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></li>
94
                <li><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></li>
95
            [% END %]
95
            [% END %]
96
            [% IF ( CAN_user_parameters_manage_marc_merge_rules ) %]
97
                <li><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></li>
98
            [% END %]
96
        </ul>
99
        </ul>
97
    [% END %]
100
    [% END %]
98
101
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc (+5 lines)
Lines 210-215 Link Here
210
            Manage search engine configuration
210
            Manage search engine configuration
211
        </span>
211
        </span>
212
        <span class="permissioncode">([% name | html %])</span>
212
        <span class="permissioncode">([% name | html %])</span>
213
    [%- CASE 'manage_marc_merge_rules' -%]
214
        <span class="sub_permission manage_marc_merge_rules_subpermission">
215
          Manage MARC merge rules configuration
216
        </span>
217
        <span class="permissioncode">([% name | html %])</span>
213
    [%- CASE 'manage_search_targets' -%]
218
    [%- CASE 'manage_search_targets' -%]
214
        <span class="sub_permission manage_search_targets_subpermission">
219
        <span class="sub_permission manage_search_targets_subpermission">
215
            Manage Z39.50 and SRU server configuration
220
            Manage Z39.50 and SRU server configuration
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+2 lines)
Lines 172-177 Link Here
172
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></dt>
172
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration (Elasticsearch)</a></dt>
173
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields</dd>
173
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields</dd>
174
                    [% END %]
174
                    [% END %]
175
                    <dt><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></dt>
176
                    <dd>Managed MARC field merge rules</dd>
175
                </dl>
177
                </dl>
176
            [% END %]
178
            [% END %]
177
179
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc-merge-rules.tt (+519 lines)
Line 0 Link Here
1
[% USE Koha %]
2
[% INCLUDE 'doc-head-open.inc' %]
3
<title>Koha &rsaquo; Administration &rsaquo; MARC merge rules</title>
4
[% INCLUDE 'doc-head-close.inc' %]
5
[% Asset.css("css/datatables.css") %]
6
[% INCLUDE 'datatables.inc' %]
7
8
<style type="text/css">
9
    .required {
10
        background-color: #C00;
11
    }
12
</style>
13
14
<script type="text/javascript">
15
function doSubmit(op, id) {
16
    $('<input type="hidden"/>')
17
    .attr('name', 'op')
18
    .attr('value', op)
19
    .appendTo('#marc-merge-rules-form');
20
21
    if(id) {
22
        $('<input type="hidden"/>')
23
        .attr('name', 'id')
24
        .attr('value', id)
25
        .appendTo('#marc-merge-rules-form');
26
    }
27
28
    var valid = true;
29
    if (op == 'add' || op == 'edit') {
30
        var validate = [
31
            $('#marc-merge-rules-form input[name="filter"]'),
32
            $('#marc-merge-rules-form input[name="tag"]')
33
        ];
34
        for(var i = 0; i < validate.length; i++) {
35
            if (validate[i].length) {
36
                if(validate[i].val().length == 0) {
37
                    validate[i].addClass('required');
38
                    valid = false;
39
                } else {
40
                    validate[i].removeClass('required');
41
                }
42
            }
43
        }
44
    }
45
46
    if (valid) {
47
        $('#marc-merge-rules-form').submit();
48
    }
49
50
    return valid;
51
}
52
53
$(document).ready(function(){
54
    $('#doremove').on('click', function(){
55
        doSubmit('doremove');
56
    });
57
    $('#doedit').on('click', function(){
58
        doSubmit('doedit', $("#doedit").attr('value'));
59
    });
60
    $('#add').on('click', function(){
61
        doSubmit('add');
62
        return false;
63
    });
64
    $('#btn_batchremove').on('click', function(){
65
        doSubmit('remove');
66
    });
67
68
    /* Disable batch remove unless one or more checkboxes are checked */
69
    $('input[name="batchremove"]').change(function() {
70
        if($('input[name="batchremove"]:checked').length > 0) {
71
            $('#btn_batchremove').removeAttr('disabled');
72
        } else {
73
            $('#btn_batchremove').attr('disabled', 'disabled');
74
        }
75
    });
76
77
    $.fn.dataTable.ext.order['dom-input'] = function (settings, col) {
78
        return this.api().column(col, { order: 'index' }).nodes()
79
            .map(function (td, i) {
80
                if($('input', td).val() != undefined) {
81
                    return $('input', td).val();
82
                } else if($('select', td).val() != undefined) {
83
                    return $('option[selected="selected"]', td).val();
84
                } else {
85
                    return $(td).html();
86
                }
87
            });
88
    }
89
90
    $('#marc-merge-rules').dataTable($.extend(true, {}, dataTablesDefaults, {
91
        "aoColumns": [
92
            {"bSearchable": false, "bSortable": false},
93
            {"sSortDataType": "dom-input"},
94
            {"sSortDataType": "dom-input"},
95
            {"bSearchable": false, "sSortDataType": "dom-input"},
96
            {"bSearchable": false, "sSortDataType": "dom-input"},
97
            {"bSearchable": false, "sSortDataType": "dom-input"},
98
            {"bSearchable": false, "sSortDataType": "dom-input"},
99
            {"bSearchable": false, "sSortDataType": "dom-input"},
100
            {"bSearchable": false, "sSortDataType": "dom-input"},
101
            {"bSearchable": false, "bSortable": false},
102
            {"bSearchable": false, "bSortable": false}
103
        ],
104
        "pagingType": "simple"
105
    }));
106
107
    var merge_rules_presets = {};
108
    merge_rules_presets[_("Protect")] = {
109
      'add': 0,
110
      'append': 0,
111
      'remove': 0,
112
      'delete': 0
113
    };
114
    merge_rules_presets[_("Overwrite")] = {
115
      'add': 1,
116
      'append': 1,
117
      'remove': 1,
118
      'delete': 1
119
    };
120
    merge_rules_presets[_("Add new")] = {
121
      'add': 1,
122
      'append': 0,
123
      'remove': 0,
124
      'delete': 0
125
    };
126
    merge_rules_presets[_("Add and append")] = {
127
      'add': 1,
128
      'append': 1,
129
      'remove': 0,
130
      'delete': 0
131
    };
132
    merge_rules_presets[_("Protect from deletion")] = {
133
      'add': 1,
134
      'append': 1,
135
      'remove': 1,
136
      'delete': 0
137
    };
138
139
    var merge_rules_label_to_value = {};
140
    merge_rules_label_to_value[_("Add")] = 1;
141
    merge_rules_label_to_value[_("Append")] = 1;
142
    merge_rules_label_to_value[_("Remove")] = 1;
143
    merge_rules_label_to_value[_("Delete")] = 1;
144
    merge_rules_label_to_value[_("Skip")] = 0;
145
146
    function hash_config(config) {
147
      return JSON.stringify(config, Object.keys(config).sort());
148
    }
149
150
    var merge_rules_preset_map = {};
151
    $.each(merge_rules_presets, function(preset, config) {
152
      merge_rules_preset_map[hash_config(config)] = preset;
153
    });
154
155
    function operations_config_merge_rule_preset(config) {
156
      return merge_rules_preset_map[hash_config(config)] || '';
157
    }
158
159
    /* Set preset values according to operation config */
160
    $('.rule').each(function() {
161
      var $this = $(this);
162
      var operations_config = {};
163
      $('.rule-operation-action', $this).each(function() {
164
        var $operation = $(this);
165
        operations_config[$operation.data('operation')] = merge_rules_label_to_value[$operation.text()];
166
      });
167
      $('.rule-preset', $this).text(
168
        operations_config_merge_rule_preset(operations_config) || _("Custom")
169
      );
170
    });
171
172
    /* Listen to operations config changes and set presets accordingly */
173
    $('.rule-operation-action-edit select').change(function() {
174
      var operations_config = {};
175
      var $parent_row = $(this).closest('tr');
176
      $('.rule-operation-action-edit select', $parent_row).each(function() {
177
        var $this = $(this);
178
        operations_config[$this.attr('name')] = parseInt($this.val());
179
      });
180
      $('select[name="preset"]', $parent_row).val(
181
          operations_config_merge_rule_preset(operations_config)
182
      );
183
    });
184
185
    /* Listen to preset changes and set operations config accordingly */
186
    $('select[name="preset"]').change(function() {
187
      var $this = $(this);
188
      var $parent_row = $this.closest('tr');
189
      var preset = $this.val();
190
      if (preset) {
191
        $.each(merge_rules_presets[preset], function(operation, action) {
192
          $('select[name="' + operation + '"]', $parent_row).val(action);
193
        });
194
      }
195
    });
196
197
    var module_filter_options = {
198
      source: {
199
        '*': '*',
200
        batchmod: "Batch record modification",
201
        intranet: "Staff client MARC editor",
202
        batchimport: "Staged MARC import",
203
        z3950: "Z39.50",
204
        bulkmarcimport: "bulkmarcimport.pl",
205
        import_lexile: "import_lexile.pl"
206
      },
207
      categorycode: {
208
        '*': '*',
209
        [% FOREACH categorycode IN categorycodes %]
210
          [% categorycode.categorycode | html %]: "[% categorycode.description | html %]",
211
        [% END %]
212
      }
213
    };
214
215
    //Kind of hack: Replace filter value with label when one exist
216
    $('.rule-module').each(function() {
217
      var $this = $(this);
218
      var module = $this.text();
219
      if (module in module_filter_options) {
220
        let $filter = $this.siblings('.rule-filter');
221
        if ($filter.text() in module_filter_options[module]) {
222
          $filter.text(module_filter_options[module][$filter.text()]);
223
        }
224
      }
225
    });
226
227
    var $filter_container = $('#filter-container');
228
229
    /* Listen to module changes and set filter input accordingly */
230
    $('select[name="module"]').change(function() {
231
      var $this = $(this);
232
      var module_name = $this.val();
233
234
      /* Remove current element if any */
235
      $filter_container.empty();
236
237
      var filter_elem = null;
238
      if (module_name in module_filter_options) {
239
        // Create select element
240
        filter_elem = document.createElement('select');
241
        for (var filter_value in module_filter_options[module_name]) {
242
          var option = document.createElement('option');
243
          option.value = filter_value;
244
          option.text = module_filter_options[module_name][filter_value];
245
          filter_elem.appendChild(option);
246
        }
247
      }
248
      else {
249
        // Create text input element
250
        filter_elem = document.createElement('input');
251
        filter_elem.type = 'text';
252
        filter_elem.setAttribute('size', 5);
253
      }
254
      filter_elem.name = 'filter';
255
      filter_elem.id = 'filter';
256
      $filter_container.append(filter_elem);
257
    }).change(); // Trigger change
258
259
    // Hack: set value if editing rule
260
    if ($filter_container.data('filter')) {
261
      $('#filter').val($filter_container.data('filter'));
262
    }
263
264
});
265
</script>
266
</head>
267
<body id="admin_marc-merge-rules" class="admin">
268
[% INCLUDE 'header.inc' %]
269
[% INCLUDE 'cat-search.inc' %]
270
271
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
272
 &rsaquo; MARC merge rules
273
</div>
274
275
<div class="main container-fluid">
276
<div class="row">
277
<div class="col-sm-10 col-sm-push-2">
278
279
<h1>Manage MARC merge rules</h1>
280
281
[% FOR m IN messages %]
282
  <div class="dialog [% m.type | html %]">
283
    [% SWITCH m.code %]
284
    [% CASE 'invalid_tag_regexp' %]
285
      Invalid regular expression "[% m.tag | html %]".
286
    [% CASE 'invalid_control_field_actions' %]
287
      Invalid combination of actions for tag [% m.tag | html %]. Control field rules do not allow "Appended: Append" and "Removed: Skip".
288
    [% CASE %]
289
      [% m.code | html %]
290
    [% END %]
291
  </div>
292
[% END %]
293
294
[% UNLESS Koha.Preference( 'MARCMergeRules' ) %]
295
    <div class="dialog message">
296
        The <b>MARCMergeRules</b> preference is not set, don't forget to enable it for rules to take effect.
297
    </div>
298
[% END %]
299
[% IF removeConfirm %]
300
<div class="dialog alert">
301
<h3>Remove rule?</h3>
302
<p>Are you sure you want to remove the selected rule(s)?</p>
303
304
<form action="[% script_name %]" method="GET">
305
    <button type="submit" class="deny"><i class="fa fa-fw fa-remove"></i> No, do not remove</button>
306
</form>
307
    <button type="button" class="approve" id="doremove"><i class="fa fa-fw fa-check"></i> Yes, remove</button>
308
</div>
309
[% END %]
310
311
<form action="[% script_name %]" method="POST" id="marc-merge-rules-form">
312
<table id="marc-merge-rules">
313
    <thead><tr>
314
        <th>Rule</th>
315
        <th>Module</th>
316
        <th>Filter</th>
317
        <th>Tag</th>
318
        <th>Preset</th>
319
        <th>Added</th>
320
        <th>Appended</th>
321
        <th>Removed</th>
322
        <th>Deleted</th>
323
        <th>Actions</th>
324
        <th>&nbsp;</th>
325
    </tr></thead>
326
    [% UNLESS edit %]
327
    <tfoot>
328
        <tr class="rule-new">
329
            <th>&nbsp;</th>
330
            <th>
331
                <select name="module">
332
                    <option value="source">Source</option>
333
                    <option value="categorycode">User category</option>
334
                    <option value="userid">Username</option>
335
                </select>
336
            </th>
337
            <th id="filter-container"></th>
338
            <th><input type="text" size="5" name="tag"/></th>
339
            <th>
340
                <select name="preset">
341
                    <option value="" selected>Custom</option>
342
                    [% FOR preset IN [
343
                            'Protect',
344
                            'Overwrite',
345
                            'Add new',
346
                            'Add and append',
347
                            'Protect from deletion'
348
                        ]
349
                    %]
350
                        <option value="[% preset | html %]">[% preset | html %]</option>
351
                    [% END %]
352
                </select>
353
            </th>
354
            <th class="rule-operation-action-edit">
355
                <select name="add">
356
                    <option value="0">Skip</option>
357
                    <option value="1">Add</option>
358
                </select>
359
            </th>
360
            <th class="rule-operation-action-edit">
361
                <select name="append">
362
                    <option value="0">Skip</option>
363
                    <option value="1">Append</option>
364
                </select>
365
            </th>
366
            <th class="rule-operation-action-edit">
367
                <select name="remove">
368
                    <option value="0">Skip</option>
369
                    <option value="1">Remove</option>
370
                </select>
371
            </th>
372
            <th class="rule-operation-action-edit">
373
                <select name="delete">
374
                    <option value="0">Skip</option>
375
                    <option value="1">Delete</option>
376
                </select>
377
            </th>
378
            <th><button class="btn btn-sm" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
379
            <th><button id="btn_batchremove" disabled="disabled" class="btn btn-sm" title="Batch remove"><i class="fa fa-trash"></i> Delete selected</button></th>
380
        </tr>
381
    </tfoot>
382
    [% END %]
383
    <tbody>
384
        [% FOREACH rule IN rules %]
385
            <tr id="[% rule.id %]" class="rule[% IF rule.edit %]-edit[% END %]">
386
            [% IF rule.edit %]
387
                <td>[% rule.id %]</td>
388
                <td>
389
                    <select name="module">
390
                        [% IF rule.module == "source" %]
391
                            <option value="source" selected="selected">Source</option>
392
                        [% ELSE %]
393
                            <option value="source">Source</option>
394
                        [% END %]
395
                        [% IF rule.module == "categorycode" %]
396
                            <option value="categorycode" selected="selected">User category</option>
397
                        [% ELSE %]
398
                            <option value="categorycode">User category</option>
399
                        [% END %]
400
                        [% IF rule.module == "userid" %]
401
                            <option value="userid" selected="selected">Username</option>
402
                        [% ELSE %]
403
                            <option value="userid">Username</option>
404
                        [% END %]
405
                    </select>
406
                </td>
407
                <td id="filter-container" data-filter="[% rule.filter | html %]"></td>
408
                <td><input type="text" size="3" name="tag" value="[% rule.tag | html %]"/></td>
409
                <th>
410
                    <select name="preset">
411
                        <option value="" selected>Custom</option>
412
                        [% FOR preset IN [
413
                                'Protect',
414
                                'Overwrite',
415
                                'Add new',
416
                                'Add and append',
417
                                'Protect from deletion'
418
                            ]
419
                        %]
420
                            <option value="[% preset | html %]">[% preset | html %]</option>
421
                        [% END %]
422
                    </select>
423
                </th>
424
                <td class="rule-operation-action-edit">
425
                    <select name="add">
426
                        [% IF rule.add %]
427
                            <option value="0">Skip</option>
428
                            <option value="1" selected="selected">Add</option>
429
                        [% ELSE %]
430
                            <option value="0" selected="selected">Skip</option>
431
                            <option value="1">Add</option>
432
                        [% END %]
433
                    </select>
434
                </td>
435
                <td class="rule-operation-action-edit">
436
                    <select name="append">
437
                        [% IF rule.append %]
438
                            <option value="0">Skip</option>
439
                            <option value="1" selected="selected">Append</option>
440
                        [% ELSE %]
441
                            <option value="0" selected="selected">Skip</option>
442
                            <option value="1">Append</option>
443
                        [% END %]
444
                    </select>
445
                </td>
446
                <td class="rule-operation-action-edit">
447
                    <select name="remove">
448
                        [% IF rule.remove %]
449
                            <option value="0">Skip</option>
450
                            <option value="1" selected="selected">Remove</option>
451
                        [% ELSE %]
452
                            <option value="0" selected="selected">Skip</option>
453
                            <option value="1">Remove</option>
454
                        [% END %]
455
                    </select>
456
                </td>
457
                <td class="rule-operation-action-edit">
458
                    <select name="delete">
459
                        [% IF rule.delete %]
460
                            <option value="0">Skip</option>
461
                            <option value="1" selected="selected">Delete</option>
462
                        [% ELSE %]
463
                            <option value="0" selected="selected">Skip</option>
464
                            <option value="1">Delete</option>
465
                        [% END %]
466
                    </select>
467
                </td>
468
                <td class="actions">
469
                    <button class="btn btn-sm" title="Save" id="doedit" value="[% rule.id | html %]"><i class="fa fa-check"></i> Save</button>
470
                    <button type="submit" class="btn btn-sm" title="Cancel" ><i class="fa fa-times"></i> Cancel</button>
471
                </td>
472
                <td></td>
473
            [% ELSE %]
474
                <td>[% rule.id | html %]</td>
475
                <td class="rule-module">[% rule.module | html %]</td>
476
                <td class="rule-filter">[% rule.filter | html %]</td>
477
                <td>[% rule.tag | html %]</td>
478
                <td class="rule-preset"></td>
479
                <td class="rule-operation-action" data-operation="add">[% IF rule.add %]Add[% ELSE %]Skip[% END %]</td>
480
                <td class="rule-operation-action" data-operation="append">[% IF rule.append %]Append[% ELSE %]Skip[% END %]</td>
481
                <td class="rule-operation-action" data-operation="remove">[% IF rule.remove %]Remove[% ELSE %]Skip[% END %]</td>
482
                <td class="rule-operation-action" data-operation="delete">[% IF rule.delete %]Delete[% ELSE %]Skip[% END %]</td>
483
                <td class="actions">
484
                    <a href="?op=remove&id=[% rule.id %]" title="Delete" class="btn btn-sm"><i class="fa fa-trash"></i> Delete</a>
485
                    <a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-sm"><i class="fa fa-pencil"></i> Edit</a>
486
                </td>
487
                <td>
488
                    [% IF rule.removemarked %]
489
                        <input type="checkbox" name="batchremove" value="[% rule.id | html %]" checked="checked"/>
490
                    [% ELSE %]
491
                        <input type="checkbox" name="batchremove" value="[% rule.id | html %]"/>
492
                    [% END %]
493
                </td>
494
            [% END %]
495
            </tr>
496
        [% END %]
497
    </tbody>
498
</table>
499
</form>
500
501
<form action="[% script_name %]" method="post">
502
<input type="hidden" name="op" value="redo-matching" />
503
</form>
504
505
</div>
506
<!-- /.col-sm-10.col-sm-push-2 -->
507
508
<div class="col-sm-2 col-sm-pull-10">
509
    <aside>
510
        [% INCLUDE 'admin-menu.inc' %]
511
    </aside>
512
</div>
513
514
</div>
515
<!-- /.row>
516
</div>
517
<!-- /main container-fluid -->
518
519
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref (+7 lines)
Lines 305-307 Cataloging: Link Here
305
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
305
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
306
            - "<br/>"
306
            - "<br/>"
307
            - "Use of TY ( record type ) as a key will <em>replace</em> the default TY with the field value of your choosing."
307
            - "Use of TY ( record type ) as a key will <em>replace</em> the default TY with the field value of your choosing."
308
        -
309
            - When importing records
310
            - pref: MARCMergeRules
311
              choices:
312
                  yes: "use"
313
                  no: "don't use"
314
            - MARC merge rules to decide which action to take for each field.
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt (+1 lines)
Lines 911-916 function PopupMARCFieldDoc(field) { Link Here
911
                [% END %]
911
                [% END %]
912
                <input type="hidden" name="op" value="addbiblio" />
912
                <input type="hidden" name="op" value="addbiblio" />
913
                <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
913
                <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
914
                <input type="hidden" name="z3950" value="[% z3950 | html %]" />
914
                <input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
915
                <input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
915
                <input type="hidden" name="breedingid" value="[% breedingid | html %]" />
916
                <input type="hidden" name="breedingid" value="[% breedingid | html %]" />
916
                <input type="hidden" name="changed_framework" value="" />
917
                <input type="hidden" name="changed_framework" value="" />
(-)a/misc/link_bibs_to_authorities.pl (-1 / +1 lines)
Lines 229-235 sub process_bib { Link Here
229
            );
229
            );
230
        }
230
        }
231
        if ( not $test_only ) {
231
        if ( not $test_only ) {
232
            ModBiblio( $bib, $biblionumber, $frameworkcode, 1 );
232
            ModBiblio( $bib, $biblionumber, $frameworkcode, { disable_autolink => 1 });
233
            #Last param is to note ModBiblio was called from linking script and bib should not be linked again
233
            #Last param is to note ModBiblio was called from linking script and bib should not be linked again
234
            $num_bibs_modified++;
234
            $num_bibs_modified++;
235
        }
235
        }
(-)a/misc/migration_tools/bulkmarcimport.pl (-1 / +1 lines)
Lines 446-452 RECORD: while ( ) { Link Here
446
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
446
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
447
                };
447
                };
448
                if ($update) {
448
                if ($update) {
449
                    eval { ModBiblio( $record, $biblionumber, $framework ) };
449
                    eval { ModBiblio( $record, $biblionumber, $framework, { context => { source => 'bulkmarcimport' } } ) };
450
                    if ($@) {
450
                    if ($@) {
451
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
451
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
452
                        printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);
452
                        printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);
(-)a/misc/migration_tools/import_lexile.pl (-1 / +1 lines)
Lines 203-209 while ( my $row = $csv->getline_hr($fh) ) { Link Here
203
            $record->append_fields($field);
203
            $record->append_fields($field);
204
        }
204
        }
205
205
206
        ModBiblio( $record, $biblionumber ) unless ( $test );
206
        ModBiblio( $record, $biblionumber, undef, { context => { source => 'import_lexile' } } ) unless ( $test );
207
    }
207
    }
208
208
209
}
209
}
(-)a/t/db_dependent/Biblio.t (-1 / +1 lines)
Lines 718-724 subtest 'ModBiblio called from linker test' => sub { Link Here
718
    C4::Biblio::ModBiblio($record,$biblionumber,'');
718
    C4::Biblio::ModBiblio($record,$biblionumber,'');
719
    is($called,1,"We called to link bibs because not from linker");
719
    is($called,1,"We called to link bibs because not from linker");
720
    $called = 0;
720
    $called = 0;
721
    C4::Biblio::ModBiblio($record,$biblionumber,'',1);
721
    C4::Biblio::ModBiblio($record,$biblionumber,'',{ disable_autolink => 1 });
722
    is($called,0,"We didn't call to link bibs because from linker");
722
    is($called,0,"We didn't call to link bibs because from linker");
723
};
723
};
724
724
(-)a/t/db_dependent/Biblio/MarcMergeRules.t (+779 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
use Try::Tiny;
20
21
use MARC::Record;
22
23
use C4::Context;
24
use C4::Biblio;
25
use Koha::Database; #??
26
27
use Test::More tests => 23;
28
use Test::MockModule;
29
30
use Koha::MarcMergeRules;
31
32
use t::lib::Mocks;
33
34
my $schema = Koha::Database->schema;
35
$schema->storage->txn_begin;
36
37
t::lib::Mocks::mock_preference('MARCMergeRules', '1');
38
39
# Create a record
40
my $orig_record = MARC::Record->new();
41
$orig_record->append_fields (
42
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
43
    MARC::Field->new('250', '','', 'a' => '256 bottles of beer on the wall'),
44
    MARC::Field->new('500', '','', 'a' => 'One bottle of beer in the fridge'),
45
);
46
47
my $incoming_record = MARC::Record->new();
48
$incoming_record->append_fields(
49
    MARC::Field->new('250', '', '', 'a' => '256 bottles of beer on the wall'), # Unchanged
50
    MARC::Field->new('250', '', '', 'a' => '251 bottles of beer on the wall'), # Appended
51
    # MARC::Field->new('250', '', '', 'a' => '250 bottles of beer on the wall'), # Removed
52
    # MARC::Field->new('500', '', '', 'a' => 'One bottle of beer in the fridge'), # Deleted
53
    MARC::Field->new('501', '', '', 'a' => 'One cold bottle of beer in the fridge'), # Added
54
    MARC::Field->new('501', '', '', 'a' => 'Two cold bottles of beer in the fridge'), # Added
55
);
56
57
# Test default behavior when MARCMergeRules is enabled, but no rules defined (overwrite)
58
subtest 'Record fields has been overwritten when no merge rules are defined' => sub {
59
    plan tests => 4;
60
61
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
62
63
    my @all_fields = $merged_record->fields();
64
65
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
66
    is_deeply(
67
        [map { $_->subfield('a') } $merged_record->field('250') ],
68
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
69
        '"250" fields has been appended and removed'
70
    );
71
72
    my @fields = $merged_record->field('500');
73
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
74
75
    is_deeply(
76
        [map { $_->subfield('a') } $merged_record->field('501') ],
77
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
78
        '"501" fields has been added'
79
    );
80
};
81
82
my $rule =  Koha::MarcMergeRules->find_or_create({
83
    tag => '*',
84
    module => 'source',
85
    filter => '*',
86
    add => 0,
87
    append => 0,
88
    remove => 0,
89
    delete => 0
90
});
91
92
subtest 'Record fields has been protected when matched merge all rule operations are set to "0"' => sub {
93
    plan tests => 3;
94
95
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
96
97
    my @all_fields = $merged_record->fields();
98
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
99
100
    is_deeply(
101
        [map { $_->subfield('a') } $merged_record->field('250') ],
102
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
103
        '"250" fields has retained their original value'
104
    );
105
    is_deeply(
106
        [map { $_->subfield('a') } $merged_record->field('500') ],
107
        ['One bottle of beer in the fridge'],
108
        '"500" field has retained it\'s original value'
109
    );
110
};
111
112
subtest 'Only new fields has been added when add = 1, append = 0, remove = 0, delete = 0' => sub {
113
    plan tests => 4;
114
115
    $rule->set(
116
        {
117
            'add' => 1,
118
            'append' => 0,
119
            'remove' => 0,
120
            'delete' => 0,
121
        }
122
    );
123
    $rule->store();
124
125
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
126
127
    my @all_fields = $merged_record->fields();
128
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
129
130
    is_deeply(
131
        [map { $_->subfield('a') } $merged_record->field('250') ],
132
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
133
        '"250" fields retain their original value'
134
    );
135
136
    is_deeply(
137
        [map { $_->subfield('a') } $merged_record->field('500') ],
138
        ['One bottle of beer in the fridge'],
139
        '"500" field retain it\'s original value'
140
    );
141
142
    is_deeply(
143
        [map { $_->subfield('a') } $merged_record->field('501') ],
144
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
145
        '"501" fields has been added'
146
    );
147
};
148
149
subtest 'Only appended fields has been added when add = 0, append = 1, remove = 0, delete = 0' => sub {
150
    plan tests => 3;
151
152
    $rule->set(
153
        {
154
            'add' => 0,
155
            'append' => 1,
156
            'remove' => 0,
157
            'delete' => 0,
158
        }
159
    );
160
    $rule->store();
161
162
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
163
164
    my @all_fields = $merged_record->fields();
165
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
166
167
    is_deeply(
168
        [map { $_->subfield('a') } $merged_record->field('250') ],
169
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
170
        '"251" field has been appended'
171
    );
172
173
    is_deeply(
174
        [map { $_->subfield('a') } $merged_record->field('500') ],
175
        ['One bottle of beer in the fridge'],
176
        '"500" field has retained it\'s original value'
177
    );
178
179
};
180
181
subtest 'Appended and added fields has been added when add = 1, append = 1, remove = 0, delete = 0' => sub {
182
    plan tests => 4;
183
184
    $rule->set(
185
        {
186
            'add' => 1,
187
            'append' => 1,
188
            'remove' => 0,
189
            'delete' => 0,
190
        }
191
    );
192
    $rule->store();
193
194
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
195
196
    my @all_fields = $merged_record->fields();
197
    cmp_ok(scalar @all_fields, '==', 6, "Record has the expected number of fields");
198
199
    is_deeply(
200
        [map { $_->subfield('a') } $merged_record->field('250') ],
201
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
202
        '"251" field has been appended'
203
    );
204
205
    is_deeply(
206
        [map { $_->subfield('a') } $merged_record->field('500') ],
207
        ['One bottle of beer in the fridge'],
208
        '"500" field has retained it\'s original value'
209
    );
210
211
    is_deeply(
212
        [map { $_->subfield('a') } $merged_record->field('501') ],
213
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
214
        '"501" fields has been added'
215
    );
216
};
217
218
subtest 'Record fields has been only removed when add = 0, append = 0, remove = 1, delete = 0' => sub {
219
    plan tests => 3;
220
221
    $rule->set(
222
        {
223
            'add' => 0,
224
            'append' => 0,
225
            'remove' => 1,
226
            'delete' => 0,
227
        }
228
    );
229
    $rule->store();
230
231
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
232
233
    my @all_fields = $merged_record->fields();
234
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
235
236
    is_deeply(
237
        [map { $_->subfield('a') } $merged_record->field('250') ],
238
        ['256 bottles of beer on the wall'],
239
        '"250" field has been removed'
240
    );
241
    is_deeply(
242
        [map { $_->subfield('a') } $merged_record->field('500') ],
243
        ['One bottle of beer in the fridge'],
244
        '"500" field has retained it\'s original value'
245
    );
246
};
247
248
subtest 'Record fields has been added and removed when add = 1, append = 0, remove = 1, delete = 0' => sub {
249
    plan tests => 4;
250
251
    $rule->set(
252
        {
253
            'add' => 1,
254
            'append' => 0,
255
            'remove' => 1,
256
            'delete' => 0,
257
        }
258
    );
259
    $rule->store();
260
261
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
262
263
    my @all_fields = $merged_record->fields();
264
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
265
266
    is_deeply(
267
        [map { $_->subfield('a') } $merged_record->field('250') ],
268
        ['256 bottles of beer on the wall'],
269
        '"250" field has been removed'
270
    );
271
    is_deeply(
272
        [map { $_->subfield('a') } $merged_record->field('500') ],
273
        ['One bottle of beer in the fridge'],
274
        '"500" field has retained it\'s original value'
275
    );
276
277
    is_deeply(
278
        [map { $_->subfield('a') } $merged_record->field('501') ],
279
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
280
        '"501" fields has been added'
281
    );
282
};
283
284
subtest 'Record fields has been appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub {
285
    plan tests => 3;
286
287
    $rule->set(
288
        {
289
            'add' => 0,
290
            'append' => 1,
291
            'remove' => 1,
292
            'delete' => 0,
293
        }
294
    );
295
    $rule->store();
296
297
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
298
299
    my @all_fields = $merged_record->fields();
300
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
301
302
    is_deeply(
303
        [map { $_->subfield('a') } $merged_record->field('250') ],
304
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
305
        '"250" fields has been appended and removed'
306
    );
307
    is_deeply(
308
        [map { $_->subfield('a') } $merged_record->field('500') ],
309
        ['One bottle of beer in the fridge'],
310
        '"500" field has retained it\'s original value'
311
    );
312
};
313
314
subtest 'Record fields has been added, appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub {
315
    plan tests => 4;
316
317
    $rule->set(
318
        {
319
            'add' => 1,
320
            'append' => 1,
321
            'remove' => 1,
322
            'delete' => 0,
323
        }
324
    );
325
    $rule->store();
326
327
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
328
329
    my @all_fields = $merged_record->fields();
330
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
331
332
    is_deeply(
333
        [map { $_->subfield('a') } $merged_record->field('250') ],
334
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
335
        '"250" fields has been appended and removed'
336
    );
337
338
    is_deeply(
339
        [map { $_->subfield('a') } $merged_record->field('500') ],
340
        ['One bottle of beer in the fridge'],
341
        '"500" field has retained it\'s original value'
342
    );
343
344
    is_deeply(
345
        [map { $_->subfield('a') } $merged_record->field('501') ],
346
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
347
        '"501" fields has been added'
348
    );
349
};
350
351
subtest 'Record fields has been deleted when add = 0, append = 0, remove = 0, delete = 1' => sub {
352
    plan tests => 2;
353
354
    $rule->set(
355
        {
356
            'add' => 0,
357
            'append' => 0,
358
            'remove' => 0,
359
            'delete' => 1,
360
        }
361
    );
362
    $rule->store();
363
364
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
365
366
    my @all_fields = $merged_record->fields();
367
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
368
369
    is_deeply(
370
        [map { $_->subfield('a') } $merged_record->field('250') ],
371
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
372
        '"250" fields has retained their original value'
373
    );
374
};
375
376
subtest 'Record fields has been added and deleted when add = 1, append = 0, remove = 0, delete = 1' => sub {
377
    plan tests => 3;
378
379
    $rule->set(
380
        {
381
            'add' => 1,
382
            'append' => 0,
383
            'remove' => 0,
384
            'delete' => 1,
385
        }
386
    );
387
    $rule->store();
388
389
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
390
391
    my @all_fields = $merged_record->fields();
392
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
393
394
    is_deeply(
395
        [map { $_->subfield('a') } $merged_record->field('250') ],
396
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
397
        '"250" fields has retained their original value'
398
    );
399
400
    is_deeply(
401
        [map { $_->subfield('a') } $merged_record->field('501') ],
402
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
403
        '"501" fields has been added'
404
    );
405
};
406
407
subtest 'Record fields has been appended and deleted when add = 0, append = 1, remove = 0, delete = 1' => sub {
408
    plan tests => 2;
409
410
    $rule->set(
411
        {
412
            'add' => 0,
413
            'append' => 1,
414
            'remove' => 0,
415
            'delete' => 1,
416
        }
417
    );
418
    $rule->store();
419
420
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
421
422
    my @all_fields = $merged_record->fields();
423
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
424
425
    is_deeply(
426
        [map { $_->subfield('a') } $merged_record->field('250') ],
427
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
428
        '"250" field has been appended'
429
    );
430
};
431
432
subtest 'Record fields has been added, appended and deleted when add = 1, append = 1, remove = 0, delete = 1' => sub {
433
    plan tests => 3;
434
435
    $rule->set(
436
        {
437
            'add' => 1,
438
            'append' => 1,
439
            'remove' => 0,
440
            'delete' => 1,
441
        }
442
    );
443
    $rule->store();
444
445
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
446
447
    my @all_fields = $merged_record->fields();
448
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
449
450
    is_deeply(
451
        [map { $_->subfield('a') } $merged_record->field('250') ],
452
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
453
        '"250" field has been appended'
454
    );
455
456
    is_deeply(
457
        [map { $_->subfield('a') } $merged_record->field('501') ],
458
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
459
        '"501" fields has been added'
460
    );
461
};
462
463
subtest 'Record fields has been removed and deleted when add = 0, append = 0, remove = 1, delete = 1' => sub {
464
    plan tests => 2;
465
466
    $rule->set(
467
        {
468
            'add' => 0,
469
            'append' => 0,
470
            'remove' => 1,
471
            'delete' => 1,
472
        }
473
    );
474
    $rule->store();
475
476
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
477
478
    my @all_fields = $merged_record->fields();
479
    cmp_ok(scalar @all_fields, '==', 1, "Record has the expected number of fields");
480
481
    is_deeply(
482
        [map { $_->subfield('a') } $merged_record->field('250') ],
483
        ['256 bottles of beer on the wall'],
484
        '"250" field has been removed'
485
    );
486
};
487
488
subtest 'Record fields has been added, removed and deleted when add = 1, append = 0, remove = 1, delete = 1' => sub {
489
    plan tests => 3;
490
491
    $rule->set(
492
        {
493
            'add' => 1,
494
            'append' => 0,
495
            'remove' => 1,
496
            'delete' => 1,
497
        }
498
    );
499
    $rule->store();
500
501
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
502
503
    my @all_fields = $merged_record->fields();
504
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
505
506
    is_deeply(
507
        [map { $_->subfield('a') } $merged_record->field('250') ],
508
        ['256 bottles of beer on the wall'],
509
        '"250" field has been removed'
510
    );
511
512
    is_deeply(
513
        [map { $_->subfield('a') } $merged_record->field('501') ],
514
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
515
        '"501" fields has been added'
516
    );
517
};
518
519
subtest 'Record fields has been appended, removed and deleted when add = 0, append = 1, remove = 1, delete = 1' => sub {
520
    plan tests => 2;
521
522
    $rule->set(
523
        {
524
            'add' => 0,
525
            'append' => 1,
526
            'remove' => 1,
527
            'delete' => 1,
528
        }
529
    );
530
    $rule->store();
531
532
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
533
534
    my @all_fields = $merged_record->fields();
535
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
536
537
    is_deeply(
538
        [map { $_->subfield('a') } $merged_record->field('250') ],
539
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
540
        '"250" fields has been appended and removed'
541
    );
542
};
543
544
subtest 'Record fields has been overwritten when add = 1, append = 1, remove = 1, delete = 1' => sub {
545
    plan tests => 4;
546
547
    $rule->set(
548
        {
549
            'add' => 1,
550
            'append' => 1,
551
            'remove' => 1,
552
            'delete' => 1,
553
        }
554
    );
555
    $rule->store();
556
557
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
558
559
    my @all_fields = $merged_record->fields();
560
561
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
562
    is_deeply(
563
        [map { $_->subfield('a') } $merged_record->field('250') ],
564
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
565
        '"250" fields has been appended and removed'
566
    );
567
568
    my @fields = $merged_record->field('500');
569
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
570
571
    is_deeply(
572
        [map { $_->subfield('a') } $merged_record->field('501') ],
573
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
574
        '"501" fields has been added'
575
    );
576
};
577
578
# Test rule tag specificity
579
580
# Protect field 500 with more specific tag value
581
my $skip_all_rule = Koha::MarcMergeRules->find_or_create({
582
    tag => '500',
583
    module => 'source',
584
    filter => '*',
585
    add => 0,
586
    append => 0,
587
    remove => 0,
588
    delete => 0
589
});
590
591
subtest '"500" field has been protected when rule matching on tag "500" is add = 0, append = 0, remove = 0, delete = 0' => sub {
592
    plan tests => 4;
593
594
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
595
596
    my @all_fields = $merged_record->fields();
597
598
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
599
    is_deeply(
600
        [map { $_->subfield('a') } $merged_record->field('250') ],
601
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
602
        '"250" fields has been appended and removed'
603
    );
604
605
    is_deeply(
606
        [map { $_->subfield('a') } $merged_record->field('500') ],
607
        ['One bottle of beer in the fridge'],
608
        '"500" field has retained it\'s original value'
609
    );
610
611
    is_deeply(
612
        [map { $_->subfield('a') } $merged_record->field('501') ],
613
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
614
        '"501" fields has been added'
615
    );
616
};
617
618
# Test regexp matching
619
subtest '"5XX" fields has been protected when rule matching on regexp "5\d{2}" is add = 0, append = 0, remove = 0, delete = 0' => sub {
620
    plan tests => 3;
621
622
    $skip_all_rule->set(
623
        {
624
            'tag' => '5\d{2}',
625
        }
626
    );
627
    $skip_all_rule->store();
628
629
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
630
631
    my @all_fields = $merged_record->fields();
632
633
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
634
    is_deeply(
635
        [map { $_->subfield('a') } $merged_record->field('250') ],
636
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
637
        '"250" fields has been appended and removed'
638
    );
639
640
    is_deeply(
641
        [map { $_->subfield('a') } $merged_record->field('500') ],
642
        ['One bottle of beer in the fridge'],
643
        '"500" field has retained it\'s original value'
644
    );
645
};
646
647
# Test module specificity, the 0 all rule should no longer be included in set of applied rules
648
subtest 'Record fields has been overwritten when non wild card rule with filter match is add = 1, append = 1, remove = 1, delete = 1' => sub {
649
    plan tests => 4;
650
651
    $rule->set(
652
        {
653
            'filter' => 'test',
654
        }
655
    );
656
    $rule->store();
657
658
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' });
659
660
    my @all_fields = $merged_record->fields();
661
662
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
663
    is_deeply(
664
        [map { $_->subfield('a') } $merged_record->field('250') ],
665
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
666
        '"250" fields has been appended and removed'
667
    );
668
669
    my @fields = $merged_record->field('500');
670
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
671
672
    is_deeply(
673
        [map { $_->subfield('a') } $merged_record->field('501') ],
674
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
675
        '"501" fields has been added'
676
    );
677
};
678
679
subtest 'An exception is thrown when append = 1, remove = 0 is set for control field rule' => sub {
680
    plan tests => 2;
681
    my $exception = try {
682
        Koha::MarcMergeRules->validate({
683
            'tag' => '008',
684
            'append' => 1,
685
            'remove' => 0,
686
        });
687
    }
688
    catch {
689
        return $_;
690
    };
691
    ok(defined $exception, "Exception was caught");
692
    ok($exception->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions'), "Exception is of correct class");
693
};
694
695
subtest 'An exception is thrown when rule tag is set to invalid regexp' => sub {
696
    plan tests => 2;
697
698
    my $exception = try {
699
        Koha::MarcMergeRules->validate({
700
            'tag' => '**'
701
        });
702
    }
703
    catch {
704
        return $_;
705
    };
706
    ok(defined $exception, "Exception was caught");
707
    ok($exception->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp'), "Exception is of correct class");
708
};
709
710
$skip_all_rule->delete();
711
712
subtest 'context option in ModBiblio is handled correctly' => sub {
713
    plan tests => 6;
714
    $rule->set(
715
        {
716
            tag => '250',
717
            module => 'source',
718
            filter => '*',
719
            'add' => 0,
720
            'append' => 0,
721
            'remove' => 0,
722
            'delete' => 0,
723
        }
724
    );
725
    $rule->store();
726
    my ($biblionumber) = AddBiblio($orig_record, '');
727
728
    # Since marc merc rules are not run on save, only update
729
    # saved record should be identical to orig_record
730
    my $saved_record = GetMarcBiblio({ biblionumber => $biblionumber });
731
732
    my @all_fields = $saved_record->fields();
733
    # Koha also adds 999c field, therefore 4 not 3
734
    cmp_ok(scalar @all_fields, '==', 4, 'Saved record has the expected number of fields');
735
    is_deeply(
736
        [map { $_->subfield('a') } $saved_record->field('250') ],
737
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
738
        'All "250" fields of saved record are identical to original record passed to AddBiblio'
739
    );
740
741
    is_deeply(
742
        [map { $_->subfield('a') } $saved_record->field('500') ],
743
        ['One bottle of beer in the fridge'],
744
        'All "500" fields of saved record are identical to original record passed to AddBiblio'
745
    );
746
747
    $saved_record->append_fields(
748
        MARC::Field->new('250', '', '', 'a' => '251 bottles of beer on the wall'), # Appended
749
        MARC::Field->new('500', '', '', 'a' => 'One cold bottle of beer in the fridge'), # Appended
750
    );
751
752
    ModBiblio($saved_record, $biblionumber, '', { context => { 'source' => 'test' } });
753
754
    my $updated_record = GetMarcBiblio({ biblionumber => $biblionumber });
755
756
    @all_fields = $updated_record->fields();
757
    cmp_ok(scalar @all_fields, '==', 5, 'Updated record has the expected number of fields');
758
    is_deeply(
759
        [map { $_->subfield('a') } $updated_record->field('250') ],
760
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
761
        '"250" fields have retained their original values'
762
    );
763
764
    is_deeply(
765
        [map { $_->subfield('a') } $updated_record->field('500') ],
766
        ['One bottle of beer in the fridge', 'One cold bottle of beer in the fridge'],
767
        '"500" field has been appended'
768
    );
769
770
    # To trigger removal from search index etc
771
    DelBiblio($biblionumber);
772
};
773
774
# Explicityly delete rule to trigger clearing of cache
775
$rule->delete();
776
777
$schema->storage->txn_rollback;
778
779
1;
(-)a/tools/batch_record_modification.pl (-2 / +2 lines)
Lines 155-163 if ( $op eq 'form' ) { Link Here
155
            record_ids  => \@record_ids,
155
            record_ids  => \@record_ids,
156
        };
156
        };
157
157
158
        my $patron = Koha::Patrons->find( $loggedinuser );
158
        my $job_id =
159
        my $job_id =
159
          $recordtype eq 'biblio'
160
          $recordtype eq 'biblio'
160
          ? Koha::BackgroundJob::BatchUpdateBiblio->new->enqueue($params)
161
          ? Koha::BackgroundJob::BatchUpdateBiblio->new->enqueue($params, { source => 'batchmod', categorycode => $patron->categorycode, userid => $patron->userid })
161
          : Koha::BackgroundJob::BatchUpdateAuthority->new->enqueue($params);
162
          : Koha::BackgroundJob::BatchUpdateAuthority->new->enqueue($params);
162
163
163
        $template->param(
164
        $template->param(
164
- 

Return to bug 14957