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

(-)a/C4/Biblio.pm (-9 / +86 lines)
Lines 64-69 BEGIN { Link Here
64
        DelBiblio
64
        DelBiblio
65
        BiblioAutoLink
65
        BiblioAutoLink
66
        LinkBibHeadingsToAuthorities
66
        LinkBibHeadingsToAuthorities
67
        ApplyMarcMergeRules
67
        TransformMarcToKoha
68
        TransformMarcToKoha
68
        TransformHtmlToMarc
69
        TransformHtmlToMarc
69
        TransformHtmlToXml
70
        TransformHtmlToXml
Lines 107-116 use Koha::ItemTypes; Link Here
107
use Koha::SearchEngine;
108
use Koha::SearchEngine;
108
use Koha::Libraries;
109
use Koha::Libraries;
109
use Koha::Util::MARC;
110
use Koha::Util::MARC;
111
use Koha::MarcMergeRules;
110
112
111
use vars qw($debug $cgi_debug);
113
use vars qw($debug $cgi_debug);
112
114
113
114
=head1 NAME
115
=head1 NAME
115
116
116
C4::Biblio - cataloging management functions
117
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);
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 261-267 Returns 1 on success 0 on failure Link Here
261
=cut
262
=cut
262
263
263
sub ModBiblio {
264
sub ModBiblio {
264
    my ( $record, $biblionumber, $frameworkcode ) = @_;
265
    my ( $record, $biblionumber, $frameworkcode, $options ) = @_;
266
    $options //= {};
267
265
    if (!$record) {
268
    if (!$record) {
266
        carp 'No record passed to ModBiblio';
269
        carp 'No record passed to ModBiblio';
267
        return 0;
270
        return 0;
Lines 293-298 sub ModBiblio { Link Here
293
296
294
    _strip_item_fields($record, $frameworkcode);
297
    _strip_item_fields($record, $frameworkcode);
295
298
299
    # apply merge rules
300
    if (C4::Context->preference('MARCMergeRules') && $biblionumber && defined $options && exists $options->{'context'}) {
301
        $record = ApplyMarcMergeRules({
302
                biblionumber => $biblionumber,
303
                record => $record,
304
                context => $options->{'context'},
305
            }
306
        );
307
    }
308
296
    # update biblionumber and biblioitemnumber in MARC
309
    # update biblionumber and biblioitemnumber in MARC
297
    # FIXME - this is assuming a 1 to 1 relationship between
310
    # FIXME - this is assuming a 1 to 1 relationship between
298
    # biblios and biblioitems
311
    # biblios and biblioitems
Lines 303-315 sub ModBiblio { Link Here
303
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
316
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
304
317
305
    # load the koha-table data object
318
    # load the koha-table data object
306
    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode );
319
    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode, undef, $biblionumber);
307
320
308
    # update MARC subfield that stores biblioitems.cn_sort
321
    # update MARC subfield that stores biblioitems.cn_sort
309
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
322
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
310
323
311
    # update the MARC record (that now contains biblio and items) with the new record data
324
    # update the MARC record (that now contains biblio and items) with the new record data
312
    &ModBiblioMarc( $record, $biblionumber, $frameworkcode );
325
    ModBiblioMarc( $record, $biblionumber, $frameworkcode );
313
326
314
    # modify the other koha tables
327
    # modify the other koha tables
315
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
328
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
Lines 2543-2549 sub TransformHtmlToMarc { Link Here
2543
2556
2544
=head2 TransformMarcToKoha
2557
=head2 TransformMarcToKoha
2545
2558
2546
    $result = TransformMarcToKoha( $record, undef, $limit )
2559
    $result = TransformMarcToKoha($record, undef, $biblionumber, $option)
2547
2560
2548
Extract data from a MARC bib record into a hashref representing
2561
Extract data from a MARC bib record into a hashref representing
2549
Koha biblio, biblioitems, and items fields.
2562
Koha biblio, biblioitems, and items fields.
Lines 2554-2560 hash_ref. Link Here
2554
=cut
2567
=cut
2555
2568
2556
sub TransformMarcToKoha {
2569
sub TransformMarcToKoha {
2557
    my ( $record, $frameworkcode, $limit_table ) = @_;
2570
    my ( $record, $frameworkcode, $limit_table, $biblionumber ) = @_;
2558
    # FIXME  Parameter $frameworkcode is obsolete and will be removed
2571
    # FIXME  Parameter $frameworkcode is obsolete and will be removed
2559
    $limit_table //= q{};
2572
    $limit_table //= q{};
2560
2573
Lines 3266-3272 sub _koha_delete_biblio_metadata { Link Here
3266
3279
3267
=head2 ModBiblioMarc
3280
=head2 ModBiblioMarc
3268
3281
3269
  &ModBiblioMarc($newrec,$biblionumber,$frameworkcode);
3282
  &ModBiblioMarc($newrec, $biblionumber, $frameworkcode, $options);
3270
3283
3271
Add MARC XML data for a biblio to koha
3284
Add MARC XML data for a biblio to koha
3272
3285
Lines 3602-3609 sub RemoveAllNsb { Link Here
3602
    return $record;
3615
    return $record;
3603
}
3616
}
3604
3617
3605
1;
3618
=head2 ApplyMarcMergeRules
3619
3620
    my $record = ApplyMarcMergeRules($params)
3621
3622
Applies marc merge rules to a record.
3623
3624
C<$params> is expected to be a hashref with below keys defined.
3625
3626
=over 4
3627
3628
=item C<biblionumber>
3629
biblionumber of old record
3630
3631
=item C<record>
3632
Incoming record that will be merged with old record
3633
3634
=item C<context>
3635
hashref containing at least one module from marc_merge_rules_modules table
3636
with filter value on the form {module => filter, ...}. Three predefined
3637
context modules exists:
3638
3639
    * source
3640
    * category
3641
    * borrower
3642
3643
=back
3644
3645
Returns:
3606
3646
3647
=over 4
3648
3649
=item C<$record>
3650
3651
Merged MARC record based with merge rules for C<context> applied. If no old
3652
record for C<biblionumber> can be found, C<record> is returned unchanged.
3653
Default action when no matching context is found to return C<record> unchanged.
3654
If no rules are found for a certain field tag the default is to overwrite with
3655
fields with this field tag from C<record>.
3656
3657
=back
3658
3659
=cut
3660
3661
sub ApplyMarcMergeRules {
3662
    my ($params) = @_;
3663
    my $biblionumber = $params->{biblionumber};
3664
    my $incoming_record = $params->{record};
3665
3666
    if (!$biblionumber) {
3667
        carp 'ApplyMarcMergeRules called on undefined biblionumber';
3668
        return;
3669
    }
3670
    if (!$incoming_record) {
3671
        carp 'ApplyMarcMergeRules called on undefined record';
3672
        return;
3673
    }
3674
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3675
3676
    # Skip merge rules if called with no context
3677
    if ($old_record && defined $params->{context}) {
3678
        return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
3679
    }
3680
    return $incoming_record;
3681
}
3682
3683
1;
3607
3684
3608
__END__
3685
__END__
3609
3686
(-)a/C4/ImportBatch.pm (-1 / +1 lines)
Lines 667-673 sub BatchCommitRecords { Link Here
667
                }
667
                }
668
                $oldxml = $old_marc->as_xml($marc_type);
668
                $oldxml = $old_marc->as_xml($marc_type);
669
669
670
                ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode);
670
                ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode, {context => {source => 'batchimport'}});
671
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?";
671
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?";
672
672
673
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
673
                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 (+40 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 SearchField Object class
27
28
=cut
29
30
sub store {
31
    my $self = shift @_;
32
    $cache->clear_from_cache('marc_merge_rules');
33
    $self->SUPER::store(@_);
34
}
35
36
sub _type {
37
    return 'MarcMergeRule';
38
}
39
40
1;
(-)a/Koha/MarcMergeRules.pm (+348 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
sub operations {
30
    return ('add', 'append', 'remove', 'delete');
31
}
32
33
my $cache = Koha::Caches->get_instance();
34
35
=head1 NAME
36
37
Koha::MarcMergeRules - Koha MarcMergeRules Object set class
38
39
=head1 API
40
41
=head2 Class Methods
42
43
=head3 context_rules
44
45
    my $rules = Koha::MarcMergeRules->context_rules($context);
46
47
Gets all MARC merge rules for the supplied C<$context> (hashref with { module => filter, ... } values).
48
49
=cut
50
51
sub context_rules {
52
    my ($self, $context) = @_;
53
54
    return unless %{$context};
55
56
    my $rules = $cache->get_from_cache('marc_merge_rules', { unsafe => 1 });
57
58
    if (!$rules) {
59
        $rules = {};
60
        my @rules_rows = $self->_resultset()->search(
61
            undef,
62
            {
63
                prefetch => 'module',
64
                order_by => { -desc => [qw/module.specificity me.id/] }
65
            }
66
        );
67
        foreach my $rule_row (@rules_rows) {
68
            my %rule = $rule_row->get_columns();
69
            my $operations = {};
70
71
            foreach my $operation ($self->operations) {
72
                $operations->{$operation} = { allow => $rule{$operation}, rule => $rule{id} };
73
            }
74
75
            # TODO: Remove unless check and validate on saving rules?
76
            if ($rule{tag} eq '*') {
77
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{'*'}) {
78
                    $rules->{$rule{module}}->{$rule{filter}}->{'*'} = $operations;
79
                }
80
            }
81
            elsif ($rule{tag} =~ /^(\d{3})$/) {
82
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}}) {
83
                    $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}} = $operations;
84
                }
85
            }
86
            else {
87
                my $regexps = ($rules->{$rule{module}}->{$rule{filter}}->{regexps} //= []);
88
                push @{$regexps}, [$rule{tag}, $operations];
89
            }
90
        }
91
        $cache->set_in_cache('marc_merge_rules', $rules);
92
    }
93
94
    my $context_rules = undef;
95
    foreach my $module_name (keys %{$context}) {
96
        if (
97
            exists $rules->{$module_name} &&
98
            exists $rules->{$module_name}->{$context->{$module_name}}
99
        ) {
100
            $context_rules = $rules->{$module_name}->{$context->{$module_name}};
101
            last;
102
        }
103
    }
104
    if (!$context_rules) {
105
        # No perms matching specific context conditions found, try wildcard value for each active context
106
        foreach my $module_name (keys %{$context}) {
107
            if (exists $rules->{$module_name}->{'*'}) {
108
                $context_rules = $rules->{$module_name}->{'*'};
109
                last;
110
            }
111
        }
112
    }
113
    return $context_rules;
114
}
115
116
=head3 merge_records
117
118
    my $merged_record = Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $context);
