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::Holds; Link Here
107
use Koha::ItemTypes;
108
use Koha::ItemTypes;
108
use Koha::SearchEngine;
109
use Koha::SearchEngine;
109
use Koha::Libraries;
110
use Koha::Libraries;
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 2525-2531 sub TransformHtmlToMarc { Link Here
2525
2538
2526
=head2 TransformMarcToKoha
2539
=head2 TransformMarcToKoha
2527
2540
2528
    $result = TransformMarcToKoha( $record, undef, $limit )
2541
    $result = TransformMarcToKoha($record, undef, $biblionumber, $option)
2529
2542
2530
Extract data from a MARC bib record into a hashref representing
2543
Extract data from a MARC bib record into a hashref representing
2531
Koha biblio, biblioitems, and items fields.
2544
Koha biblio, biblioitems, and items fields.
Lines 2536-2542 hash_ref. Link Here
2536
=cut
2549
=cut
2537
2550
2538
sub TransformMarcToKoha {
2551
sub TransformMarcToKoha {
2539
    my ( $record, $frameworkcode, $limit_table ) = @_;
2552
    my ( $record, $frameworkcode, $limit_table, $biblionumber ) = @_;
2540
    # FIXME  Parameter $frameworkcode is obsolete and will be removed
2553
    # FIXME  Parameter $frameworkcode is obsolete and will be removed
2541
    $limit_table //= q{};
2554
    $limit_table //= q{};
2542
2555
Lines 3223-3229 sub _koha_delete_biblio_metadata { Link Here
3223
3236
3224
=head2 ModBiblioMarc
3237
=head2 ModBiblioMarc
3225
3238
3226
  &ModBiblioMarc($newrec,$biblionumber,$frameworkcode);
3239
  &ModBiblioMarc($newrec, $biblionumber, $frameworkcode, $options);
3227
3240
3228
Add MARC XML data for a biblio to koha
3241
Add MARC XML data for a biblio to koha
3229
3242
Lines 3550-3557 sub RemoveAllNsb { Link Here
3550
    return $record;
3563
    return $record;
3551
}
3564
}
3552
3565
3553
1;
3566
=head2 ApplyMarcMergeRules
3567
3568
    my $record = ApplyMarcMergeRules($params)
3569
3570
Applies marc merge rules to a record.
3571
3572
C<$params> is expected to be a hashref with below keys defined.
3573
3574
=over 4
3575
3576
=item C<biblionumber>
3577
biblionumber of old record
3578
3579
=item C<record>
3580
Incoming record that will be merged with old record
3581
3582
=item C<context>
3583
hashref containing at least one module from marc_merge_rules_modules table
3584
with filter value on the form {module => filter, ...}. Three predefined
3585
context modules exists:
3586
3587
    * source
3588
    * category
3589
    * borrower
3590
3591
=back
3592
3593
Returns:
3554
3594
3595
=over 4
3596
3597
=item C<$record>
3598
3599
Merged MARC record based with merge rules for C<context> applied. If no old
3600
record for C<biblionumber> can be found, C<record> is returned unchanged.
3601
Default action when no matching context is found to return C<record> unchanged.
3602
If no rules are found for a certain field tag the default is to overwrite with
3603
fields with this field tag from C<record>.
3604
3605
=back
3606
3607
=cut
3608
3609
sub ApplyMarcMergeRules {
3610
    my ($params) = @_;
3611
    my $biblionumber = $params->{biblionumber};
3612
    my $incoming_record = $params->{record};
3613
3614
    if (!$biblionumber) {
3615
        carp 'ApplyMarcMergeRules called on undefined biblionumber';
3616
        return;
3617
    }
3618
    if (!$incoming_record) {
3619
        carp 'ApplyMarcMergeRules called on undefined record';
3620
        return;
3621
    }
3622
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3623
3624
    # Skip merge rules if called with no context
3625
    if ($old_record && defined $params->{context}) {
3626
        return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
3627
    }
3628
    return $incoming_record;
3629
}
3630
3631
1;
3555
3632
3556
__END__
3633
__END__
3557
3634
(-)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 (+72 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
use Koha::Exceptions::MarcMergeRule;
20
use Try::Tiny;
21
use Scalar::Util qw(looks_like_number);
22
23
use parent qw(Koha::Object);
24
25
my $cache = Koha::Caches->get_instance();
26
27
=head1 NAME
28
29
Koha::MarcMergeRule - Koha SearchField Object class
30
31
=cut
32
33
sub set {
34
    my $self = shift @_;
35
    my ($rule_data) =  @_;
36
37
    if(exists $rule_data->{tag}) {
38
        if ($rule_data->{tag} ne '*') {
39
            eval { qr/$rule_data->{tag}/ };
40
            if ($@) {
41
                Koha::Exceptions::MarcMergeRule::InvalidTagRegExp->throw(
42
                    "Invalid tag regular expression"
43
                );
44
            }
45
        }
46
        # TODO: Regexp or '*' that match controlfield not currently detected
47
        if (
48
            looks_like_number($rule_data->{tag}) &&
49
            $rule_data->{tag} < 10 &&
50
            $rule_data->{append} &&
51
            !$rule_data->{remove}
52
        ) {
53
            Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions->throw(
54
                "Combination of allow append and skip remove not permitted for control fields"
55
            );
56
        }
57
    }
58
59
    $self->SUPER::set(@_);
60
}
61
62
sub store {
63
    my $self = shift @_;
64
    $cache->clear_from_cache('marc_merge_rules');
65
    $self->SUPER::store(@_);
66
}
67
68
sub _type {
69
    return 'MarcMergeRule';
70
}
71
72
1;
(-)a/Koha/MarcMergeRules.pm (+312 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 parent qw(Koha::Objects);
24
25
sub operations {
26
    return ('add', 'append', 'remove', 'delete');
27
}
28
29
my $cache = Koha::Caches->get_instance();
30
31
=head1 NAME
32
33
Koha::MarcMergeRules - Koha MarcMergeRules Object set class
34
35
=head1 API
36
37
=head2 Class Methods
38
39
=head3 context_rules
40
41
    my $rules = Koha::MarcMergeRules->context_rules($context);
42
43
Gets all MARC merge rules for the supplied C<$context> (hashref with { module => filter, ... } values).
44
45
=cut
46
47
sub context_rules {
48
    my ($self, $context) = @_;
49
50
    return unless %{$context};
51
52
    my $rules = $cache->get_from_cache('marc_merge_rules', { unsafe => 1 });
53
54
    if (!$rules) {
55
        $rules = {};
56
        my @rules_rows = $self->_resultset()->search(
57
            undef,
58
            {
59
                prefetch => 'module',
60
                order_by => { -desc => [qw/module.specificity me.id/] }
61
            }
62
        );
63
        foreach my $rule_row (@rules_rows) {
64
            my %rule = $rule_row->get_columns();
65
            my $operations = {};
66
67
            foreach my $operation ($self->operations) {
68
                $operations->{$operation} = { allow => $rule{$operation}, rule => $rule{id} };
69
            }
70
71
            # TODO: Remove unless check and validate on saving rules?
72
            if ($rule{tag} eq '*') {
73
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{'*'}) {
74
                    $rules->{$rule{module}}->{$rule{filter}}->{'*'} = $operations;
75
                }
76
            }
77
            elsif ($rule{tag} =~ /^(\d{3})$/) {
78
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}}) {
79
                    $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}} = $operations;
80
                }
81
            }
82
            else {
83
                my $regexps = ($rules->{$rule{module}}->{$rule{filter}}->{regexps} //= []);
84
                push @{$regexps}, [$rule{tag}, $operations];
85
            }
86
        }
87
        $cache->set_in_cache('marc_merge_rules', $rules);
88
    }
89
90
    my $context_rules = undef;
91
    foreach my $module_name (keys %{$context}) {
92
        if (
93
            exists $rules->{$module_name} &&
94
            exists $rules->{$module_name}->{$context->{$module_name}}
95
        ) {
96
            $context_rules = $rules->{$module_name}->{$context->{$module_name}};
97
            last;
98
        }
99
    }
100
    if (!$context_rules) {
101
        # No perms matching specific context conditions found, try wildcard value for each active context
102
        foreach my $module_name (keys %{$context}) {
103
            if (exists $rules->{$module_name}->{'*'}) {
104
                $context_rules = $rules->{$module_name}->{'*'};
105
                last;
106
            }
107
        }
108
    }
109
    return $context_rules;
110
}
111
112
=head3 merge_records
113
114
    my $merged_record = Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $context);
