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 108-117 use Koha::ItemTypes; Link Here
108
use Koha::SearchEngine;
109
use Koha::SearchEngine;
109
use Koha::Libraries;
110
use Koha::Libraries;
110
use Koha::Util::MARC;
111
use Koha::Util::MARC;
112
use Koha::MarcMergeRules;
111
113
112
use vars qw($debug $cgi_debug);
114
use vars qw($debug $cgi_debug);
113
115
114
115
=head1 NAME
116
=head1 NAME
116
117
117
C4::Biblio - cataloging management functions
118
C4::Biblio - cataloging management functions
Lines 241-247 sub AddBiblio { Link Here
241
242
242
=head2 ModBiblio
243
=head2 ModBiblio
243
244
244
  ModBiblio( $record,$biblionumber,$frameworkcode);
245
  ModBiblio($record, $biblionumber, $frameworkcode, $options);
245
246
246
Replace an existing bib record identified by C<$biblionumber>
247
Replace an existing bib record identified by C<$biblionumber>
247
with one supplied by the MARC::Record object C<$record>.  The embedded
248
with one supplied by the MARC::Record object C<$record>.  The embedded
Lines 262-268 Returns 1 on success 0 on failure Link Here
262
=cut
263
=cut
263
264
264
sub ModBiblio {
265
sub ModBiblio {
265
    my ( $record, $biblionumber, $frameworkcode ) = @_;
266
    my ( $record, $biblionumber, $frameworkcode, $options ) = @_;
267
    $options //= {};
268
266
    if (!$record) {
269
    if (!$record) {
267
        carp 'No record passed to ModBiblio';
270
        carp 'No record passed to ModBiblio';
268
        return 0;
271
        return 0;
Lines 294-299 sub ModBiblio { Link Here
294
297
295
    _strip_item_fields($record, $frameworkcode);
298
    _strip_item_fields($record, $frameworkcode);
296
299
300
    # apply merge rules
301
    if (C4::Context->preference('MARCMergeRules') && $biblionumber && defined $options && exists $options->{'context'}) {
302
        $record = ApplyMarcMergeRules({
303
                biblionumber => $biblionumber,
304
                record => $record,
305
                context => $options->{'context'},
306
            }
307
        );
308
    }
309
297
    # update biblionumber and biblioitemnumber in MARC
310
    # update biblionumber and biblioitemnumber in MARC
298
    # FIXME - this is assuming a 1 to 1 relationship between
311
    # FIXME - this is assuming a 1 to 1 relationship between
299
    # biblios and biblioitems
312
    # biblios and biblioitems
Lines 304-316 sub ModBiblio { Link Here
304
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
317
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
305
318
306
    # load the koha-table data object
319
    # load the koha-table data object
307
    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode );
320
    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode, undef, $biblionumber);
308
321
309
    # update MARC subfield that stores biblioitems.cn_sort
322
    # update MARC subfield that stores biblioitems.cn_sort
310
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
323
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
311
324
312
    # update the MARC record (that now contains biblio and items) with the new record data
325
    # update the MARC record (that now contains biblio and items) with the new record data
313
    &ModBiblioMarc( $record, $biblionumber, $frameworkcode );
326
    ModBiblioMarc( $record, $biblionumber, $frameworkcode );
314
327
315
    # modify the other koha tables
328
    # modify the other koha tables
316
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
329
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
Lines 2542-2548 sub TransformHtmlToMarc { Link Here
2542
2555
2543
=head2 TransformMarcToKoha
2556
=head2 TransformMarcToKoha
2544
2557
2545
    $result = TransformMarcToKoha( $record, undef, $limit )
2558
    $result = TransformMarcToKoha($record, undef, $biblionumber, $option)
2546
2559
2547
Extract data from a MARC bib record into a hashref representing
2560
Extract data from a MARC bib record into a hashref representing
2548
Koha biblio, biblioitems, and items fields.
2561
Koha biblio, biblioitems, and items fields.
Lines 2553-2559 hash_ref. Link Here
2553
=cut
2566
=cut
2554
2567
2555
sub TransformMarcToKoha {
2568
sub TransformMarcToKoha {
2556
    my ( $record, $frameworkcode, $limit_table ) = @_;
2569
    my ( $record, $frameworkcode, $limit_table, $biblionumber ) = @_;
2557
    # FIXME  Parameter $frameworkcode is obsolete and will be removed
2570
    # FIXME  Parameter $frameworkcode is obsolete and will be removed
2558
    $limit_table //= q{};
2571
    $limit_table //= q{};
2559
2572
Lines 3261-3267 sub _koha_delete_biblio_metadata { Link Here
3261
3274
3262
=head2 ModBiblioMarc
3275
=head2 ModBiblioMarc
3263
3276
3264
  &ModBiblioMarc($newrec,$biblionumber,$frameworkcode);
3277
  &ModBiblioMarc($newrec, $biblionumber, $frameworkcode, $options);
3265
3278
3266
Add MARC XML data for a biblio to koha
3279
Add MARC XML data for a biblio to koha
3267
3280
Lines 3597-3604 sub RemoveAllNsb { Link Here
3597
    return $record;
3610
    return $record;
3598
}
3611
}
3599
3612
3600
1;
3613
=head2 ApplyMarcMergeRules
3614
3615
    my $record = ApplyMarcMergeRules($params)
3616
3617
Applies marc merge rules to a record.
3618
3619
C<$params> is expected to be a hashref with below keys defined.
3620
3621
=over 4
3622
3623
=item C<biblionumber>
3624
biblionumber of old record
3625
3626
=item C<record>
3627
Incoming record that will be merged with old record
3628
3629
=item C<context>
3630
hashref containing at least one module from marc_merge_rules_modules table
3631
with filter value on the form {module => filter, ...}. Three predefined
3632
context modules exists:
3633
3634
    * source
3635
    * category
3636
    * borrower
3637
3638
=back
3639
3640
Returns:
3601
3641
3642
=over 4
3643
3644
=item C<$record>
3645
3646
Merged MARC record based with merge rules for C<context> applied. If no old
3647
record for C<biblionumber> can be found, C<record> is returned unchanged.
3648
Default action when no matching context is found to return C<record> unchanged.
3649
If no rules are found for a certain field tag the default is to overwrite with
3650
fields with this field tag from C<record>.
3651
3652
=back
3653
3654
=cut
3655
3656
sub ApplyMarcMergeRules {
3657
    my ($params) = @_;
3658
    my $biblionumber = $params->{biblionumber};
3659
    my $incoming_record = $params->{record};
3660
3661
    if (!$biblionumber) {
3662
        carp 'ApplyMarcMergeRules called on undefined biblionumber';
3663
        return;
3664
    }
3665
    if (!$incoming_record) {
3666
        carp 'ApplyMarcMergeRules called on undefined record';
3667
        return;
3668
    }
3669
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3670
3671
    # Skip merge rules if called with no context
3672
    if ($old_record && defined $params->{context}) {
3673
        return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
3674
    }
3675
    return $incoming_record;
3676
}
3677
3678
1;
3602
3679
3603
__END__
3680
__END__
3604
3681
(-)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 832-838 if ( $op eq "addbiblio" ) { Link Here
832
    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
833
    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
833
        my $oldbibitemnum;
834
        my $oldbibitemnum;
834
        if ( $is_a_modif ) {
835
        if ( $is_a_modif ) {
835
            ModBiblio( $record, $biblionumber, $frameworkcode );
836
            my $member = Koha::Patrons->find($loggedinuser);
837
            ModBiblio( $record, $biblionumber, $frameworkcode, {
838
                    context => {
839
                        source => $z3950 ? 'z39.50' : 'intranet',
840
                        category => $member->{'category_type'},
841
                        borrower => $loggedinuser
842
                    }
843
                }
844
            );
836
        }
845
        }
837
        else {
846
        else {
838
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
847
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
Lines 925-930 elsif ( $op eq "delete" ) { Link Here
925
    $template->param(
934
    $template->param(
926
        biblionumberdata => $biblionumber,
935
        biblionumberdata => $biblionumber,
927
        op               => $op,
936
        op               => $op,
937
        z3950            => $z3950
928
    );
938
    );
929
    if ( $op eq "duplicate" ) {
939
    if ( $op eq "duplicate" ) {
930
        $biblionumber = "";
940
        $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_didyouean', 'Manage Did you mean? configuration'),
29
   ( 3, 'manage_didyouean', '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 (+1 lines)
Lines 53-58 Link Here
53
    [%- CASE 'manage_oai_sets' -%]<span>Manage OAI sets</span>
53
    [%- CASE 'manage_oai_sets' -%]<span>Manage OAI sets</span>
54
    [%- CASE 'manage_item_search_fields' -%]<span>Manage item search fields</span>
54
    [%- CASE 'manage_item_search_fields' -%]<span>Manage item search fields</span>
55
    [%- CASE 'manage_search_engine_config' -%]<span>Manage search engine configuration</span>
55
    [%- CASE 'manage_search_engine_config' -%]<span>Manage search engine configuration</span>
56
    [%- CASE 'manage_marc_merge_rules' -%]<span>Manage MARC merge rules configuration</span>
56
    [%- CASE 'manage_search_targets' -%]<span>Manage Z39.50 and SRU server configuration</span>
57
    [%- CASE 'manage_search_targets' -%]<span>Manage Z39.50 and SRU server configuration</span>
57
    [%- CASE 'manage_didyoumean' -%]<span>Manage Did you mean? configuration</span>
58
    [%- CASE 'manage_didyoumean' -%]<span>Manage Did you mean? configuration</span>
58
    [%- CASE 'manage_column_config' -%]<span>Manage column configuration</span>
59
    [%- CASE 'manage_column_config' -%]<span>Manage column configuration</span>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+2 lines)
Lines 131-136 Link Here
131
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
131
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
132
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
132
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
133
                    [% END %]
133
                    [% END %]
134
                    <dt><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></dt>
135
                    <dd>Managed MARC field merge rules</dd>
134
                </dl>
136
                </dl>
135
            [% END %]
137
            [% END %]
136
138
(-)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 561-566 function Changefwk() { Link Here
561
[% END %]
561
[% END %]
562
        <input type="hidden" name="op" value="addbiblio" />
562
        <input type="hidden" name="op" value="addbiblio" />
563
        <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
563
        <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode | html %]" />
564
        <input type="hidden" name="z3950" value="[% z3950 | html %]" />
564
        <input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
565
        <input type="hidden" name="biblionumber" value="[% biblionumber | html %]" />
565
        <input type="hidden" name="breedingid" value="[% breedingid | html %]" />
566
        <input type="hidden" name="breedingid" value="[% breedingid | html %]" />
566
        <input type="hidden" name="changed_framework" value="" />
567
        <input type="hidden" name="changed_framework" value="" />
(-)a/misc/migration_tools/bulkmarcimport.pl (-1 / +1 lines)
Lines 454-460 RECORD: while ( ) { Link Here
454
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
454
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
455
                };
455
                };
456
                if ($update) {
456
                if ($update) {
457
                    eval { ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber) ) };
457
                    eval { ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber), {context => {source => 'bulkmarcimport'}} ) };
458
                    if ($@) {
458
                    if ($@) {
459
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
459
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
460
                        printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);
460
                        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 159-165 if ( $op eq 'form' ) { Link Here
159
    my ( $job );
159
    my ( $job );
160
    if ( $runinbackground ) {
160
    if ( $runinbackground ) {
161
        my $job_size = scalar( @record_ids );
161
        my $job_size = scalar( @record_ids );
162
        $job = C4::BackgroundJob->new( $sessionID, "FIXME", '/cgi-bin/koha/tools/batch_record_modification.pl', $job_size );
162
        $job = C4::BackgroundJob->new( $sessionID, "FIXME", $ENV{SCRIPT_NAME}, $job_size );
163
        my $job_id = $job->id;
163
        my $job_id = $job->id;
164
        if (my $pid = fork) {
164
        if (my $pid = fork) {
165
            $dbh->{InactiveDestroy}  = 1;
165
            $dbh->{InactiveDestroy}  = 1;
Lines 171-177 if ( $op eq 'form' ) { Link Here
171
        } elsif (defined $pid) {
171
        } elsif (defined $pid) {
172
            close STDOUT;
172
            close STDOUT;
173
        } else {
173
        } else {
174
            warn "fork failed while attempting to run tools/batch_record_modification.pl as a background job";
174
            warn "fork failed while attempting to run $ENV{'SCRIPT_NAME'} as a background job";
175
            exit 0;
175
            exit 0;
176
        }
176
        }
177
    }
177
    }
Lines 195-201 if ( $op eq 'form' ) { Link Here
195
                my $record = GetMarcBiblio({ biblionumber => $biblionumber });
195
                my $record = GetMarcBiblio({ biblionumber => $biblionumber });
196
                ModifyRecordWithTemplate( $mmtid, $record );
196
                ModifyRecordWithTemplate( $mmtid, $record );
197
                my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
197
                my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
198
                ModBiblio( $record, $biblionumber, $frameworkcode );
198
                my ($member) = Koha::Patrons->find($loggedinuser);
199
                ModBiblio( $record, $biblionumber, $frameworkcode,
200
                    {
201
                        source => 'batchmod',
202
                        category => $member->{'category_type'},
203
                        borrower => $loggedinuser
204
                    }
205
                );
199
            };
206
            };
200
            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
207
            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
201
                push @messages, {
208
                push @messages, {
202
- 

Return to bug 14957