119
120
Merge C<$old_record> with C<$incoming_record> applying merge rules for C<$context>.
121
Returns merged record C<$merged_record>. C<$old_record>, C<$incoming_record> and
122
C<$merged_record> are all MARC::Record objects.
123
124
=cut
125
126
sub merge_records {
127
    my ($self, $old_record, $incoming_record, $context) = @_;
128
129
    my $rules = $self->context_rules($context);
130
131
    # Default when no rules found is to overwrite with incoming record
132
    return $incoming_record unless $rules;
133
134
    my $fields_by_tag = sub {
135
        my ($record) = @_;
136
        my $fields = {};
137
        foreach my $field ($record->fields()) {
138
            $fields->{$field->tag()} //= [];
139
            push @{$fields->{$field->tag()}}, $field;
140
        }
141
        return $fields;
142
    };
143
144
    my $hash_field_data = sub {
145
        my ($field) = @_;
146
        my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
147
        return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields());
148
    };
149
150
    my $diff_by_key = sub {
151
        my ($a, $b) = @_;
152
        my @removed;
153
        my @intersecting;
154
        my @added;
155
        my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
156
        foreach my $key (keys %keys_index) {
157
            if ($a->{$key} && $b->{$key}) {
158
                push @intersecting, $a->{$key};
159
            }
160
            elsif ($a->{$key}) {
161
                push @removed, $a->{$key};
162
            }
163
            else {
164
                push @added, $b->{$key};
165
            }
166
        }
167
        return (\@removed, \@intersecting, \@added);
168
    };