115
116
Merge C<$old_record> with C<$incoming_record> applying merge rules for C<$context>.
117
Returns merged record C<$merged_record>. C<$old_record>, C<$incoming_record> and
118
C<$merged_record> are all MARC::Record objects.
119
120
=cut
121
122
sub merge_records {
123
    my ($self, $old_record, $incoming_record, $context) = @_;
124
125
    my $rules = $self->context_rules($context);
126
127
    # Default when no rules found is to overwrite with incoming record
128
    return $incoming_record unless $rules;
129
130
    my $fields_by_tag = sub {
131
        my ($record) = @_;
132
        my $fields = {};
133
        foreach my $field ($record->fields()) {
134
            $fields->{$field->tag()} //= [];
135
            push @{$fields->{$field->tag()}}, $field;
136
        }
137
        return $fields;
138
    };
139
140
    my $hash_field_data = sub {
141
        my ($field) = @_;
142
        my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
143
        return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields());
144
    };
145
146
    my $diff_by_key = sub {
147
        my ($a, $b) = @_;
148
        my @removed;
149
        my @intersecting;
150
        my @added;
151
        my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
152
        foreach my $key (keys %keys_index) {
153
            if ($a->{$key} && $b->{$key}) {
154
                push @intersecting, $a->{$key};
155
            }
156
            elsif ($a->{$key}) {
157
                push @removed, $a->{$key};
158
            }
159
            else {
160
                push @added, $b->{$key};
161
            }
162
        }
163
        return (\@removed, \@intersecting, \@added);
164
    };
