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

(-)a/C4/Biblio.pm (-8 / +94 lines)
Lines 60-65 BEGIN { Link Here
60
        DelBiblio
60
        DelBiblio
61
        BiblioAutoLink
61
        BiblioAutoLink
62
        LinkBibHeadingsToAuthorities
62
        LinkBibHeadingsToAuthorities
63
        ApplyMarcMergeRules
63
        TransformMarcToKoha
64
        TransformMarcToKoha
64
        TransformHtmlToMarc
65
        TransformHtmlToMarc
65
        TransformHtmlToXml
66
        TransformHtmlToXml
Lines 105-114 use Koha::Plugins; Link Here
105
use Koha::SearchEngine;
106
use Koha::SearchEngine;
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 240-246 sub AddBiblio { Link Here
240
241
241
=head2 ModBiblio
242
=head2 ModBiblio
242
243
243
  ModBiblio( $record,$biblionumber,$frameworkcode, $disable_autolink);
244
  ModBiblio($record, $biblionumber, $frameworkcode, $options);
244
245
245
Replace an existing bib record identified by C<$biblionumber>
246
Replace an existing bib record identified by C<$biblionumber>
246
with one supplied by the MARC::Record object C<$record>.  The embedded
247
with one supplied by the MARC::Record object C<$record>.  The embedded
Lines 256-271 in the C<biblio> and C<biblioitems> tables, as well as Link Here
256
which fields are used to store embedded item, biblioitem,
257
which fields are used to store embedded item, biblioitem,
257
and biblionumber data for indexing.
258
and biblionumber data for indexing.
258
259
259
Unless C<$disable_autolink> is passed ModBiblio will relink record headings
260
The C<$options> argument is a hashref with additional parameters:
261
262
=over 4
263
264
=item C<context>
265
266
This parameter is forwared to L</ApplyMarcMergeRules> where it is used for
267
selecting the current rule set if Marc Merge Rules is enabled.
268
See L</ApplyMarcMergeRules> for more details.
269
270
=item C<disable_autolink>
271
272
Unless C<disable_autolink> is passed ModBiblio will relink record headings
260
to authorities based on settings in the system preferences. This flag allows
273
to authorities based on settings in the system preferences. This flag allows
261
us to not relink records when the authority linker is saving modifications.
274
us to not relink records when the authority linker is saving modifications.
262
275
276
=back
277
263
Returns 1 on success 0 on failure
278
Returns 1 on success 0 on failure
264
279
265
=cut
280
=cut
266
281
267
sub ModBiblio {
282
sub ModBiblio {
268
    my ( $record, $biblionumber, $frameworkcode, $disable_autolink ) = @_;
283
    my ( $record, $biblionumber, $frameworkcode, $options ) = @_;
284
    $options //= {};
285
269
    if (!$record) {
286
    if (!$record) {
270
        carp 'No record passed to ModBiblio';
287
        carp 'No record passed to ModBiblio';
271
        return 0;
288
        return 0;
Lines 276-282 sub ModBiblio { Link Here
276
        logaction( "CATALOGUING", "MODIFY", $biblionumber, "biblio BEFORE=>" . $newrecord->as_formatted );
293
        logaction( "CATALOGUING", "MODIFY", $biblionumber, "biblio BEFORE=>" . $newrecord->as_formatted );
277
    }
294
    }
278
295
279
    if ( !$disable_autolink && C4::Context->preference('BiblioAddsAuthorities') ) {
296
    if ( !$options->{disable_autolink} && C4::Context->preference('BiblioAddsAuthorities') ) {
280
        BiblioAutoLink( $record, $frameworkcode );
297
        BiblioAutoLink( $record, $frameworkcode );
281
    }
298
    }
282
299
Lines 297-302 sub ModBiblio { Link Here
297
314
298
    _strip_item_fields($record, $frameworkcode);
315
    _strip_item_fields($record, $frameworkcode);
299
316
317
    # apply merge rules
318
    if (C4::Context->preference('MARCMergeRules') && $biblionumber && defined $options && exists $options->{'context'}) {
319
        $record = ApplyMarcMergeRules({
320
                biblionumber => $biblionumber,
321
                record => $record,
322
                context => $options->{'context'},
323
            }
324
        );
325
    }
326
300
    # update biblionumber and biblioitemnumber in MARC
327
    # update biblionumber and biblioitemnumber in MARC
301
    # FIXME - this is assuming a 1 to 1 relationship between
328
    # FIXME - this is assuming a 1 to 1 relationship between
302
    # biblios and biblioitems
329
    # biblios and biblioitems
Lines 313-319 sub ModBiblio { Link Here
313
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
340
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
314
341
315
    # update the MARC record (that now contains biblio and items) with the new record data
342
    # update the MARC record (that now contains biblio and items) with the new record data
316
    &ModBiblioMarc( $record, $biblionumber, $frameworkcode );
343
    ModBiblioMarc( $record, $biblionumber, $frameworkcode );
317
344
318
    # modify the other koha tables
345
    # modify the other koha tables
319
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
346
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
Lines 3065-3071 sub _koha_delete_biblio_metadata { Link Here
3065
3092
3066
=head2 ModBiblioMarc
3093
=head2 ModBiblioMarc
3067
3094
3068
  &ModBiblioMarc($newrec,$biblionumber,$frameworkcode);
3095
  ModBiblioMarc($newrec, $biblionumber, $frameworkcode);
3069
3096
3070
Add MARC XML data for a biblio to koha
3097
Add MARC XML data for a biblio to koha
3071
3098
Lines 3382-3389 sub RemoveAllNsb { Link Here
3382
    return $record;
3409
    return $record;
3383
}
3410
}
3384
3411
3385
1;
3412
=head2 ApplyMarcMergeRules
3413
3414
    my $record = ApplyMarcMergeRules($params)
3415
3416
Applies marc merge rules to a record.
3417
3418
C<$params> is expected to be a hashref with below keys defined.
3419
3420
=over 4
3421
3422
=item C<biblionumber>
3423
biblionumber of old record
3424
3425
=item C<record>
3426
Incoming record that will be merged with old record
3427
3428
=item C<context>
3429
hashref containing at least one context module and filter value on
3430
the form {module => filter, ...}.
3386
3431
3432
=back
3433
3434
Returns:
3435
3436
=over 4
3437
3438
=item C<$record>
3439
3440
Merged MARC record based with merge rules for C<context> applied. If no old
3441
record for C<biblionumber> can be found, C<record> is returned unchanged.
3442
Default action when no matching context is found to return C<record> unchanged.
3443
If no rules are found for a certain field tag the default is to overwrite with
3444
fields with this field tag from C<record>.
3445
3446
=back
3447
3448
=cut
3449
3450
sub ApplyMarcMergeRules {
3451
    my ($params) = @_;
3452
    my $biblionumber = $params->{biblionumber};
3453
    my $incoming_record = $params->{record};
3454
3455
    if (!$biblionumber) {
3456
        carp 'ApplyMarcMergeRules called on undefined biblionumber';
3457
        return;
3458
    }
3459
    if (!$incoming_record) {
3460
        carp 'ApplyMarcMergeRules called on undefined record';
3461
        return;
3462
    }
3463
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3464
3465
    # Skip merge rules if called with no context
3466
    if ($old_record && defined $params->{context}) {
3467
        return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
3468
    }
3469
    return $incoming_record;
3470
}
3471
3472
1;
3387
3473
3388
=head2 _after_biblio_action_hooks
3474
=head2 _after_biblio_action_hooks
3389
3475
(-)a/C4/ImportBatch.pm (-1 / +1 lines)
Lines 663-669 sub BatchCommitRecords { Link Here
663
                }
663
                }
664
                $oldxml = $old_marc->as_xml($marc_type);
664
                $oldxml = $old_marc->as_xml($marc_type);
665
665
666
                ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode);
666
                ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode, {context => {source => 'batchimport'}});
667
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?";
667
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?";
668
668
669
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
669
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
(-)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 (+382 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
                prefetch => 'module',
70
                order_by => { -desc => [qw/module.specificity me.id/] }
71
            }
72
        );
73
        foreach my $rule_row (@rules_rows) {
74
            my %rule = $rule_row->get_columns();
75
            my $operations = {};
76
77
            foreach my $operation ($self->operations) {
78
                $operations->{$operation} = { allow => $rule{$operation}, rule => $rule{id} };
79
            }
80
81
            # TODO: Remove unless check and validate on saving rules?
82
            if ($rule{tag} eq '*') {
83
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{'*'}) {
84
                    $rules->{$rule{module}}->{$rule{filter}}->{'*'} = $operations;
85
                }
86
            }
87
            elsif ($rule{tag} =~ /^(\d{3})$/) {
88
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}}) {
89
                    $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}} = $operations;