169
170
    my $tag_rules = $rules->{tags} // {};
171
    my $default_rule = $rules->{'*'} // {
172
        add => { allow => 1, 'rule' => 0},
173
        append => { allow => 1, 'rule' => 0},
174
        delete => { allow => 1, 'rule' => 0},
175
        remove => { allow => 1, 'rule' => 0},
176
    };
177
178
    # Precompile regexps
179
    my @regexp_rules = map { { regexp => qr/^$_->[0]$/, actions => $_->[1] } } @{$rules->{regexps} // []};
180
181
    my $get_matching_field_rule = sub {
182
        my ($tag) = @_;
183
        # Exact match takes precedence, then regexp, then wildcard/defaults
184
        return $tag_rules->{$tag} //
185
            %{(first { $tag =~ $_->{regexp} } @regexp_rules) // {}}{actions} //
186
            $default_rule;
187
    };
188
189
    my %merged_record_fields;
190
191
    my $current_fields = $fields_by_tag->($old_record);
192
    my $incoming_fields = $fields_by_tag->($incoming_record);
193
194
    # First we get all new incoming fields
195
    my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
196
    foreach my $tag (@new_field_tags) {
197
        my $rule = $get_matching_field_rule->($tag);
198
        if ($rule->{add}->{allow}) {
199
            $merged_record_fields{$tag} //= [];
200
            push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
201
        }
202
    }
203
204
    # Then we get all fields no longer present in incoming fields
205
    my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
206
    foreach my $tag (@deleted_field_tags) {
207
        my $rule = $get_matching_field_rule->($tag);
208
        if (!$rule->{delete}->{allow}) {
209
            $merged_record_fields{$tag} //= [];
210
            push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
211
        }
212
    }
213
214
    # Then we get the intersection of fields, present both in
215
    # current and incoming record (possibly to be overwritten)
216
    my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
217
    foreach my $tag (@common_field_tags) {
218
        my $rule = $get_matching_field_rule->($tag);
219
220
        # Special handling for control fields
221
        if ($tag < 10) {
222
            if (
223
                $rule->{append}->{allow} &&
224
                !$rule->{remove}->{allow}
225
            ) {
226
                # This should be highly unlikely since we have input validation to protect against this case
227
                carp "Allowing \"append\" and skipping \"remove\" is not permitted for control fields, falling back to skipping both \"append\" and \"remove\"";
228
                push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
229
            }
230
            elsif ($rule->{append}->{allow}) {
231
                push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
232
            }
233
            else {
234
                push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
235
            }
236
        }
237
        else {
238
            # Compute intersection and diff using field data
239
            my $sort_weight = 0;
240
            my %current_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$current_fields->{$tag}};
241
242
            # Always put incoming fields after current fields
243
            my %incoming_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$incoming_fields->{$tag}};
244
245
            my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
246
247
            my @merged_fields;
248
249
            # First add common fields (intersection)
250
            # Unchanged
251
            if (@{$common_fields}) {
252
                push @merged_fields, @{$common_fields};
253
            }
254
            # Removed
255
            if (@{$current_fields_only}) {
256
                if (!$rule->{remove}->{allow}) {
257
                    push @merged_fields, @{$current_fields_only};
258
                }
259
            }
260
            # Appended
261
            if (@{$incoming_fields_only}) {
262
                if ($rule->{append}->{allow}) {
263
                    push @merged_fields, @{$incoming_fields_only};
264
                }
265
            }
266
            $merged_record_fields{$tag} //= [];
267
268
            # Sort ascending according to weight (original order)
269
            push @{$merged_record_fields{$tag}}, map { $_->[1] } sort { $a->[0] <=> $b->[0] } @merged_fields;
270
        }
271
    }
272
273
    my $merged_record = MARC::Record->new();
274
275
    # Leader is always overwritten, or kept???
276
    $merged_record->leader($incoming_record->leader());
277
278
    if (%merged_record_fields) {
279
        foreach my $tag (sort keys %merged_record_fields) {
280
            $merged_record->append_fields(@{$merged_record_fields{$tag}});
281
        }
282
    }
283
    return $merged_record;