165
166
    my $tag_rules = $rules->{tags} // {};
167
    my $default_rule = $rules->{'*'} // {
168
        add => { allow => 1, 'rule' => 0},
169
        append => { allow => 1, 'rule' => 0},
170
        delete => { allow => 1, 'rule' => 0},
171
        remove => { allow => 1, 'rule' => 0},
172
    };
173
174
    # Precompile regexps
175
    my @regexp_rules = map { { regexp => qr/^$_->[0]$/, actions => $_->[1] } } @{$rules->{regexps} // []};
176
177
    my $get_matching_field_rule = sub {
178
        my ($tag) = @_;
179
        # Exact match takes precedence, then regexp, then wildcard/defaults
180
        return $tag_rules->{$tag} //
181
            %{(first { $tag =~ $_->{regexp} } @regexp_rules) // {}}{actions} //
182
            $default_rule;
183
    };
184
185
    my %merged_record_fields;
186
187
    my $current_fields = $fields_by_tag->($old_record);
188
    my $incoming_fields = $fields_by_tag->($incoming_record);
189
190
    # First we get all new incoming fields
191
    my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
192
    foreach my $tag (@new_field_tags) {
193
        my $rule = $get_matching_field_rule->($tag);
194
        if ($rule->{add}->{allow}) {
195
            $merged_record_fields{$tag} //= [];
196
            push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
197
        }
198
    }
199
200
    # Then we get all fields no longer present in incoming fields
201
    my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
202
    foreach my $tag (@deleted_field_tags) {
203
        my $rule = $get_matching_field_rule->($tag);
204
        if (!$rule->{delete}->{allow}) {
205
            $merged_record_fields{$tag} //= [];
206
            push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
207
        }
208
    }
209
210
    # Then we get the intersection of fields, present both in
211
    # current and incoming record (possibly to be overwritten)
212
    my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
213
    foreach my $tag (@common_field_tags) {
214
        my $rule = $get_matching_field_rule->($tag);
215
216
        # Special handling for control fields
217
        if ($tag < 10) {
218
            if (
219
                $rule->{append}->{allow} &&
220
                !$rule->{remove}->{allow}
221
            ) {
222
                # This should be highly unlikely since we have input validation to protect against this case
223
                carp "Allowing \"append\" and skipping \"remove\" is not permitted for control fields, falling back to skipping both \"append\" and \"remove\"";
224
                push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
225
            }
226
            elsif ($rule->{append}->{allow}) {
227
                push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
228
            }
229
            else {
230
                push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
231
            }
232
        }
233
        else {
234
            # Compute intersection and diff using field data
235
            my $sort_weight = 0;
236
            my %current_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$current_fields->{$tag}};
237
238
            # Always put incoming fields after current fields
239
            my %incoming_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$incoming_fields->{$tag}};
240
241
            my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
242
243
            my @merged_fields;
244
245
            # First add common fields (intersection)
246
            # Unchanged
247
            if (@{$common_fields}) {
248
                push @merged_fields, @{$common_fields};
249
            }
250
            # Removed
251
            if (@{$current_fields_only}) {
252
                if (!$rule->{remove}->{allow}) {
253
                    push @merged_fields, @{$current_fields_only};
254
                }
255
            }
256
            # Appended
257
            if (@{$incoming_fields_only}) {
258
                if ($rule->{append}->{allow}) {
259
                    push @merged_fields, @{$incoming_fields_only};
260
                }
261
            }
262
            $merged_record_fields{$tag} //= [];
263
264
            # Sort ascending according to weight (original order)
265
            push @{$merged_record_fields{$tag}}, map { $_->[1] } sort { $a->[0] <=> $b->[0] } @merged_fields;
266
        }
267
    }
268
269
    my $merged_record = MARC::Record->new();
270
271
    # Leader is always overwritten, or kept???
272
    $merged_record->leader($incoming_record->leader());
273
274
    if (%merged_record_fields) {
275
        foreach my $tag (sort keys %merged_record_fields) {
276
            $merged_record->append_fields(@{$merged_record_fields{$tag}});
277
        }
278
    }