90
                }
91
            }
92
            else {
93
                my $regexps = ($rules->{$rule{module}}->{$rule{filter}}->{regexps} //= []);
94
                push @{$regexps}, [$rule{tag}, $operations];
95
            }
96
        }
97
        $cache->set_in_cache('marc_merge_rules', $rules);
98
    }
99
100
    my $context_rules = undef;
101
    foreach my $module_name (keys %{$context}) {
102
        if (
103
            exists $rules->{$module_name} &&
104
            exists $rules->{$module_name}->{$context->{$module_name}}
105
        ) {
106
            $context_rules = $rules->{$module_name}->{$context->{$module_name}};
107
            last;
108
        }
109
    }
110
    if (!$context_rules) {
111
        # No perms matching specific context conditions found, try wildcard value for each active context
112
        foreach my $module_name (keys %{$context}) {
113
            if (exists $rules->{$module_name}->{'*'}) {
114
                $context_rules = $rules->{$module_name}->{'*'};
115
                last;
116
            }
117
        }
118
    }
119
    return $context_rules;
120
}
121
122
=head3 merge_records
123
124
    my $merged_record = Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $context);
125
126
Merge C<$old_record> with C<$incoming_record> applying merge rules for C<$context>.
127
Returns merged record C<$merged_record>. C<$old_record>, C<$incoming_record> and
128
C<$merged_record> are all MARC::Record objects.
129
130
=cut
131
132
sub merge_records {
133
    my ($self, $old_record, $incoming_record, $context) = @_;
134
135
    my $rules = $self->context_rules($context);
136
137
    # Default when no rules found is to overwrite with incoming record
138
    return $incoming_record unless $rules;
139
140
    my $fields_by_tag = sub {
141
        my ($record) = @_;
142
        my $fields = {};
143
        foreach my $field ($record->fields()) {
144
            $fields->{$field->tag()} //= [];
145
            push @{$fields->{$field->tag()}}, $field;
146
        }
147
        return $fields;
148
    };
149
150
    my $hash_field_data = sub {
151
        my ($field) = @_;
152
        my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
153
        return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields());
154
    };
155
156
    my $diff_by_key = sub {
157
        my ($a, $b) = @_;
158
        my @removed;
159
        my @intersecting;
160
        my @added;
161
        my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
162
        foreach my $key (keys %keys_index) {
163
            if ($a->{$key} && $b->{$key}) {
164
                push @intersecting, $a->{$key};
165
            }
166
            elsif ($a->{$key}) {
167
                push @removed, $a->{$key};
168
            }
169
            else {
170
                push @added, $b->{$key};
171
            }
172
        }
173
        return (\@removed, \@intersecting, \@added);
174
    };
175
176
    my $tag_rules = $rules->{tags} // {};
177
    my $default_rule = $rules->{'*'} // {
178
        add => { allow => 1, 'rule' => 0},
179
        append => { allow => 1, 'rule' => 0},
180
        delete => { allow => 1, 'rule' => 0},
181
        remove => { allow => 1, 'rule' => 0},
182
    };
183
184
    # Precompile regexps
185
    my @regexp_rules = map { { regexp => qr/^$_->[0]$/, actions => $_->[1] } } @{$rules->{regexps} // []};
186
187
    my $get_matching_field_rule = sub {
188
        my ($tag) = @_;
189
        # Exact match takes precedence, then regexp, then wildcard/defaults
190
        return $tag_rules->{$tag} //
191
            %{(first { $tag =~ $_->{regexp} } @regexp_rules) // {}}{actions} //
192
            $default_rule;
193
    };
194
195
    my %merged_record_fields;
196
197
    my $current_fields = $fields_by_tag->($old_record);
198
    my $incoming_fields = $fields_by_tag->($incoming_record);
199
200
    # First we get all new incoming fields
201
    my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
202
    foreach my $tag (@new_field_tags) {
203
        my $rule = $get_matching_field_rule->($tag);
204
        if ($rule->{add}->{allow}) {
205
            $merged_record_fields{$tag} //= [];
206
            push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
207
        }
208
    }
209
210
    # Then we get all fields no longer present in incoming fields
211
    my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
212
    foreach my $tag (@deleted_field_tags) {
213
        my $rule = $get_matching_field_rule->($tag);
214
        if (!$rule->{delete}->{allow}) {
215
            $merged_record_fields{$tag} //= [];
216
            push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
217
        }
218
    }
219
220
    # Then we get the intersection of fields, present both in
221
    # current and incoming record (possibly to be overwritten)
222
    my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
223
    foreach my $tag (@common_field_tags) {
224
        my $rule = $get_matching_field_rule->($tag);
225
226
        # Special handling for control fields
227
        if ($tag < 10) {
228
            if (
229
                $rule->{append}->{allow} &&
230
                !$rule->{remove}->{allow}
231
            ) {
232
                # This should be highly unlikely since we have input validation to protect against this case
233
                carp "Allowing \"append\" and skipping \"remove\" is not permitted for control fields, falling back to skipping both \"append\" and \"remove\"";
234
                push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
235
            }
236
            elsif ($rule->{append}->{allow}) {
237
                push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
238
            }
239
            else {
240
                push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
241
            }
242
        }
243
        else {
244
            # Compute intersection and diff using field data
245
            my $sort_weight = 0;
246
            my %current_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$current_fields->{$tag}};
247
248
            # Always put incoming fields after current fields
249
            my %incoming_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$incoming_fields->{$tag}};
250
251
            my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
252
253
            my @merged_fields;
254
255
            # First add common fields (intersection)
256
            # Unchanged
257
            if (@{$common_fields}) {
258
                push @merged_fields, @{$common_fields};
259
            }
260
            # Removed
261
            if (@{$current_fields_only}) {
262
                if (!$rule->{remove}->{allow}) {
263
                    push @merged_fields, @{$current_fields_only};
264
                }
265
            }
266
            # Appended
267
            if (@{$incoming_fields_only}) {
268
                if ($rule->{append}->{allow}) {
269
                    push @merged_fields, @{$incoming_fields_only};
270
                }
271
            }
272
            $merged_record_fields{$tag} //= [];
273
274
            # Sort ascending according to weight (original order)
275
            push @{$merged_record_fields{$tag}}, map { $_->[1] } sort { $a->[0] <=> $b->[0] } @merged_fields;
276
        }
277
    }
278
279
    my $merged_record = MARC::Record->new();
280
281
    # Leader is always overwritten, or kept???
282
    $merged_record->leader($incoming_record->leader());
283
284
    if (%merged_record_fields) {
285
        foreach my $tag (sort keys %merged_record_fields) {
286
            $merged_record->append_fields(@{$merged_record_fields{$tag}});
287
        }
288
    }
289
    return $merged_record;
290
}
291
292
sub _clear_caches {
293
    $cache->clear_from_cache('marc_merge_rules');
294
}
295
296
=head2 find_or_create
297
298
Override C<find_or_create> to clear marc merge rules cache.
299
300
=cut
301
302
sub find_or_create {
303
    my $self = shift @_;
304
    $self->_clear_caches();
305
    $self->SUPER::find_or_create(@_);
306
}
307
308
=head2 update
309
310
Override C<update> to clear marc merge rules cache.
311
312
=cut
313
314
sub update {
315
    my $self = shift @_;
316
    $self->_clear_caches();
317
    return $self->SUPER::update(@_);
318
}
319
320
=head2 delete
321
322
Override C<delete> to clear marc merge rules cache.
323
324
=cut
325
326
sub delete {
327
    my $self = shift @_;
328
    $self->_clear_caches();
329
    return $self->SUPER::delete(@_);
330
}
331
332
=head2 validate
333
334
    Koha::MarcMergeRules->validate($rule_data);