284
}
285
286
sub _clear_caches {
287
    $cache->clear_from_cache('marc_merge_rules');
288
}
289
290
sub find_or_create {
291
    my $self = shift @_;
292
    $self->_clear_caches();
293
    $self->SUPER::find_or_create(@_);
294
}
295
296
sub update {
297
    my $self = shift @_;
298
    $self->_clear_caches();
299
    return $self->SUPER::update(@_);
300
}
301
302
sub delete {
303
    my $self = shift @_;
304
    $self->_clear_caches();
305
    return $self->SUPER::delete(@_);
306
}
307
308
sub validate {
309
    my ($self, $rule_data) = @_;
310
311
    #use Data::Dumper;
312
    #Koha::Exceptions::MarcMergeRule::InvalidTagRegExp->throw(
313
    #    Dumper($rule_data)
314
    #);
315
316
    if(exists $rule_data->{tag}) {
317
        if ($rule_data->{tag} ne '*') {
318
            eval { qr/$rule_data->{tag}/ };
319
            if ($@) {
320
                Koha::Exceptions::MarcMergeRule::InvalidTagRegExp->throw(
321
                    "Invalid tag regular expression"
322
                );
323
            }
324
        }
325
        # TODO: Regexp or '*' that match controlfield not currently detected
326
        if (
327
            looks_like_number($rule_data->{tag}) &&
328
            $rule_data->{tag} < 10 &&
329
            $rule_data->{append} &&
330
            !$rule_data->{remove}
331
        ) {
332
            Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions->throw(
333
                "Combination of allow append and skip remove not permitted for control fields"
334
            );
335
        }
336
    }
337
    return 1;
338
}
339
340
sub _type {
341
    return 'MarcMergeRule';
342
}
343
344
sub object_class {
345
    return 'Koha::MarcMergeRule';
346
}
347
348
1;
(-)a/Koha/MarcMergeRulesModule.pm (+40 lines)
Line 0 Link Here
1
package Koha::MarcMergeRulesModule;
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
=head1 NAME
23
24
Koha::MarcMergeRulesModule - Koha SearchField Object class
25
26
=head1 API
27
28
=head2 Class Methods
29
30
=cut
31
32
=head3 type
33
34
=cut
35
36
sub _type {
37
    return 'MarcMergeRulesModule';
38
}
39
40
1;
(-)a/Koha/MarcMergeRulesModules.pm (+31 lines)
Line 0 Link Here
1
package Koha::MarcMergeRulesModules;
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 Koha::MarcMergeRulesModule;
20
21
use parent qw(Koha::Objects);
22
23
sub _type {
24
    return 'MarcMergeRulesModule';
25
}
26
27
sub object_class {
28
    return 'Koha::MarcMergeRulesModule';
29
}
30
31
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 strict;
19
use warnings;
20
21
# standard or CPAN modules used
22
use CGI qw ( -utf8 );
23
use CGI::Cookie;
24
use MARC::File::USMARC;
25
use Try::Tiny;
26
27
# Koha modules used
28
use C4::Context;
29
use C4::Koha;
30
use C4::Auth;
31
use C4::AuthoritiesMarc;
32
use C4::Output;
33
use C4::Biblio;
34
use C4::ImportBatch;
35
use C4::Matcher;
36
use C4::BackgroundJob;
37
use C4::Labels::Batch;
38
use Koha::MarcMergeRules;
39
use Koha::MarcMergeRule;
40
use Koha::MarcMergeRulesModules;
41
42
my $script_name = "/cgi-bin/koha/admin/marc-merge-rules.pl";
43
44
my $input = new CGI;
45
my $op = $input->param('op') || '';
46
my $errors = [];
47
48
my $rule_from_cgi = sub {
49
    my ($cgi) = @_;
50
51
    my %rule = map { $_ => scalar $cgi->param($_) } (
52
        'tag',
53
        'module',
54
        'filter',
55
        'add',
56
        'append',
57
        'remove',
58
        'delete'
59
    );
60
61
    my $id = $cgi->param('id');
62
    if ($id) {
63
        $rule{id} = $id;
64
    }
65
66
    return \%rule;
67
};
68
69
my ($template, $loggedinuser, $cookie) = get_template_and_user(
70
    {
71
        template_name   => "admin/marc-merge-rules.tt",
72
        query           => $input,
73
        type            => "intranet",
74
        authnotrequired => 0,
75
        flagsrequired   => { parameters => 'manage_marc_merge_rules' },
76
        debug           => 1,
77
    }
78
);
79
80
my %cookies = parse CGI::Cookie($cookie);
81
our $sessionID = $cookies{'CGISESSID'}->value;
82
83
my $get_rules = sub {
84
    # TODO: order?
85
    return [map { { $_->get_columns() } } Koha::MarcMergeRules->_resultset->all];
86
};
87
my $rules;
88
89
if ($op eq 'remove' || $op eq 'doremove') {
90
    my @remove_ids = $input->multi_param('batchremove');
91
    push @remove_ids, scalar $input->param('id') if $input->param('id');
92
    if ($op eq 'remove') {
93
        $template->{VARS}->{removeConfirm} = 1;
94
        my %remove_ids = map { $_ => undef } @remove_ids;
95
        $rules = $get_rules->();
96
        for my $rule (@{$rules}) {
97
            $rule->{'removemarked'} = 1 if exists $remove_ids{$rule->{id}};
98
        }
99
    }
100
    elsif ($op eq 'doremove') {
101
        my @remove_ids = $input->multi_param('batchremove');
102
        push @remove_ids, scalar $input->param('id') if $input->param('id');
103
        Koha::MarcMergeRules->search({ id => { in => \@remove_ids } })->delete();
104
        $rules = $get_rules->();
105
    }
106
}
107
elsif ($op eq 'edit') {
108
    $template->{VARS}->{edit} = 1;
109
    my $id = $input->param('id');
110
    $rules = $get_rules->();
111
    for my $rule(@{$rules}) {
112
        if ($rule->{id} == $id) {
113
            $rule->{'edit'} = 1;
114
            last;
115
        }
116
    }
117
}
118
elsif ($op eq 'doedit' || $op eq 'add') {
119
    my $rule_data = $rule_from_cgi->($input);
120
    if (!@{$errors}) {
121
        try {
122
            Koha::MarcMergeRules->validate($rule_data);
123
        }
124
        catch {
125
            die $_ unless blessed $_ && $_->can('rethrow');
126
127
            if ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp')) {
128
                push @{$errors}, {
129
                    type => 'error',
130
                    code => 'invalid_tag_regexp',
131
                    tag => $rule_data->{tag},
132
                };
133
            }
134
            elsif ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions')) {
135
                push @{$errors}, {
136
                    type => 'error',
137
                    code => 'invalid_control_field_actions',
138
                    tag => $rule_data->{tag},
139
                };
140
            }
141
            else {
142
                $_->rethrow;
143
            }
144
        };