279
    return $merged_record;
280
}
281
282
sub _clear_caches {
283
    $cache->clear_from_cache('marc_merge_rules');
284
}
285
286
sub find_or_create {
287
    my $self = shift @_;
288
    $self->_clear_caches();
289
    $self->SUPER::find_or_create(@_);
290
}
291
292
sub update {
293
    my $self = shift @_;
294
    $self->_clear_caches();
295
    return $self->SUPER::update(@_);
296
}
297
298
sub delete {
299
    my $self = shift @_;
300
    $self->_clear_caches();
301
    return $self->SUPER::delete(@_);
302
}
303
304
sub _type {
305
    return 'MarcMergeRule';
306
}
307
308
sub object_class {
309
    return 'Koha::MarcMergeRule';
310
}
311
312
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 (+157 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 => 'parameters_remaining_permissions' },
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
        my $rule = Koha::MarcMergeRules->find_or_create($rule_data);
122
        try {
123
            $rule->set($rule_data);
124
            $rule->store();
125
        }
126
        catch {
127
            die $_ unless blessed $_ && $_->can('rethrow');
128
129
            if ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp')) {
130
                push @{$errors}, {
131
                    type => 'error',
132
                    code => 'invalid_tag_regexp',
133
                    tag => $rule_data->{tag},
134
                };
135
            }
136
            elsif ($_->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions')) {
137
                push @{$errors}, {
138
                    type => 'error',
139
                    code => 'invalid_control_field_actions',
140
                    tag => $rule_data->{tag},
141
                };
142
            }
143
            else {
144
                $_->rethrow;
145
            }
146
        };
147
        $rules = $get_rules->();
148
    }
149
}
150
else {
151
    $rules = $get_rules->();
152
}
153
154
my $modules = Koha::MarcMergeRulesModules->as_list;
155
$template->param( rules => $rules, modules => $modules, messages => $errors );
156
157
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.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(255) 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(255) 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/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc (+1 lines)
Lines 40-45 Link Here
40
    [% IF Koha.Preference('SearchEngine') == 'Elasticsearch' %]
40
    [% IF Koha.Preference('SearchEngine') == 'Elasticsearch' %]
41
        <li><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></li>
41
        <li><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></li>
42
    [% END %]
42
    [% END %]
43
    <li><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></li>
43
</ul>
44
</ul>
44
45
45
[% IF ( CAN_user_acquisition_period_manage || CAN_user_acquisition_budget_manage || CAN_user_parameters || CAN_user_acquisition_edi_manage ) %]
46
[% IF ( CAN_user_acquisition_period_manage || CAN_user_acquisition_budget_manage || CAN_user_parameters || CAN_user_acquisition_edi_manage ) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+2 lines)
Lines 93-98 Link Here
93
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
93
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
94
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
94
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
95
                    [% END %]
95
                    [% END %]
96
                    <dt><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></dt>
97
                    <dd>Managed MARC field merge rules</dd>
96
                </dl>
98
                </dl>
97
99
98
                [% IF ( CAN_user_acquisition_currencies_manage || CAN_user_acquisition_period_manage || CAN_user_acquisition_budget_manage || CAN_user_acquisition_edi_manage ) %]
100
                [% IF ( CAN_user_acquisition_currencies_manage || CAN_user_acquisition_period_manage || CAN_user_acquisition_budget_manage || CAN_user_acquisition_edi_manage ) %]
(-)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 280-282 Cataloging: Link Here
280
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
280
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
281
            - "<br/>"
281
            - "<br/>"
282
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
282
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
283
        -
284
            - When importing records
285
            - pref: MARCMergeRules
286
              choices:
287
                  yes: "use"
288
                  no: "don't use"
289
            - 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 (+721 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
        $rule->set(
688
            {
689
                'tag' => '008',
690
                'append' => 1,
691
                'remove' => 0,
692
            }
693
        );
694
    }
695
    catch {
696
        return $_;
697
    };
698
    ok(defined $exception, "Exception was caught");
699
    ok($exception->isa('Koha::Exceptions::MarcMergeRule::InvalidControlFieldActions'), "Exception is of correct class");
700
};
701
702
subtest 'An exception is thrown when rule tag is set to invalid regexp' => sub {
703
    plan tests => 2;
704
705
    my $exception = try {
706
        $rule->set(
707
            {
708
                'tag' => '**'
709
            }
710
        );
711
    }
712
    catch {
713
        return $_;
714
    };
715
    ok(defined $exception, "Exception was caught");
716
    ok($exception->isa('Koha::Exceptions::MarcMergeRule::InvalidTagRegExp'), "Exception is of correct class");
717
};
718
719
$schema->storage->txn_rollback;
720
721
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