335
336
Validates C<$rule_data>. Throws C<Koha::Exceptions::MarcMergeRule::InvalidTagRegExp>
337
if C<$rule_data->{tag}> contains an invalid regular expression. Throws
338
C<Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions> if contains invalid
339
combination of actions for control fields. Otherwise returns true.
340
341
=cut
342
343
sub validate {
344
    my ($self, $rule_data) = @_;
345
346
    if(exists $rule_data->{tag}) {
347
        if ($rule_data->{tag} ne '*') {
348
            eval { qr/$rule_data->{tag}/ };
349
            if ($@) {
350
                Koha::Exceptions::MarcMergeRule::InvalidTagRegExp->throw(
351
                    "Invalid tag regular expression"
352
                );
353
            }
354
        }
355
        # TODO: Regexp or '*' that match controlfield not currently detected
356
        if (
357
            looks_like_number($rule_data->{tag}) &&
358
            $rule_data->{tag} < 10 &&
359
            $rule_data->{append} &&
360
            !$rule_data->{remove}
361
        ) {
362
            Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions->throw(
363
                "Combination of allow append and skip remove not permitted for control fields"
364
            );
365
        }
366
    }
367
    return 1;
368
}
369
370
sub _type {
371
    return 'MarcMergeRule';
372
}
373
374
=head3 object_class
375
376
=cut
377
378
sub object_class {
379
    return 'Koha::MarcMergeRule';
380
}
381
382
1;
(-)a/admin/marc-merge-rules.pl (+160 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
my %cookies = parse CGI::Cookie($cookie);
80
our $sessionID = $cookies{'CGISESSID'}->value;
81
82
my $get_rules = sub {
83
    # TODO: order?
84
    return [map { { $_->get_columns() } } Koha::MarcMergeRules->_resultset->all];
85
};
86
my $rules;
87
88
if ($op eq 'remove' || $op eq 'doremove') {
89
    my @remove_ids = $input->multi_param('batchremove');
90
    push @remove_ids, scalar $input->param('id') if $input->param('id');
91
    if ($op eq 'remove') {
92
        $template->{VARS}->{removeConfirm} = 1;
93
        my %remove_ids = map { $_ => undef } @remove_ids;
94
        $rules = $get_rules->();
95
        for my $rule (@{$rules}) {
96
            $rule->{'removemarked'} = 1 if exists $remove_ids{$rule->{id}};
97
        }
98
    }
99
    elsif ($op eq 'doremove') {
100
        my @remove_ids = $input->multi_param('batchremove');
101
        push @remove_ids, scalar $input->param('id') if $input->param('id');
102
        Koha::MarcMergeRules->search({ id => { in => \@remove_ids } })->delete();
103
        $rules = $get_rules->();
104
    }
105
}
106
elsif ($op eq 'edit') {
107
    $template->{VARS}->{edit} = 1;
108
    my $id = $input->param('id');
109
    $rules = $get_rules->();
110
    for my $rule(@{$rules}) {
111
        if ($rule->{id} == $id) {
112
            $rule->{'edit'} = 1;
113
            last;
114
        }
115
    }
116
}
117
elsif ($op eq 'doedit' || $op eq 'add') {
118
    my $rule_data = $rule_from_cgi->($input);
119
    if (!@{$errors}) {
120
        try {
121
            Koha::MarcMergeRules->validate($rule_data);
122
        }
123
        catch {
124
            die $_ unless blessed $_ && $_->can('rethrow');
125
126
            if ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp')) {
127
                push @{$errors}, {
128
                    type => 'error',
129
                    code => 'invalid_tag_regexp',
130
                    tag => $rule_data->{tag},
131
                };
132
            }
133
            elsif ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions')) {
134
                push @{$errors}, {
135
                    type => 'error',
136
                    code => 'invalid_control_field_actions',
137
                    tag => $rule_data->{tag},
138
                };
139
            }
140
            else {
141
                $_->rethrow;
142
            }
143
        };
144
        if (!@{$errors}) {
145
            my $rule = Koha::MarcMergeRules->find_or_create($rule_data);
146
            # Need to call set and store here in case we have an update
147
            $rule->set($rule_data);
148
            $rule->store();
149
        }
150
        $rules = $get_rules->();
151
    }