145
        if (!@{$errors}) {
146
            my $rule = Koha::MarcMergeRules->find_or_create($rule_data);
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 $modules = Koha::MarcMergeRulesModules->as_list;
158
$template->param( rules => $rules, modules => $modules, 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 830-836 if ( $op eq "addbiblio" ) { Link Here
830
    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
831
    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
831
        my $oldbibitemnum;
832
        my $oldbibitemnum;
832
        if ( $is_a_modif ) {
833
        if ( $is_a_modif ) {
833
            ModBiblio( $record, $biblionumber, $frameworkcode );
834
            my $member = Koha::Patrons->find($loggedinuser);
835
            ModBiblio( $record, $biblionumber, $frameworkcode, {
836
                    context => {
837
                        source => $z3950 ? 'z39.50' : 'intranet',
838
                        category => $member->{'category_type'},
839
                        borrower => $loggedinuser
840
                    }
841
                }
842
            );
834
        }
843
        }
835
        else {
844
        else {
836
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
845
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
Lines 923-928 elsif ( $op eq "delete" ) { Link Here
923
    $template->param(
932
    $template->param(
924
        biblionumberdata => $biblionumber,
933
        biblionumberdata => $biblionumber,
925
        op               => $op,
934
        op               => $op,
935
        z3950            => $z3950
926
    );
936
    );
927
    if ( $op eq "duplicate" ) {
937
    if ( $op eq "duplicate" ) {
928
        $biblionumber = "";
938
        $biblionumber = "";
(-)a/installer/data/mysql/atomicupdate/bug_14957-marc-merge-rules-permissions.sql (+2 lines)
Line 0 Link Here
1
INSERT INTO permissions (module_bit, code, description) VALUES
2
  ( 3, 'manage_marc_merge_rules', 'Manage MARC merge rules configuration');
(-)a/installer/data/mysql/atomicupdate/bug_14957-marc-merge-rules.sql (+30 lines)
Line 0 Link Here
1
DROP TABLE IF EXISTS `marc_merge_rules`;
2
DROP TABLE IF EXISTS `marc_merge_rules_modules`;
3
4
CREATE TABLE `marc_merge_rules_modules` (
5
    `name` varchar(127) NOT NULL,
6
    `description` varchar(255),
7
    `specificity` int(11) NOT NULL UNIQUE, -- higher specificity will override rules with lower specificity
8
    PRIMARY KEY(`name`)
9
);
10
11
-- a couple of useful default filter modules
12
-- these are used in various scripts, so don't remove them if you don't know
13
-- what you're doing.
14
-- New filter modules can be added here when needed
15
INSERT INTO `marc_merge_rules_modules` VALUES('source', 'source from where modification request was sent', 0);
16
INSERT INTO `marc_merge_rules_modules` VALUES('category', 'categorycode of user who requested modification', 1);
17
INSERT INTO `marc_merge_rules_modules` VALUES('borrower', 'borrowernumber of user who requested modification', 2);
18
19
CREATE TABLE IF NOT EXISTS `marc_merge_rules` (
20
    `id` int(11) NOT NULL auto_increment,
21
    `tag` varchar(255) NOT NULL, -- can be regexp, so need > 3 chars
22
    `module` varchar(127) NOT NULL,
23
    `filter` varchar(255) NOT NULL,
24
    `add` tinyint NOT NULL,
25
    `append` tinyint NOT NULL,
26
    `remove` tinyint NOT NULL,
27
    `delete` tinyint NOT NULL,
28
    PRIMARY KEY(`id`),
29
    CONSTRAINT `marc_merge_rules_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_merge_rules_modules` (`name`) ON DELETE CASCADE
30
);
(-)a/installer/data/mysql/atomicupdate/bug_14957-merge-rules-syspref.sql (+1 lines)
Line 0 Link Here
1
INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('MARCMergeRules','0','','Use the MARC merge rules system to decide what actions to take for each field when modifying records.','YesNo');
(-)a/installer/data/mysql/userpermissions.sql (+1 lines)
Lines 24-29 INSERT INTO permissions (module_bit, code, description) VALUES Link Here
24
   ( 3, 'manage_oai_sets', 'Manage OAI sets'),
24
   ( 3, 'manage_oai_sets', 'Manage OAI sets'),
25
   ( 3, 'manage_item_search_fields', 'Manage item search fields'),
25
   ( 3, 'manage_item_search_fields', 'Manage item search fields'),
26
   ( 3, 'manage_search_engine_config', 'Manage search engine configuration'),
26
   ( 3, 'manage_search_engine_config', 'Manage search engine configuration'),
27
   ( 3, 'manage_marc_merge_rules', 'Manage MARC merge rules configuration'),
27
   ( 3, 'manage_search_targets', 'Manage Z39.50 and SRU server configuration'),
28
   ( 3, 'manage_search_targets', 'Manage Z39.50 and SRU server configuration'),
28
   ( 3, 'manage_didyoumean', 'Manage Did you mean? configuration'),
29
   ( 3, 'manage_didyoumean', 'Manage Did you mean? configuration'),
29
   ( 3, 'manage_column_config', 'Manage column configuration'),
30
   ( 3, 'manage_column_config', 'Manage column configuration'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc (+3 lines)
Lines 80-85 Link Here
80
            [% IF ( Koha.Preference('SearchEngine') == 'Elasticsearch' && CAN_user_parameters_manage_search_engine_config ) %]
80
            [% IF ( Koha.Preference('SearchEngine') == 'Elasticsearch' && CAN_user_parameters_manage_search_engine_config ) %]
81
                <li><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></li>
81
                <li><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></li>
82
            [% END %]
82
            [% END %]
83
            [% IF ( CAN_user_parameters_manage_marc_merge_rules ) %]
84
                <li><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></li>
85
            [% END %]
83
        </ul>
86
        </ul>
84
    [% END %]
87
    [% END %]
85
88
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc (+5 lines)
Lines 201-206 Link Here
201
            Manage search engine configuration
201
            Manage search engine configuration
202
        </span>
202
        </span>
203
        <span class="permissioncode">([% name | html %])</span>
203
        <span class="permissioncode">([% name | html %])</span>
204
    [%- CASE 'manage_marc_merge_rules' -%]
205
        <span class="sub_permission manage_marc_merge_rules_subpermission">
206
          Manage MARC merge rules configuration
207
        </span>
208
        <span class="permissioncode">([% name | html %])</span>
204
    [%- CASE 'manage_search_targets' -%]
209
    [%- CASE 'manage_search_targets' -%]
205
        <span class="sub_permission manage_search_targets_subpermission">
210
        <span class="sub_permission manage_search_targets_subpermission">
206
            Manage Z39.50 and SRU server configuration
211
            Manage Z39.50 and SRU server configuration
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+2 lines)
Lines 147-152 Link Here
147
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
147
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
148
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
148
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
149
                    [% END %]
149
                    [% END %]
150
                    <dt><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></dt>
151
                    <dd>Managed MARC field merge rules</dd>
150
                </dl>
152
                </dl>
151
            [% END %]
153
            [% END %]
152
154
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc-merge-rules.tt (+395 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
<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/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
      'Protect existing': {
119
        'add': 1,
120
        'append': 0,
121
        'remove': 0,
122
        'delete': 0
123
      },
124
      'Add only': {
125
        'add': 1,
126
        'append': 1,
127
        'remove': 0,
128
        'delete': 0
129
      },
130
      'Protect from delete': {
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
});
194
</script>
195
</head>
196
<body id="admin_marc-merge-rules" class="admin">
197
[% INCLUDE 'header.inc' %]
198
[% INCLUDE 'cat-search.inc' %]
199
200
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
201
 &rsaquo; MARC merge rules
202
</div>
203
204
<div id="doc3" class="yui-t2">
205
   <div id="bd">
206
    <div id="yui-main">
207
    <div class="yui-b">
208
209
<h1>Manage MARC merge rules</h1>
210
211
[% FOR m IN messages %]
212
  <div class="dialog [% m.type | html %]">
213
    [% SWITCH m.code %]
214
    [% CASE 'invalid_tag_regexp' %]
215
      Invalid regular expression "[% m.tag | html %]".
216
    [% CASE 'invalid_control_field_actions' %]
217
      Invalid combination of actions for tag [% m.tag | html %]. Control field rules do not allow "Appended: Append" and "Removed: Skip".
218
    [% CASE %]
219
      [% m.code | html %]
220
    [% END %]
221
  </div>
222
[% END %]
223
224
[% UNLESS Koha.Preference( 'MARCMergeRules' ) %]
225
    <div class="dialog message">
226
        The <b>MARCMergeRules</b> preference is not set, don't forget to enable it for rules to take effect.
227
    </div>
228
[% END %]
229
[% IF removeConfirm %]
230
<div class="dialog alert">
231
<h3>Remove rule?</h3>
232
<p>Are you sure you want to remove the selected rule(s)?</p>
233
234
<form action="[% script_name %]" method="GET">
235
    <input type="submit" value="No, do not remove" class="deny"/>
236
</form>
237
<input type="button" value="Yes, remove" class="approve" id="doremove" />
238
</div>
239
[% END %]
240
241
<form action="[% script_name %]" method="POST" id="marc-merge-rules-form">
242
<table id="marc-merge-rules">
243
    <thead><tr>
244
        <th>Rule</th>
245
        <th>Module</th>
246
        <th>Filter</th>
247
        <th>Tag</th>
248
        <th>Preset</th>
249
        <th>Added</th>
250
        <th>Appended</th>
251
        <th>Removed</th>
252
        <th>Deleted</th>
253
        <th>Actions</th>
254
        <th>&nbsp;</th>
255
    </tr></thead>
256
    [% UNLESS edit %]
257
    <tfoot>
258
        <tr class="rule-new">
259
            <th>&nbsp;</th>
260
            <th>
261
                <select name="module">
262
                    [% FOREACH module IN modules %]
263
                        <option value="[% module.id %]">[% module.name %]</option>
264
                    [% END %]
265
                </select>
266
            </th>
267
            <th><input type="text" size="5" name="filter"/></th>
268
            <th><input type="text" size="5" name="tag"/></th>
269
            <th>
270
                <select name="preset">
271
                    <option value="" selected>Custom</option>
272
                    [% FOR preset IN ['Protect', 'Overwrite', 'Protect existing', 'Add only', 'Protect from delete'] %]
273
                        <option value="[% preset %]">[% preset %]</option>
274
                    [% END %]
275
                </select>
276
            </th>
277
            <th class="rule-operation-action-edit">
278
                <select name="add">
279
                    <option value="0">Skip</option>
280
                    <option value="1">Add</option>
281
                </select>
282
            </th>
283
            <th class="rule-operation-action-edit">
284
                <select name="append">
285
                    <option value="0">Skip</option>
286
                    <option value="1">Append</option>
287
                </select>
288
            </th>
289
            <th class="rule-operation-action-edit">
290
                <select name="remove">
291
                    <option value="0">Skip</option>
292
                    <option value="1">Remove</option>
293
                </select>
294
            </th>
295
            <th class="rule-operation-action-edit">
296
                <select name="delete">
297
                    <option value="0">Skip</option>
298
                    <option value="1">Delete</option>
299
                </select>
300
            </th>
301
            <th><button class="btn btn-small" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
302
            <th><button id="btn_batchremove" disabled="disabled" class="btn btn-small" title="Batch remove"><i class="fa fa-trash"></i> Delete selected</button></th>
303
        </tr>
304
    </tfoot>
305
    [% END %]
306
    <tbody>
307
        [% FOREACH rule IN rules %]
308
            <tr id="[% rule.id %]" class="rule[% IF rule.edit %]-edit[% END %]">
309
            [% IF rule.edit %]
310
                <td>[% rule.id %]</td>
311
                <td>
312
                    <select name="module">
313
                        [% FOREACH module IN modules %]
314
                            [% IF module.name == rule.module %]
315
                                <option value="[% module.id %]" selected="selected">[% module.name %]</option>
316
                            [% ELSE %]
317
                                <option value="[% module.id %]">[% module.name %]</option>
318
                            [% END %]
319
                        [% END %]
320
                    </select>
321
                </td>
322
                <td><input type="text" size="5" name="filter" value="[% rule.filter %]"/></td>
323
                <td><input type="text" size="3" name="tag" value="[% rule.tag %]"/></td>
324
                <th>
325
                    <select name="preset">
326
                        <option value="" selected>Custom</option>
327
                        [% FOR preset IN ['Protect', 'Overwrite', 'Protect existing', 'Add only', 'Protect from delete'] %]
328
                            <option value="[% preset %]">[% preset %]</option>
329
                        [% END %]
330
                    </select>
331
                </th>
332
                <td class="rule-operation-action-edit">
333
                    <select name="add">
334
                        <option value="0"[% IF !rule.add %] selected="selected"[% END %]>Skip</option>
335
                        <option value="1"[% IF rule.add %] selected="selected"[% END %]>Add</option>
336
                    </select>
337
                </td>
338
                <td class="rule-operation-action-edit">
339
                    <select name="append">
340
                        <option value="0"[% IF !rule.append %] selected="selected"[% END %]>Skip</option>
341
                        <option value="1"[% IF rule.append %] selected="selected"[% END %]>Append</option>
342
                    </select>
343
                </td>
344
                <td class="rule-operation-action-edit">
345
                    <select name="remove">
346
                        <option value="0"[% IF !rule.remove %] selected="selected"[% END %]>Skip</option>
347
                        <option value="1"[% IF rule.remove %] selected="selected"[% END %]>Remove</option>
348
                    </select>
349
                </td>
350
                <td class="rule-operation-action-edit">
351
                    <select name="delete">
352
                        <option value="0"[% IF !rule.delete %] selected="selected"[% END %]>Skip</option>
353
                        <option value="1"[% IF rule.delete %] selected="selected"[% END %]>Delete</option>
354
                    </select>
355
                </td>
356
                <td class="actions">
357
                    <button class="btn btn-mini" title="Save" id="doedit" value="[% rule.id %]"><i class="fa fa-check"></i> Save</button>
358
                    <a href="?"><button class="btn btn-mini" title="Cancel" ><i class="fa fa-times"></i> Cancel</button></a>
359
                </td>
360
                <td></td>
361
            [% ELSE %]
362
                <td>[% rule.id %]</td>
363
                <td>[% rule.module %]</td>
364
                <td>[% rule.filter %]</td>
365
                <td>[% rule.tag %]</td>
366
                <td class="rule-preset"></td>
367
                <td class="rule-operation-action" data-operation="add">[% IF rule.add %]Add[% ELSE %]Skip[% END %]</td>
368
                <td class="rule-operation-action" data-operation="append">[% IF rule.append %]Append[% ELSE %]Skip[% END %]</td>
369
                <td class="rule-operation-action" data-operation="remove">[% IF rule.remove %]Remove[% ELSE %]Skip[% END %]</td>
370
                <td class="rule-operation-action" data-operation="delete">[% IF rule.delete %]Delete[% ELSE %]Skip[% END %]</td>
371
                <td class="actions">
372
                    <a href="?op=remove&id=[% rule.id %]" title="Delete" class="btn btn-mini"><i class="fa fa-trash"></i> Delete</a>
373
                    <a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-mini"><i class="fa fa-pencil"></i> Edit</a>
374
                </td>
375
                <td>
376
                    <input type="checkbox" name="batchremove" value="[% rule.id %]"[% IF rule.removemarked %] checked="checked"[% END %]/>
377
                </td>
378
            [% END %]
379
            </tr>
380
        [% END %]
381
    </tbody>
382
</table>
383
</form>
384
385
<form action="[% script_name %]" method="post">
386
<input type="hidden" name="op" value="redo-matching" />
387
</form>
388
389
</div>
390
</div>
391
<div class="yui-b">
392
[% INCLUDE 'admin-menu.inc' %]
393
</div>
394
</div>
395
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref (+7 lines)
Lines 291-293 Cataloging: Link Here
291
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
291
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
292
            - "<br/>"
292
            - "<br/>"
293
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
293
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
294
        -
295
            - When importing records
296
            - pref: MARCMergeRules
297
              choices:
298
                  yes: "use"
299
                  no: "don't use"
300
            - 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 611-616 function Changefwk() { Link Here
611
[% END %]
611
[% END %]
612
        <input type="hidden" name="op" value="addbiblio" />
612
        <input type="hidden" name="op" value="addbiblio" />
613
        <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
613
        <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
614
        <input type="hidden" name="z3950" value="[% z3950 | html %]" />
614
        <input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
615
        <input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
615
        <input type="hidden" name="breedingid" value="[% breedingid | html %]" />
616
        <input type="hidden" name="breedingid" value="[% breedingid | html %]" />
616
        <input type="hidden" name="changed_framework" value="" />
617
        <input type="hidden" name="changed_framework" value="" />
(-)a/misc/migration_tools/bulkmarcimport.pl (-1 / +1 lines)
Lines 457-463 RECORD: while ( ) { Link Here
457
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
457
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
458
                };
458
                };
459
                if ($update) {
459
                if ($update) {
460
                    eval { ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber) ) };
460
                    eval { ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber), {context => {source => 'bulkmarcimport'}} ) };
461
                    if ($@) {
461
                    if ($@) {
462
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
462
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
463
                        printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);
463
                        printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);
(-)a/misc/migration_tools/import_lexile.pl (-1 / +2 lines)
Lines 153-158 while ( my $row = $csv->getline_hr($fh) ) { Link Here
153
    foreach my $biblionumber (@biblionumbers) {
153
    foreach my $biblionumber (@biblionumbers) {
154
        $counter++;
154
        $counter++;
155
        my $record = GetMarcBiblio({ biblionumber => $biblionumber });
155
        my $record = GetMarcBiblio({ biblionumber => $biblionumber });
156
        my $frameworkcode = GetFrameworkCode($biblionumber);
156
157
157
        if ($verbose) {
158
        if ($verbose) {
158
            say "Found matching record! Biblionumber: $biblionumber";
159
            say "Found matching record! Biblionumber: $biblionumber";
Lines 202-208 while ( my $row = $csv->getline_hr($fh) ) { Link Here
202
            $record->append_fields($field);
203
            $record->append_fields($field);
203
        }
204
        }
204
205
205
        ModBiblio( $record, $biblionumber ) unless ( $test );
206
        ModBiblio( $record, $biblionumber, $frameworkcode, {context => {source => 'import_lexile'}} ) unless ( $test );
206
    }
207
    }
207
208
208
}
209
}
(-)a/t/db_dependent/Biblio/MarcMergeRules.t (+717 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 => 22;
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]->id,
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]->id,
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
$schema->storage->txn_rollback;
716
717
1;
(-)a/tools/batch_record_modification.pl (-4 / +10 lines)
Lines 169-175 if ( $op eq 'form' ) { Link Here
169
    my ( $job );
169
    my ( $job );
170
    if ( $runinbackground ) {
170
    if ( $runinbackground ) {
171
        my $job_size = scalar( @record_ids );
171
        my $job_size = scalar( @record_ids );
172
        $job = C4::BackgroundJob->new( $sessionID, "FIXME", '/cgi-bin/koha/tools/batch_record_modification.pl', $job_size );
172
        $job = C4::BackgroundJob->new( $sessionID, "FIXME", $ENV{SCRIPT_NAME}, $job_size );
173
        my $job_id = $job->id;
173
        my $job_id = $job->id;
174
        if (my $pid = fork) {
174
        if (my $pid = fork) {
175
            $dbh->{InactiveDestroy}  = 1;
175
            $dbh->{InactiveDestroy}  = 1;
Lines 181-187 if ( $op eq 'form' ) { Link Here
181
        } elsif (defined $pid) {
181
        } elsif (defined $pid) {
182
            close STDOUT;
182
            close STDOUT;
183
        } else {
183
        } else {
184
            warn "fork failed while attempting to run tools/batch_record_modification.pl as a background job";
184
            warn "fork failed while attempting to run $ENV{'SCRIPT_NAME'} as a background job";
185
            exit 0;
185
            exit 0;
186
        }
186
        }
187
    }
187
    }
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
                        category => $member->{'category_type'},
213
                        borrower => $loggedinuser
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