152
}
153
else {
154
    $rules = $get_rules->();
155
}
156
157
my $categorycodes = Koha::Patron::Categories->search_limited({}, {order_by => ['description']});
158
$template->param( rules => $rules, categorycodes => $categorycodes, messages => $errors );
159
160
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 847-853 if ( $op eq "addbiblio" ) { Link Here
847
    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
848
    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
848
        my $oldbibitemnum;
849
        my $oldbibitemnum;
849
        if ( $is_a_modif ) {
850
        if ( $is_a_modif ) {
850
            ModBiblio( $record, $biblionumber, $frameworkcode );
851
            my $member = Koha::Patrons->find($loggedinuser);
852
            ModBiblio( $record, $biblionumber, $frameworkcode, {
853
                    context => {
854
                        source => $z3950 ? 'z39.50' : 'intranet',
855
                        categorycode => $member->categorycode,
856
                        userid => $member->userid
857
                    }
858
                }
859
            );
851
        }
860
        }
852
        else {
861
        else {
853
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
862
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
Lines 940-945 elsif ( $op eq "delete" ) { Link Here
940
    $template->param(
949
    $template->param(
941
        biblionumberdata => $biblionumber,
950
        biblionumberdata => $biblionumber,
942
        op               => $op,
951
        op               => $op,
952
        z3950            => $z3950
943
    );
953
    );
944
    if ( $op eq "duplicate" ) {
954
    if ( $op eq "duplicate" ) {
945
        $biblionumber = "";
955
        $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 1084-1089 CREATE TABLE `marc_matchers` ( Link Here
1084
  KEY `record_type` (`record_type`)
1084
  KEY `record_type` (`record_type`)
1085
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
1085
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
1086
1086
1087
--
1088
-- Table structure for table `marc_merge_rules_modules`
1089
--
1090
1091
DROP TABLE IF EXISTS `marc_merge_rules_modules`;
1092
CREATE TABLE `marc_merge_rules_modules` (
1093
  `name` varchar(127) NOT NULL,
1094
  `description` varchar(255),
1095
  `specificity` int(11) NOT NULL UNIQUE,
1096
  PRIMARY KEY(`name`)
1097
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
1098
1099
--
1100
-- Table structure for table `marc_merge_rules`
1101
--
1102
1103
DROP TABLE IF EXISTS `marc_merge_rules`;
1104
CREATE TABLE IF NOT EXISTS `marc_merge_rules` (
1105
  `id` int(11) NOT NULL auto_increment,
1106
  `tag` varchar(255) NOT NULL, -- can be regexp, so need > 3 chars
1107
  `module` varchar(127) NOT NULL,
1108
  `filter` varchar(255) NOT NULL,
1109
  `add` tinyint NOT NULL,
1110
  `append` tinyint NOT NULL,
1111
  `remove` tinyint NOT NULL,
1112
  `delete` tinyint NOT NULL,
1113
  PRIMARY KEY(`id`),
1114
  CONSTRAINT `marc_merge_rules_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_merge_rules_modules` (`name`) ON DELETE CASCADE ON UPDATE CASCADE
1115
);
1116
1087
--
1117
--
1088
-- Table structure for table `matchpoints`
1118
-- Table structure for table `matchpoints`
1089
--
1119
--
(-)a/installer/data/mysql/sysprefs.sql (+1 lines)
Lines 313-318 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
313
('MarcFieldForModifierName','',NULL,'Where to store the name of the record''s last modifier','Free'),
313
('MarcFieldForModifierName','',NULL,'Where to store the name of the record''s last modifier','Free'),
314
('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'),
314
('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'),
315
('MarcItemFieldsToOrder','',NULL,'Set the mapping values for new item records created from a MARC record in a staged file. In a YAML format.','textarea'),
315
('MarcItemFieldsToOrder','',NULL,'Set the mapping values for new item records created from a MARC record in a staged file. In a YAML format.','textarea'),
316
('MARCMergeRules','0',NULL,'Use the MARC merge rules system to decide what actions to take for each field when modifying records.','YesNo'),
316
('MarkLostItemsAsReturned','batchmod,moredetail,cronjob,additem,pendingreserves,onpayment','batchmod|moredetail|cronjob|additem|pendingreserves|onpayment','Mark items as returned when flagged as lost','multiple'),
317
('MarkLostItemsAsReturned','batchmod,moredetail,cronjob,additem,pendingreserves,onpayment','batchmod|moredetail|cronjob|additem|pendingreserves|onpayment','Mark items as returned when flagged as lost','multiple'),
317
('MARCOrgCode','OSt','','Define MARC Organization Code for MARC21 records - http://www.loc.gov/marc/organizations/orgshome.html','free'),
318
('MARCOrgCode','OSt','','Define MARC Organization Code for MARC21 records - http://www.loc.gov/marc/organizations/orgshome.html','free'),
318
('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'),
319
('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/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 (+481 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].val().length == 0) {
36
                validate[i].addClass('required');
37
                valid = false;
38
            } else {
39
                validate[i].removeClass('required');
40
            }
41
        }
42
    }
43
44
    if(valid ) {
45
        $('#marc-merge-rules-form').submit();
46
    }
47
48
    return valid;
49
}
50
51
$(document).ready(function(){
52
    $('#doremove').on("click",function(){
53
        doSubmit('doremove');
54
    });
55
    $('#doedit').on("click",function(){
56
        doSubmit('doedit', $("#doedit").attr('value'));
57
    });
58
    $('#add').on("click", function(){
59
        doSubmit('add');
60
        return false;
61
    });
62
    $('#btn_batchremove').on("click", function(){
63
        doSubmit('remove');
64
    });
65
66
    /* Disable batch remove unless one or more checkboxes are checked */
67
    $('input[name="batchremove"]').change(function() {
68
        if($('input[name="batchremove"]:checked').length > 0) {
69
            $('#btn_batchremove').removeAttr('disabled');
70
        } else {
71
            $('#btn_batchremove').attr('disabled', 'disabled');
72
        }
73
    });
74
75
    $.fn.dataTable.ext.order['dom-input'] = function (settings, col) {
76
        return this.api().column(col, { order: 'index' }).nodes()
77
            .map(function (td, i) {
78
                if($('input', td).val() != undefined) {
79
                    return $('input', td).val();
80
                } else if($('select', td).val() != undefined) {
81
                    return $('option[selected="selected"]', td).val();
82
                } else {
83
                    return $(td).html();
84
                }
85
            });
86
    }
87
88
    $('#marc-merge-rules').dataTable($.extend(true, {}, dataTablesDefaults, {
89
        "aoColumns": [
90
            {"bSearchable": false, "bSortable": false},
91
            {"sSortDataType": "dom-input"},
92
            {"sSortDataType": "dom-input"},
93
            {"bSearchable": false, "sSortDataType": "dom-input"},
94
            {"bSearchable": false, "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, "bSortable": false},
100
            {"bSearchable": false, "bSortable": false}
101
        ],
102
        "sPaginationType": "four_button"
103
    }));
104
105
    var merge_rules_presets = {
106
      'Protect': {
107
        'add': 0,
108
        'append': 0,
109
        'remove': 0,
110
        'delete': 0
111
      },
112
      'Overwrite': {
113
        'add': 1,
114
        'append': 1,
115
        'remove': 1,
116
        'delete': 1
117
      },
118
      'Add new': {
119
        'add': 1,
120
        'append': 0,
121
        'remove': 0,
122
        'delete': 0
123
      },
124
      'Add and append': {
125
        'add': 1,
126
        'append': 1,
127
        'remove': 0,
128
        'delete': 0
129
      },
130
      'Protect from deletion': {
131
        'add': 1,
132
        'append': 1,
133
        'remove': 1,
134
        'delete': 0
135
      },
136
    };
137
138
    var merge_rules_label_to_value = {
139
      'Add': 1,
140
      'Append': 1,
141
      'Remove': 1,
142
      'Delete': 1,
143
      'Skip': 0
144
    };
145
146
    var merge_rules_preset_map = {};
147
    $.each(merge_rules_presets, function(preset, config) {
148
      merge_rules_preset_map[JSON.stringify(config)] = preset;
149
    });
150
151
    function operations_config_merge_rule_preset(config) {
152
      return merge_rules_preset_map[JSON.stringify(config)] || '';
153
    }
154
155
    /* Set preset values according to operation config */
156
    $('.rule').each(function() {
157
      var $this = $(this);
158
      var operations_config = {};
159
      $('.rule-operation-action', $this).each(function() {
160
        var $operation = $(this);
161
        operations_config[$operation.data('operation')] = merge_rules_label_to_value[$operation.text()];
162
      });
163
      $('.rule-preset', $this).text(
164
        operations_config_merge_rule_preset(operations_config)
165
      );
166
    });
167
168
    /* Listen to operations config changes and set presets accordingly */
169
    $('.rule-operation-action-edit select').change(function() {
170
      var operations_config = {};
171
      var $parent_row = $(this).closest('tr');
172
      $('.rule-operation-action-edit select', $parent_row).each(function() {
173
        var $this = $(this);
174
        operations_config[$this.attr('name')] = $this.val();
175
      });
176
      $('select[name="preset"]', $parent_row).val(
177
          operations_config_merge_rule_preset(operations_config)
178
      );
179
    });
180
181
    /* Listen to preset changes and set operations config accordingly */
182
    $('select[name="preset"]').change(function() {
183
      var $this = $(this);
184
      var $parent_row = $this.closest('tr');
185
      var preset = $this.val();
186
      if (preset) {
187
        $.each(merge_rules_presets[preset], function(operation, action) {
188
          $('select[name="' + operation + '"]', $parent_row).val(action);
189
        });
190
      }
191
    });
192
193
    var module_filter_options = {
194
      source: {
195
        '*': '*',
196
        batchmod: "Batch record modification",
197
        intranet: "Staff client MARC editor",
198
        batchimport: "Staged MARC import",
199
        z3950: "Z39.50",
200
        bulkmarcimport: "bulkmarcimport.pl",
201
        import_lexile: "import_lexile.pl"
202
      },
203
      categorycode: {
204
        '*': '*',
205
        [% FOREACH categorycode IN categorycodes %]
206
          [% categorycode.categorycode | html %]: "[% categorycode.description | html %]",
207
        [% END %]
208
      }
209
    };
210
211
    var $filter_container = $('#filter-container');
212
213
    /* Listen to module changes and set filter input accordingly */
214
    $('select[name="module"]').change(function() {
215
      var $this = $(this);
216
      var module_name = $this.val();
217
218
      /* Remove current element if any */
219
      $filter_container.empty();
220
221
      var filter_elem = null;
222
      if (module_name in module_filter_options) {
223
        // Create select element
224
        filter_elem = document.createElement('select');
225
        for (var filter_value in module_filter_options[module_name]) {
226
          var option = document.createElement('option');
227
          option.value = filter_value;
228
          option.text = module_filter_options[module_name][filter_value];
229
          filter_elem.appendChild(option);
230
        }
231
      }
232
      else {
233
        // Create text input element
234
        filter_elem = document.createElement('input');
235
        filter_elem.type = 'text';
236
        filter_elem.setAttribute('size', 5);
237
      }
238
      filter_elem.name = 'filter';
239
      filter_elem.id = 'filter';
240
      $filter_container.append(filter_elem);
241
    }).change(); // Trigger change
242
243
    // Hack: set value if editing rule
244
    if ($filter_container.data('filter')) {
245
      $('#filter').val($filter_container.data('filter'));
246
    }
247
});
248
</script>
249
</head>
250
<body id="admin_marc-merge-rules" class="admin">
251
[% INCLUDE 'header.inc' %]
252
[% INCLUDE 'cat-search.inc' %]
253
254
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
255
 &rsaquo; MARC merge rules
256
</div>
257
258
<div id="doc3" class="yui-t2">
259
   <div id="bd">
260
    <div id="yui-main">
261
    <div class="yui-b">
262
263
<h1>Manage MARC merge rules</h1>
264
265
[% FOR m IN messages %]
266
  <div class="dialog [% m.type | html %]">
267
    [% SWITCH m.code %]
268
    [% CASE 'invalid_tag_regexp' %]
269
      Invalid regular expression "[% m.tag | html %]".
270
    [% CASE 'invalid_control_field_actions' %]
271
      Invalid combination of actions for tag [% m.tag | html %]. Control field rules do not allow "Appended: Append" and "Removed: Skip".
272
    [% CASE %]
273
      [% m.code | html %]
274
    [% END %]
275
  </div>
276
[% END %]
277
278
[% UNLESS Koha.Preference( 'MARCMergeRules' ) %]
279
    <div class="dialog message">
280
        The <b>MARCMergeRules</b> preference is not set, don't forget to enable it for rules to take effect.
281
    </div>
282
[% END %]
283
[% IF removeConfirm %]
284
<div class="dialog alert">
285
<h3>Remove rule?</h3>
286
<p>Are you sure you want to remove the selected rule(s)?</p>
287
288
<form action="[% script_name %]" method="GET">
289
    <input type="submit" value="No, do not remove" class="deny"/>
290
</form>
291
<input type="button" value="Yes, remove" class="approve" id="doremove" />
292
</div>
293
[% END %]
294
295
<form action="[% script_name %]" method="POST" id="marc-merge-rules-form">
296
<table id="marc-merge-rules">
297
    <thead><tr>
298
        <th>Rule</th>
299
        <th>Module</th>
300
        <th>Filter</th>
301
        <th>Tag</th>
302
        <th>Preset</th>
303
        <th>Added</th>
304
        <th>Appended</th>
305
        <th>Removed</th>
306
        <th>Deleted</th>
307
        <th>Actions</th>
308
        <th>&nbsp;</th>
309
    </tr></thead>
310
    [% UNLESS edit %]
311
    <tfoot>
312
        <tr class="rule-new">
313
            <th>&nbsp;</th>
314
            <th>
315
                <select name="module">
316
                    <option value="source">Source</option>
317
                    <option value="categorycode">User category</option>
318
                    <option value="userid">Username</option>
319
                </select>
320
            </th>
321
            <th id="filter-container"></th>
322
            <th><input type="text" size="5" name="tag"/></th>
323
            <th>
324
                <select name="preset">
325
                    <option value="" selected>Custom</option>
326
                    [% FOR preset IN ['Protect', 'Overwrite', 'Add new', 'Add and append', 'Protect from deletion'] %]
327
                        <option value="[% preset %]">[% preset %]</option>
328
                    [% END %]
329
                </select>
330
            </th>
331
            <th class="rule-operation-action-edit">
332
                <select name="add">
333
                    <option value="0">Skip</option>
334
                    <option value="1">Add</option>
335
                </select>
336
            </th>
337
            <th class="rule-operation-action-edit">
338
                <select name="append">
339
                    <option value="0">Skip</option>
340
                    <option value="1">Append</option>
341
                </select>
342
            </th>
343
            <th class="rule-operation-action-edit">
344
                <select name="remove">
345
                    <option value="0">Skip</option>
346
                    <option value="1">Remove</option>
347
                </select>
348
            </th>
349
            <th class="rule-operation-action-edit">
350
                <select name="delete">
351
                    <option value="0">Skip</option>
352
                    <option value="1">Delete</option>
353
                </select>
354
            </th>
355
            <th><button class="btn btn-sm" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
356
            <th><button id="btn_batchremove" disabled="disabled" class="btn btn-sm" title="Batch remove"><i class="fa fa-trash"></i> Delete selected</button></th>
357
        </tr>
358
    </tfoot>
359
    [% END %]
360
    <tbody>
361
        [% FOREACH rule IN rules %]
362
            <tr id="[% rule.id %]" class="rule[% IF rule.edit %]-edit[% END %]">
363
            [% IF rule.edit %]
364
                <td>[% rule.id %]</td>
365
                <td>
366
                    <select name="module">
367
                        [% IF rule.module == "source" %]
368
                            <option value="source" selected="selected">Source</option>
369
                        [% ELSE %]
370
                            <option value="source">Source</option>
371
                        [% END %]
372
                        [% IF rule.module == "categorycode" %]
373
                            <option value="categorycode" selected="selected">User category</option>
374
                        [% ELSE %]
375
                            <option value="categorycode">User category</option>
376
                        [% END %]
377
                        [% IF rule.module == "userid" %]
378
                            <option value="userid" selected="selected">Username</option>
379
                        [% ELSE %]
380
                            <option value="userid">Username</option>
381
                        [% END %]
382
                    </select>
383
                </td>
384
                <td id="filter-container" data-filter="[% rule.filter | html %]"></td>
385
                <td><input type="text" size="3" name="tag" value="[% rule.tag | html %]"/></td>
386
                <th>
387
                    <select name="preset">
388
                        <option value="" selected>Custom</option>
389
                        [% FOR preset IN ['Protect', 'Overwrite', 'Add new', 'Add and append', 'Protect from deletion'] %]
390
                            <option value="[% preset %]">[% preset %]</option>
391
                        [% END %]
392
                    </select>
393
                </th>
394
                <td class="rule-operation-action-edit">
395
                    <select name="add">
396
                        [% IF rule.add %]
397
                            <option value="0">Skip</option>
398
                            <option value="1" selected="selected">Add</option>
399
                        [% ELSE %]
400
                            <option value="0" selected="selected">Skip</option>
401
                            <option value="1">Add</option>
402
                        [% END %]
403
                    </select>
404
                </td>
405
                <td class="rule-operation-action-edit">
406
                    <select name="append">
407
                        [% IF rule.append %]
408
                            <option value="0">Skip</option>
409
                            <option value="1" selected="selected">Append</option>
410
                        [% ELSE %]
411
                            <option value="0" selected="selected">Skip</option>
412
                            <option value="1">Append</option>
413
                        [% END %]
414
                    </select>
415
                </td>
416
                <td class="rule-operation-action-edit">
417
                    <select name="remove">
418
                        [% IF rule.remove %]
419
                            <option value="0">Skip</option>
420
                            <option value="1" selected="selected">Remove</option>
421
                        [% ELSE %]
422
                            <option value="0" selected="selected">Skip</option>
423
                            <option value="1">Remove</option>
424
                        [% END %]
425
                    </select>
426
                </td>
427
                <td class="rule-operation-action-edit">
428
                    <select name="delete">
429
                        [% IF rule.delete %]
430
                            <option value="0">Skip</option>
431
                            <option value="1" selected="selected">Delete</option>
432
                        [% ELSE %]
433
                            <option value="0" selected="selected">Skip</option>
434
                            <option value="1">Delete</option>
435
                        [% END %]
436
                    </select>
437
                </td>
438
                <td class="actions">
439
                    <button class="btn btn-sm" title="Save" id="doedit" value="[% rule.id | html %]"><i class="fa fa-check"></i> Save</button>
440
                    <a href="?"><button class="btn btn-sm" title="Cancel" ><i class="fa fa-times"></i> Cancel</button></a>
441
                </td>
442
                <td></td>
443
            [% ELSE %]
444
                <td>[% rule.id | html %]</td>
445
                <td>[% rule.module | html %]</td>
446
                <td>[% rule.filter | html %]</td>
447
                <td>[% rule.tag | html %]</td>
448
                <td class="rule-preset"></td>
449
                <td class="rule-operation-action" data-operation="add">[% IF rule.add %]Add[% ELSE %]Skip[% END %]</td>
450
                <td class="rule-operation-action" data-operation="append">[% IF rule.append %]Append[% ELSE %]Skip[% END %]</td>
451
                <td class="rule-operation-action" data-operation="remove">[% IF rule.remove %]Remove[% ELSE %]Skip[% END %]</td>
452
                <td class="rule-operation-action" data-operation="delete">[% IF rule.delete %]Delete[% ELSE %]Skip[% END %]</td>
453
                <td class="actions">
454
                    <a href="?op=remove&id=[% rule.id %]" title="Delete" class="btn btn-sm"><i class="fa fa-trash"></i> Delete</a>
455
                    <a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-sm"><i class="fa fa-pencil"></i> Edit</a>
456
                </td>
457
                <td>
458
                    [% IF rule.removemarked %]
459
                        <input type="checkbox" name="batchremove" value="[% rule.id | html %]" checked="checked"/>
460
                    [% ELSE %]
461
                        <input type="checkbox" name="batchremove" value="[% rule.id | html %]"/>
462
                    [% END %]
463
                </td>
464
            [% END %]
465
            </tr>
466
        [% END %]
467
    </tbody>
468
</table>
469
</form>
470
471
<form action="[% script_name %]" method="post">
472
<input type="hidden" name="op" value="redo-matching" />
473
</form>
474
475
</div>
476
</div>
477
<div class="yui-b">
478
[% INCLUDE 'admin-menu.inc' %]
479
</div>
480
</div>
481
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref (+7 lines)
Lines 295-297 Cataloging: Link Here
295
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
295
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
296
            - "<br/>"
296
            - "<br/>"
297
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
297
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
298
        -
299
            - When importing records
300
            - pref: MARCMergeRules
301
              choices:
302
                  yes: "use"
303
                  no: "don't use"
304
            - 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 692-697 Link Here
692
                [% END %]
692
                [% END %]
693
                <input type="hidden" name="op" value="addbiblio" />
693
                <input type="hidden" name="op" value="addbiblio" />
694
                <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
694
                <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
695
                <input type="hidden" name="z3950" value="[% z3950 | html %]" />
695
                <input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
696
                <input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
696
                <input type="hidden" name="breedingid" value="[% breedingid | html %]" />
697
                <input type="hidden" name="breedingid" value="[% breedingid | html %]" />
697
                <input type="hidden" name="changed_framework" value="" />
698
                <input type="hidden" name="changed_framework" value="" />
(-)a/misc/link_bibs_to_authorities.pl (-1 / +1 lines)
Lines 221-227 sub process_bib { Link Here
221
            );
221
            );
222
        }
222
        }
223
        if ( not $test_only ) {
223
        if ( not $test_only ) {
224
            ModBiblio( $bib, $biblionumber, $frameworkcode, 1 );
224
            ModBiblio( $bib, $biblionumber, $frameworkcode, { disable_autolink => 1 });
225
            #Last param is to note ModBiblio was called from linking script and bib should not be linked again
225
            #Last param is to note ModBiblio was called from linking script and bib should not be linked again
226
            $num_bibs_modified++;
226
            $num_bibs_modified++;
227
        }
227
        }
(-)a/misc/migration_tools/bulkmarcimport.pl (-1 / +1 lines)
Lines 455-461 RECORD: while ( ) { Link Here
455
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
455
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
456
                };
456
                };
457
                if ($update) {
457
                if ($update) {
458
                    eval { ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber) ) };
458
                    eval { ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber), {context => {source => 'bulkmarcimport'}} ) };
459
                    if ($@) {
459
                    if ($@) {
460
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
460
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
461
                        printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);
461
                        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 700-706 subtest 'ModBiblio called from linker test' => sub { Link Here
700
    C4::Biblio::ModBiblio($record,$biblionumber,'');
700
    C4::Biblio::ModBiblio($record,$biblionumber,'');
701
    is($called,1,"We called to link bibs because not from linker");
701
    is($called,1,"We called to link bibs because not from linker");
702
    $called = 0;
702
    $called = 0;
703
    C4::Biblio::ModBiblio($record,$biblionumber,'',1);
703
    C4::Biblio::ModBiblio($record,$biblionumber,'',{ disable_autolink => 1 });
704
    is($called,0,"We didn't call to link bibs because from linker");
704
    is($called,0,"We didn't call to link bibs because from linker");
705
};
705
};
706
706
(-)a/t/db_dependent/Biblio/MarcMergeRules.t (+784 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
use Koha::MarcMergeRulesModules;
32
33
use t::lib::Mocks;
34
35
my $schema = Koha::Database->schema;
36
$schema->storage->txn_begin;
37
38
t::lib::Mocks::mock_preference('MARCMergeRules', '1');
39
40
# Create a record
41
my $orig_record = MARC::Record->new();
42
$orig_record->append_fields (
43
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
44
    MARC::Field->new('250', '','', 'a' => '256 bottles of beer on the wall'),
45
    MARC::Field->new('500', '','', 'a' => 'One bottle of beer in the fridge'),
46
);
47
48
my $modules_rs = Koha::MarcMergeRulesModules->search(undef, { order_by => { -desc => 'specificity' } });
49
my @modules;
50
push @modules, $modules_rs->next;
51
52
my $incoming_record = MARC::Record->new();
53
$incoming_record->append_fields(
54
    MARC::Field->new('250', '', '', 'a' => '256 bottles of beer on the wall'), # Unchanged
55
    MARC::Field->new('250', '', '', 'a' => '251 bottles of beer on the wall'), # Appended
56
    # MARC::Field->new('250', '', '', 'a' => '250 bottles of beer on the wall'), # Removed
57
    # MARC::Field->new('500', '', '', 'a' => 'One bottle of beer in the fridge'), # Deleted
58
    MARC::Field->new('501', '', '', 'a' => 'One cold bottle of beer in the fridge'), # Added
59
    MARC::Field->new('501', '', '', 'a' => 'Two cold bottles of beer in the fridge'), # Added
60
);
61
62
# Test default behavior when MARCMergeRules is enabled, but no rules defined (overwrite)
63
subtest 'Record fields has been overwritten when no merge rules are defined' => sub {
64
    plan tests => 4;
65
66
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
67
68
    my @all_fields = $merged_record->fields();
69
70
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
71
    is_deeply(
72
        [map { $_->subfield('a') } $merged_record->field('250') ],
73
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
74
        '"250" fields has been appended and removed'
75
    );
76
77
    my @fields = $merged_record->field('500');
78
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
79
80
    is_deeply(
81
        [map { $_->subfield('a') } $merged_record->field('501') ],
82
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
83
        '"501" fields has been added'
84
    );
85
};
86
87
my $rule =  Koha::MarcMergeRules->find_or_create({
88
    tag => '*',
89
    module => $modules[0]->name,
90
    filter => '*',
91
    add => 0,
92
    append => 0,
93
    remove => 0,
94
    delete => 0
95
});
96
97
subtest 'Record fields has been protected when matched merge all rule operations are set to "0"' => sub {
98
    plan tests => 3;
99
100
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
101
102
    my @all_fields = $merged_record->fields();
103
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
104
105
    is_deeply(
106
        [map { $_->subfield('a') } $merged_record->field('250') ],
107
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
108
        '"250" fields has retained their original value'
109
    );
110
    is_deeply(
111
        [map { $_->subfield('a') } $merged_record->field('500') ],
112
        ['One bottle of beer in the fridge'],
113
        '"500" field has retained it\'s original value'
114
    );
115
};
116
117
subtest 'Only new fields has been added when add = 1, append = 0, remove = 0, delete = 0' => sub {
118
    plan tests => 4;
119
120
    $rule->set(
121
        {
122
            'add' => 1,
123
            'append' => 0,
124
            'remove' => 0,
125
            'delete' => 0,
126
        }
127
    );
128
    $rule->store();
129
130
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
131
132
    my @all_fields = $merged_record->fields();
133
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
134
135
    is_deeply(
136
        [map { $_->subfield('a') } $merged_record->field('250') ],
137
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
138
        '"250" fields retain their original value'
139
    );
140
141
    is_deeply(
142
        [map { $_->subfield('a') } $merged_record->field('500') ],
143
        ['One bottle of beer in the fridge'],
144
        '"500" field retain it\'s original value'
145
    );
146
147
    is_deeply(
148
        [map { $_->subfield('a') } $merged_record->field('501') ],
149
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
150
        '"501" fields has been added'
151
    );
152
};
153
154
subtest 'Only appended fields has been added when add = 0, append = 1, remove = 0, delete = 0' => sub {
155
    plan tests => 3;
156
157
    $rule->set(
158
        {
159
            'add' => 0,
160
            'append' => 1,
161
            'remove' => 0,
162
            'delete' => 0,
163
        }
164
    );
165
    $rule->store();
166
167
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
168
169
    my @all_fields = $merged_record->fields();
170
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
171
172
    is_deeply(
173
        [map { $_->subfield('a') } $merged_record->field('250') ],
174
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
175
        '"251" field has been appended'
176
    );
177
178
    is_deeply(
179
        [map { $_->subfield('a') } $merged_record->field('500') ],
180
        ['One bottle of beer in the fridge'],
181
        '"500" field has retained it\'s original value'
182
    );
183
184
};
185
186
subtest 'Appended and added fields has been added when add = 1, append = 1, remove = 0, delete = 0' => sub {
187
    plan tests => 4;
188
189
    $rule->set(
190
        {
191
            'add' => 1,
192
            'append' => 1,
193
            'remove' => 0,
194
            'delete' => 0,
195
        }
196
    );
197
    $rule->store();
198
199
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
200
201
    my @all_fields = $merged_record->fields();
202
    cmp_ok(scalar @all_fields, '==', 6, "Record has the expected number of fields");
203
204
    is_deeply(
205
        [map { $_->subfield('a') } $merged_record->field('250') ],
206
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
207
        '"251" field has been appended'
208
    );
209
210
    is_deeply(
211
        [map { $_->subfield('a') } $merged_record->field('500') ],
212
        ['One bottle of beer in the fridge'],
213
        '"500" field has retained it\'s original value'
214
    );
215
216
    is_deeply(
217
        [map { $_->subfield('a') } $merged_record->field('501') ],
218
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
219
        '"501" fields has been added'
220
    );
221
};
222
223
subtest 'Record fields has been only removed when add = 0, append = 0, remove = 1, delete = 0' => sub {
224
    plan tests => 3;
225
226
    $rule->set(
227
        {
228
            'add' => 0,
229
            'append' => 0,
230
            'remove' => 1,
231
            'delete' => 0,
232
        }
233
    );
234
    $rule->store();
235
236
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
237
238
    my @all_fields = $merged_record->fields();
239
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
240
241
    is_deeply(
242
        [map { $_->subfield('a') } $merged_record->field('250') ],
243
        ['256 bottles of beer on the wall'],
244
        '"250" field has been removed'
245
    );
246
    is_deeply(
247
        [map { $_->subfield('a') } $merged_record->field('500') ],
248
        ['One bottle of beer in the fridge'],
249
        '"500" field has retained it\'s original value'
250
    );
251
};
252
253
subtest 'Record fields has been added and removed when add = 1, append = 0, remove = 1, delete = 0' => sub {
254
    plan tests => 4;
255
256
    $rule->set(
257
        {
258
            'add' => 1,
259
            'append' => 0,
260
            'remove' => 1,
261
            'delete' => 0,
262
        }
263
    );
264
    $rule->store();
265
266
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
267
268
    my @all_fields = $merged_record->fields();
269
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
270
271
    is_deeply(
272
        [map { $_->subfield('a') } $merged_record->field('250') ],
273
        ['256 bottles of beer on the wall'],
274
        '"250" field has been removed'
275
    );
276
    is_deeply(
277
        [map { $_->subfield('a') } $merged_record->field('500') ],
278
        ['One bottle of beer in the fridge'],
279
        '"500" field has retained it\'s original value'
280
    );
281
282
    is_deeply(
283
        [map { $_->subfield('a') } $merged_record->field('501') ],
284
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
285
        '"501" fields has been added'
286
    );
287
};
288
289
subtest 'Record fields has been appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub {
290
    plan tests => 3;
291
292
    $rule->set(
293
        {
294
            'add' => 0,
295
            'append' => 1,
296
            'remove' => 1,
297
            'delete' => 0,
298
        }
299
    );
300
    $rule->store();
301
302
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
303
304
    my @all_fields = $merged_record->fields();
305
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
306
307
    is_deeply(
308
        [map { $_->subfield('a') } $merged_record->field('250') ],
309
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
310
        '"250" fields has been appended and removed'
311
    );
312
    is_deeply(
313
        [map { $_->subfield('a') } $merged_record->field('500') ],
314
        ['One bottle of beer in the fridge'],
315
        '"500" field has retained it\'s original value'
316
    );
317
};
318
319
subtest 'Record fields has been added, appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub {
320
    plan tests => 4;
321
322
    $rule->set(
323
        {
324
            'add' => 1,
325
            'append' => 1,
326
            'remove' => 1,
327
            'delete' => 0,
328
        }
329
    );
330
    $rule->store();
331
332
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
333
334
    my @all_fields = $merged_record->fields();
335
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
336
337
    is_deeply(
338
        [map { $_->subfield('a') } $merged_record->field('250') ],
339
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
340
        '"250" fields has been appended and removed'
341
    );
342
343
    is_deeply(
344
        [map { $_->subfield('a') } $merged_record->field('500') ],
345
        ['One bottle of beer in the fridge'],
346
        '"500" field has retained it\'s original value'
347
    );
348
349
    is_deeply(
350
        [map { $_->subfield('a') } $merged_record->field('501') ],
351
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
352
        '"501" fields has been added'
353
    );
354
};
355
356
subtest 'Record fields has been deleted when add = 0, append = 0, remove = 0, delete = 1' => sub {
357
    plan tests => 2;
358
359
    $rule->set(
360
        {
361
            'add' => 0,
362
            'append' => 0,
363
            'remove' => 0,
364
            'delete' => 1,
365
        }
366
    );
367
    $rule->store();
368
369
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
370
371
    my @all_fields = $merged_record->fields();
372
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
373
374
    is_deeply(
375
        [map { $_->subfield('a') } $merged_record->field('250') ],
376
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
377
        '"250" fields has retained their original value'
378
    );
379
};
380
381
subtest 'Record fields has been added and deleted when add = 1, append = 0, remove = 0, delete = 1' => sub {
382
    plan tests => 3;
383
384
    $rule->set(
385
        {
386
            'add' => 1,
387
            'append' => 0,
388
            'remove' => 0,
389
            'delete' => 1,
390
        }
391
    );
392
    $rule->store();
393
394
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
395
396
    my @all_fields = $merged_record->fields();
397
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
398
399
    is_deeply(
400
        [map { $_->subfield('a') } $merged_record->field('250') ],
401
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
402
        '"250" fields has retained their original value'
403
    );
404
405
    is_deeply(
406
        [map { $_->subfield('a') } $merged_record->field('501') ],
407
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
408
        '"501" fields has been added'
409
    );
410
};
411
412
subtest 'Record fields has been appended and deleted when add = 0, append = 1, remove = 0, delete = 1' => sub {
413
    plan tests => 2;
414
415
    $rule->set(
416
        {
417
            'add' => 0,
418
            'append' => 1,
419
            'remove' => 0,
420
            'delete' => 1,
421
        }
422
    );
423
    $rule->store();
424
425
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
426
427
    my @all_fields = $merged_record->fields();
428
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
429
430
    is_deeply(
431
        [map { $_->subfield('a') } $merged_record->field('250') ],
432
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
433
        '"250" field has been appended'
434
    );
435
};
436
437
subtest 'Record fields has been added, appended and deleted when add = 1, append = 1, remove = 0, delete = 1' => sub {
438
    plan tests => 3;
439
440
    $rule->set(
441
        {
442
            'add' => 1,
443
            'append' => 1,
444
            'remove' => 0,
445
            'delete' => 1,
446
        }
447
    );
448
    $rule->store();
449
450
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
451
452
    my @all_fields = $merged_record->fields();
453
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
454
455
    is_deeply(
456
        [map { $_->subfield('a') } $merged_record->field('250') ],
457
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
458
        '"250" field has been appended'
459
    );
460
461
    is_deeply(
462
        [map { $_->subfield('a') } $merged_record->field('501') ],
463
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
464
        '"501" fields has been added'
465
    );
466
};
467
468
subtest 'Record fields has been removed and deleted when add = 0, append = 0, remove = 1, delete = 1' => sub {
469
    plan tests => 2;
470
471
    $rule->set(
472
        {
473
            'add' => 0,
474
            'append' => 0,
475
            'remove' => 1,
476
            'delete' => 1,
477
        }
478
    );
479
    $rule->store();
480
481
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
482
483
    my @all_fields = $merged_record->fields();
484
    cmp_ok(scalar @all_fields, '==', 1, "Record has the expected number of fields");
485
486
    is_deeply(
487
        [map { $_->subfield('a') } $merged_record->field('250') ],
488
        ['256 bottles of beer on the wall'],
489
        '"250" field has been removed'
490
    );
491
};
492
493
subtest 'Record fields has been added, removed and deleted when add = 1, append = 0, remove = 1, delete = 1' => sub {
494
    plan tests => 3;
495
496
    $rule->set(
497
        {
498
            'add' => 1,
499
            'append' => 0,
500
            'remove' => 1,
501
            'delete' => 1,
502
        }
503
    );
504
    $rule->store();
505
506
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
507
508
    my @all_fields = $merged_record->fields();
509
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
510
511
    is_deeply(
512
        [map { $_->subfield('a') } $merged_record->field('250') ],
513
        ['256 bottles of beer on the wall'],
514
        '"250" field has been removed'
515
    );
516
517
    is_deeply(
518
        [map { $_->subfield('a') } $merged_record->field('501') ],
519
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
520
        '"501" fields has been added'
521
    );
522
};
523
524
subtest 'Record fields has been appended, removed and deleted when add = 0, append = 1, remove = 1, delete = 1' => sub {
525
    plan tests => 2;
526
527
    $rule->set(
528
        {
529
            'add' => 0,
530
            'append' => 1,
531
            'remove' => 1,
532
            'delete' => 1,
533
        }
534
    );
535
    $rule->store();
536
537
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
538
539
    my @all_fields = $merged_record->fields();
540
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
541
542
    is_deeply(
543
        [map { $_->subfield('a') } $merged_record->field('250') ],
544
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
545
        '"250" fields has been appended and removed'
546
    );
547
};
548
549
subtest 'Record fields has been overwritten when add = 1, append = 1, remove = 1, delete = 1' => sub {
550
    plan tests => 4;
551
552
    $rule->set(
553
        {
554
            'add' => 1,
555
            'append' => 1,
556
            'remove' => 1,
557
            'delete' => 1,
558
        }
559
    );
560
    $rule->store();
561
562
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
563
564
    my @all_fields = $merged_record->fields();
565
566
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
567
    is_deeply(
568
        [map { $_->subfield('a') } $merged_record->field('250') ],
569
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
570
        '"250" fields has been appended and removed'
571
    );
572
573
    my @fields = $merged_record->field('500');
574
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
575
576
    is_deeply(
577
        [map { $_->subfield('a') } $merged_record->field('501') ],
578
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
579
        '"501" fields has been added'
580
    );
581
};
582
583
# Test rule tag specificity
584
585
# Protect field 500 with more specific tag value
586
my $skip_all_rule = Koha::MarcMergeRules->find_or_create({
587
    tag => '500',
588
    module => $modules[0]->name,
589
    filter => '*',
590
    add => 0,
591
    append => 0,
592
    remove => 0,
593
    delete => 0
594
});
595
596
subtest '"500" field has been protected when rule matching on tag "500" is add = 0, append = 0, remove = 0, delete = 0' => sub {
597
    plan tests => 4;
598
599
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
600
601
    my @all_fields = $merged_record->fields();
602
603
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
604
    is_deeply(
605
        [map { $_->subfield('a') } $merged_record->field('250') ],
606
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
607
        '"250" fields has been appended and removed'
608
    );
609
610
    is_deeply(
611
        [map { $_->subfield('a') } $merged_record->field('500') ],
612
        ['One bottle of beer in the fridge'],
613
        '"500" field has retained it\'s original value'
614
    );
615
616
    is_deeply(
617
        [map { $_->subfield('a') } $merged_record->field('501') ],
618
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
619
        '"501" fields has been added'
620
    );
621
};
622
623
# Test regexp matching
624
subtest '"5XX" fields has been protected when rule matching on regexp "5\d{2}" is add = 0, append = 0, remove = 0, delete = 0' => sub {
625
    plan tests => 3;
626
627
    $skip_all_rule->set(
628
        {
629
            'tag' => '5\d{2}',
630
        }
631
    );
632
    $skip_all_rule->store();
633
634
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
635
636
    my @all_fields = $merged_record->fields();
637
638
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
639
    is_deeply(
640
        [map { $_->subfield('a') } $merged_record->field('250') ],
641
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
642
        '"250" fields has been appended and removed'
643
    );
644
645
    is_deeply(
646
        [map { $_->subfield('a') } $merged_record->field('500') ],
647
        ['One bottle of beer in the fridge'],
648
        '"500" field has retained it\'s original value'
649
    );
650
};
651
652
# Test module specificity, the 0 all rule should no longer be included in set of applied rules
653
subtest 'Record fields has been overwritten when non wild card rule with filter match is add = 1, append = 1, remove = 1, delete = 1' => sub {
654
    plan tests => 4;
655
656
    $rule->set(
657
        {
658
            'filter' => 'test',
659
        }
660
    );
661
    $rule->store();
662
663
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
664
665
    my @all_fields = $merged_record->fields();
666
667
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
668
    is_deeply(
669
        [map { $_->subfield('a') } $merged_record->field('250') ],
670
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
671
        '"250" fields has been appended and removed'
672
    );
673
674
    my @fields = $merged_record->field('500');
675
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
676
677
    is_deeply(
678
        [map { $_->subfield('a') } $merged_record->field('501') ],
679
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
680
        '"501" fields has been added'
681
    );
682
};
683
684
subtest 'An exception is thrown when append = 1, remove = 0 is set for control field rule' => sub {
685
    plan tests => 2;
686
    my $exception = try {
687
        Koha::MarcMergeRules->validate({
688
            'tag' => '008',
689
            'append' => 1,
690
            'remove' => 0,
691
        });
692
    }
693
    catch {
694
        return $_;
695
    };
696
    ok(defined $exception, "Exception was caught");
697
    ok($exception->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions'), "Exception is of correct class");
698
};
699
700
subtest 'An exception is thrown when rule tag is set to invalid regexp' => sub {
701
    plan tests => 2;
702
703
    my $exception = try {
704
        Koha::MarcMergeRules->validate({
705
            'tag' => '**'
706
        });
707
    }
708
    catch {
709
        return $_;
710
    };
711
    ok(defined $exception, "Exception was caught");
712
    ok($exception->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp'), "Exception is of correct class");
713
};
714
715
$skip_all_rule->delete();
716
717
subtest 'context option in ModBiblio is handled correctly' => sub {
718
    plan tests => 6;
719
    $rule->set(
720
        {
721
            tag => '250',
722
            module => $modules[0]->name,
723
            filter => '*',
724
            'add' => 0,
725
            'append' => 0,
726
            'remove' => 0,
727
            'delete' => 0,
728
        }
729
    );
730
    $rule->store();
731
    my ($biblionumber) = AddBiblio($orig_record, '');
732
733
    # Since marc merc rules are not run on save, only update
734
    # saved record should be identical to orig_record
735
    my $saved_record = GetMarcBiblio({ biblionumber => $biblionumber });
736
737
    my @all_fields = $saved_record->fields();
738
    # Koha also adds 999c field, therefore 4 not 3
739
    cmp_ok(scalar @all_fields, '==', 4, 'Saved record has the expected number of fields');
740
    is_deeply(
741
        [map { $_->subfield('a') } $saved_record->field('250') ],
742
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
743
        'All "250" fields of saved record are identical to original record passed to AddBiblio'
744
    );
745
746
    is_deeply(
747
        [map { $_->subfield('a') } $saved_record->field('500') ],
748
        ['One bottle of beer in the fridge'],
749
        'All "500" fields of saved record are identical to original record passed to AddBiblio'
750
    );
751
752
    $saved_record->append_fields(
753
        MARC::Field->new('250', '', '', 'a' => '251 bottles of beer on the wall'), # Appended
754
        MARC::Field->new('500', '', '', 'a' => 'One cold bottle of beer in the fridge'), # Appended
755
    );
756
757
    ModBiblio($saved_record, $biblionumber, '', { context => { $modules[0]->name => 'test' } });
758
759
    my $updated_record = GetMarcBiblio({ biblionumber => $biblionumber });
760
761
    @all_fields = $updated_record->fields();
762
    cmp_ok(scalar @all_fields, '==', 5, 'Updated record has the expected number of fields');
763
    is_deeply(
764
        [map { $_->subfield('a') } $updated_record->field('250') ],
765
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
766
        '"250" fields have retained their original values'
767
    );
768
769
    is_deeply(
770
        [map { $_->subfield('a') } $updated_record->field('500') ],
771
        ['One bottle of beer in the fridge', 'One cold bottle of beer in the fridge'],
772
        '"500" field has been appended'
773
    );
774
775
    # To trigger removal from search index etc
776
    DelBiblio($biblionumber);
777
};
778
779
# Explicityly delete rule to trigger clearing of cache
780
$rule->delete();
781
782
$schema->storage->txn_rollback;
783
784
1;
(-)a/tools/batch_record_modification.pl (-2 / +8 lines)
Lines 205-211 if ( $op eq 'form' ) { Link Here
205
                my $record = GetMarcBiblio({ biblionumber => $biblionumber });
205
                my $record = GetMarcBiblio({ biblionumber => $biblionumber });
206
                ModifyRecordWithTemplate( $mmtid, $record );
206
                ModifyRecordWithTemplate( $mmtid, $record );
207
                my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
207
                my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
208
                ModBiblio( $record, $biblionumber, $frameworkcode );
208
                my ($member) = Koha::Patrons->find($loggedinuser);
209
                ModBiblio( $record, $biblionumber, $frameworkcode,
210
                    {
211
                        source => 'batchmod',
212
                        categorycode => $member->categorycode,
213
                        userid => $member->userid
214
                    }
215
                );
209
            };
216
            };
210
            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
217
            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
211
                push @messages, {
218
                push @messages, {
212
- 

Return to bug 14957