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

(-)a/C4/Biblio.pm (-550 / +34 lines)
Lines 100-110 BEGIN { Link Here
100
      &CountItemsIssued
100
      &CountItemsIssued
101
      &CountBiblioInOrders
101
      &CountBiblioInOrders
102
102
103
      &GetMarcPermissionsRules
103
      &GetMarcMergeRules
104
      &GetMarcPermissionsModules
104
      &GetMarcMergeRulesModules
105
      &ModMarcPermissionsRule
105
      &ModMarcMergeRulesRule
106
      &AddMarcPermissionsRule
106
      &AddMarcMergeRulesRule
107
      &DelMarcPermissionsRule
107
      &DelMarcMergeRulesRule
108
    );
108
    );
109
109
110
    # To modify something
110
    # To modify something
Lines 132-138 BEGIN { Link Here
132
    # they are useful in a few circumstances, so they are exported,
132
    # they are useful in a few circumstances, so they are exported,
133
    # but don't use them unless you are a core developer ;-)
133
    # but don't use them unless you are a core developer ;-)
134
    push @EXPORT, qw(
134
    push @EXPORT, qw(
135
      &ApplyMarcPermissions
135
      &ApplyMarcMergeRules
136
      &ModBiblioMarc
136
      &ModBiblioMarc
137
    );
137
    );
138
138
Lines 321-332 sub ModBiblio { Link Here
321
321
322
    _strip_item_fields($record, $frameworkcode);
322
    _strip_item_fields($record, $frameworkcode);
323
323
324
    # apply permissions
324
    # apply merge rules
325
    if (C4::Context->preference('MARCPermissions') && $biblionumber && defined $options && exists $options->{'context'}) {
325
    if (C4::Context->preference('MARCMergeRules') && $biblionumber && defined $options && exists $options->{'context'}) {
326
        $record = ApplyMarcPermissions({
326
        $record = ApplyMarcMergeRules({
327
                biblionumber => $biblionumber,
327
                biblionumber => $biblionumber,
328
                record => $record,
328
                record => $record,
329
                filter => $options->{'context'},
329
                context => $options->{'context'},
330
            }
330
            }
331
        );
331
        );
332
    }
332
    }
Lines 3588-3600 sub RemoveAllNsb { Link Here
3588
    return $record;
3588
    return $record;
3589
}
3589
}
3590
3590
3591
=head2 ApplyMarcPermissions
3591
=head2 ApplyMarcMergeRules
3592
3592
3593
    my $record = ApplyMarcPermissions($arguments)
3593
    my $record = ApplyMarcMergeRules($params)
3594
3594
3595
Applies marc permission rules to a record.
3595
Applies marc merge rules to a record.
3596
3596
3597
C<$arguments> is expected to be a hashref with below keys defined.
3597
C<$params> is expected to be a hashref with below keys defined.
3598
3598
3599
=over 4
3599
=over 4
3600
3600
Lines 3602-3627 C<$arguments> is expected to be a hashref with below keys defined. Link Here
3602
biblionumber of old record
3602
biblionumber of old record
3603
3603
3604
=item C<record>
3604
=item C<record>
3605
record that will modify old record
3605
Incoming record that will be merged with old record
3606
3606
3607
=item C<frameworkcode>
3607
=item C<context>
3608
only tags included in framework will be processed
3608
hashref containing at least one module from marc_merge_rules_modules table
3609
3609
with filter value on the form {module => filter, ...}. Three predefined
3610
=item C<filter>
3610
context modules exists:
3611
hashref containing at least one filter module from the marc_permissions_modules
3612
table in form {module => filter}. Three predefined filter modules exists:
3613
3611
3614
    * source
3612
    * source
3615
    * category
3613
    * category
3616
    * borrower
3614
    * borrower
3617
3615
3618
=item C<log>
3619
optional reference to array that will be filled with rule evaluation log
3620
entries.
3621
3622
=item C<nolog>
3623
optional boolean which when true disables logging to action log.
3624
3625
=back
3616
=back
3626
3617
3627
Returns:
3618
Returns:
Lines 3630-4174 Returns: Link Here
3630
3621
3631
=item C<$record>
3622
=item C<$record>
3632
3623
3633
new MARC record based on C<record> with C<filter> applied. If no old
3624
Merged MARC record based with merge rules for C<context> applied. If no old
3634
record for C<biblionumber> can be found, C<record> is returned unchanged.
3625
record for C<biblionumber> can be found, C<record> is returned unchanged.
3635
Default action when no matching filter found is to leave old record unchanged.
3626
Default action when no matching context is found to return C<record> unchanged.
3627
If no rules are found for a certain field tag the default is to overwrite with
3628
fields with this field tag from C<record>.
3636
3629
3637
=back
3630
=back
3638
3631
3639
=cut
3632
=cut
3640
3633
3641
sub ApplyMarcPermissions {
3634
sub ApplyMarcMergeRules {
3642
    my ($arguments) = @_;
3635
    my ($params) = @_;
3643
    my $biblionumber = $arguments->{biblionumber};
3636
    my $biblionumber = $params->{biblionumber};
3644
    my $incoming_record = $arguments->{record};
3637
    my $incoming_record = $params->{record};
3645
3638
3646
    if ( !$biblionumber ) {
3639
    if (!$biblionumber) {
3647
        carp 'ApplyMarcPermissions called on undefined biblionumber';
3640
        carp 'ApplyMarcMergeRules called on undefined biblionumber';
3648
        return;
3641
        return;
3649
    }
3642
    }
3650
    if ( !$incoming_record ) {
3643
    if (!$incoming_record) {
3651
        carp 'ApplyMarcPermissions called on undefined record';
3644
        carp 'ApplyMarcMergeRules called on undefined record';
3652
        return;
3645
        return;
3653
    }
3646
    }
3654
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3647
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3655
3648
3656
    my $merge_rules = undef;
3649
    # Skip merge rules if called with no context
3657
    if ($old_record && $arguments->{filter} && ($merge_rules = GetMarcPermissions($arguments->{filter}))) {
3650
    if ($old_record && defined $params->{context}) {
3658
        return MergeRecords($old_record, $incoming_record, $merge_rules);
3651
        return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
3659
    }
3652
    }
3660
    return $incoming_record;
3653
    return $incoming_record;
3661
}
3654
}
3662
3655
3663
sub MergeRecords {
3664
    my ($old_record, $incoming_record, $merge_rules) = @_;
3665
    my $is_matching_regex = sub {
3666
        my ( $tag, $m ) = @_;
3667
3668
        # tag is not exactly same as possible regex
3669
        $tag ne $m &&
3670
3671
        # wildcard
3672
        $m ne '*' &&
3673
3674
        # valid tagDataType
3675
        $m !~ /^(0[1-9A-z][\dA-Z]) |
3676
        ([1-9A-z][\dA-z]{2})$/x &&
3677
3678
        # nor valid controltagDataType
3679
        $m !~ /00[1-9A-Za-z]{1}/ &&
3680
3681
        # so we try it as a regex
3682
        $tag =~ /^$m$/
3683
    };
3684
3685
    my $fields_by_tag = sub {
3686
        my ($record) = @_;
3687
        my $fields = {};
3688
        foreach my $field ($record->fields()) {
3689
            $fields->{$field->tag()} //= [];
3690
            push @{$fields->{$field->tag()}}, $field;
3691
        }
3692
        return $fields;
3693
    };
3694
3695
    my $hash_field_data = sub {
3696
        my ($field) = @_;
3697
        my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
3698
        return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields());
3699
    };
3700
3701
    my $diff_by_key = sub {
3702
        my ($a, $b) = @_;
3703
        my @removed;
3704
        my @intersecting;
3705
        my @added;
3706
        my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
3707
        foreach my $key (keys %keys_index) {
3708
            if ($a->{$key} && $b->{$key}) {
3709
                push @intersecting, $a->{$key};
3710
            }
3711
            elsif ($a->{$key}) {
3712
                push @removed, $a->{$key};
3713
            }
3714
            else {
3715
                push @added, $b->{$key};
3716
            }
3717
        }
3718
        return (\@removed, \@intersecting, \@added);
3719
    };
3720
3721
    my $get_matching_field_rule = sub {
3722
        my ($tag) = @_;
3723
        my $matched_rule = undef;
3724
        # Exact match takes precedence
3725
        if (exists $merge_rules->{$tag}) {
3726
            $matched_rule = $merge_rules->{$tag};
3727
        }
3728
        else {
3729
            # TODO: sorty by module/weight rule id or something, grab first matching via listutils thingy
3730
            my @matching_rules = map { $merge_rules->{$_} } grep { $is_matching_regex->($tag, $_) } sort keys %{$merge_rules};
3731
            # TODO: fix
3732
            if (@matching_rules) {
3733
                $matched_rule = pop @matching_rules;
3734
            }
3735
            elsif($merge_rules->{'*'}) {
3736
                $matched_rule = $merge_rules->{'*'};
3737
            }
3738
        }
3739
        return $matched_rule;
3740
    };
3741
3742
    my $merged_record = MARC::Record->new();
3743
    my @merged_record_fields;
3744
3745
    # Leader is always overwritten, or kept???
3746
    $merged_record->leader($incoming_record->leader());
3747
3748
    my $current_fields = $fields_by_tag->($old_record);
3749
    my $incoming_fields = $fields_by_tag->($incoming_record);
3750
3751
    # First we get all new incoming control fields
3752
    my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
3753
3754
    foreach my $tag (@new_field_tags) {
3755
        my $rule = $get_matching_field_rule->($tag) // {
3756
            on_new => {'action' => 'skip', 'rule' => 0}
3757
        };
3758
        if (
3759
            $rule->{on_new}->{action} eq 'add' ||
3760
            $rule->{on_new}->{action} eq 'overwrite' # ???
3761
        ) { # Or could just be write/protect?
3762
            # Hmm, only one control field possible??
3763
            push @merged_record_fields, @{$incoming_fields->{$tag}};
3764
        }
3765
    }
3766
3767
    # Then we get all control fields no longer present in incoming fields
3768
    # (removed)
3769
    my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
3770
    foreach my $tag (@deleted_field_tags) {
3771
        my $rule = $get_matching_field_rule->($tag) // {
3772
            on_deleted => {'action' => 'skip', 'rule' => 0}
3773
        };
3774
        if ($rule->{on_deleted}->{action} eq 'skip') {
3775
            push @merged_record_fields, @{$current_fields->{$tag}};
3776
        }
3777
    }
3778
3779
    # Then we get the intersection of control fields, present both in
3780
    # current and incoming record (possibly to be overwritten)
3781
    my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
3782
    foreach my $tag (@common_field_tags) {
3783
        # Is control field
3784
        my $rule = $get_matching_field_rule->($tag) // {
3785
            on_removed => {'action' => 'skip', 'rule' => 0},
3786
            on_appended => {'action' => 'skip', 'rule' => 0}
3787
        };
3788
        if ($tag < 10) {
3789
            # on_existing = on_match
3790
            if ($rule->{on_appended}->{action} eq 'skip') { # TODO: replace with "protect", "keep"
3791
                push @merged_record_fields, @{$current_fields->{$tag}};
3792
            }
3793
            elsif ($rule->{on_appended}->{action} eq 'append') {
3794
                push @merged_record_fields, @{$incoming_fields->{$tag}};
3795
            }
3796
            if (
3797
                $rule->{on_appended}->{action} eq 'append' &&
3798
                $rule->{on_removed}->{action} eq 'skip'
3799
            ) {
3800
                #TODO: This is an invalid combination for control fields, warn!!
3801
                # Or should perform client/server side validation to prevent this choice
3802
            }
3803
        }
3804
        else {
3805
            # Compute intersection and diff using field data
3806
            my %current_fields_by_data = map { $hash_field_data->($_) => $_ } @{$current_fields->{$tag}};
3807
            my %incoming_fields_by_data = map { $hash_field_data->($_) => $_ } @{$incoming_fields->{$tag}};
3808
            my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
3809
3810
            # First add common fields (intersection)
3811
            # Unchanged
3812
            if (@{$common_fields}) {
3813
                push @merged_record_fields, @{$common_fields};
3814
            }
3815
            # Removed
3816
            if (@{$current_fields_only}) {
3817
                if ($rule->{on_removed}->{action} eq 'skip') {
3818
                    push @merged_record_fields, @{$current_fields_only};
3819
                }
3820
            }
3821
            # Appended
3822
            if (@{$incoming_fields_only}) {
3823
                if ($rule->{on_appended}->{action} eq 'append') {
3824
                    push @merged_record_fields, @{$incoming_fields_only};
3825
                }
3826
            }
3827
        }
3828
    }
3829
    if ($#merged_record_fields != 0) {
3830
        $merged_record->insert_fields_ordered(@merged_record_fields);
3831
    }
3832
    return $merged_record;
3833
}
3834
3835
=head2 GetMarcPermissions
3836
3837
    my $marc_permissions = GetMarcPermissions()
3838
3839
Loads MARC field permissions from the marc_permissions table.
3840
3841
Returns:
3842
3843
=over 4
3844
3845
=item C<$marc_permissions>
3846
3847
hashref with permissions structure for use with GetMarcPermissionsAction.
3848
3849
=back
3850
3851
=cut
3852
3853
sub GetMarcPermissions {
3854
    my ($filter) = @_;
3855
    my $dbh = C4::Context->dbh;
3856
    my $rule_count = 0;
3857
    my $modules = GetMarcPermissionsModules();
3858
    # We only care about modules included in the context/filter
3859
    # TODO: Perhaps make sure source => '*' is default?
3860
    my @filter_modules = grep { exists $filter->{$_->{name}} } @{$modules};
3861
3862
    my $cache = Koha::Caches->get_instance();
3863
    my $permissions = $cache->get_from_cache('marc_permissions', { unsafe => 1 });
3864
3865
    if (!$permissions) {
3866
        my $query = '
3867
            SELECT `marc_permissions`.*,
3868
                `marc_permissions_modules`.`name`,
3869
                `marc_permissions_modules`.`description`,
3870
                `marc_permissions_modules`.`specificity`
3871
                FROM `marc_permissions`
3872
                LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id`
3873
                ORDER BY `marc_permissions_modules`.`specificity`, `id`
3874
        ';
3875
        my $sth = $dbh->prepare($query);
3876
        $sth->execute();
3877
        while (my $perm = $sth->fetchrow_hashref) {
3878
            my $target = ($permissions->{$perm->{name}}->{$perm->{filter}}->{$perm->{tagfield}} //= {});
3879
            foreach my $event (GetMarcPermissionEvents()) {
3880
                $target->{$event} = { action => $perm->{$event}, rule => $perm->{'id'} };
3881
            }
3882
        }
3883
        $cache->set_in_cache('marc_permissions', $permissions);
3884
    }
3885
3886
    my $filtered_permissions = undef;
3887
    foreach my $module (@filter_modules) {
3888
        if (
3889
            exists $permissions->{$module->{name}} &&
3890
            exists $permissions->{$module->{name}}->{$filter->{$module->{name}}}
3891
        ) {
3892
            # TODO: Support multiple overlapping filters/context??
3893
            $filtered_permissions = $permissions->{$module->{name}}->{$filter->{$module->{name}}};
3894
            last;
3895
        }
3896
    }
3897
    if (!$filtered_permissions) {
3898
        # No perms matching specific context conditions found, try wildcard value for each active context
3899
        foreach my $module (@filter_modules) {
3900
            if (exists $permissions->{$module->{name}}->{'*'}) {
3901
                $filtered_permissions = $permissions->{$module->{name}}->{'*'};
3902
                last;
3903
            }
3904
        }
3905
    }
3906
    return $filtered_permissions;
3907
}
3908
3909
# TODO: Use this in GetMarcPermissions
3910
=head2 GetMarcPermissionsRules
3911
3912
    my $rules = GetMarcPermissionsRules()
3913
3914
Returns:
3915
3916
=over 4
3917
3918
=item C<$rules>
3919
3920
array (in list context, arrayref otherwise) of hashrefs from marc_permissions
3921
table in order of module specificity and rule id.
3922
3923
=back
3924
3925
=cut
3926
3927
sub GetMarcPermissionsRules {
3928
    my $dbh = C4::Context->dbh;
3929
    my @rules = ();
3930
3931
    my $query = '
3932
    SELECT `marc_permissions`.`id`,
3933
           `marc_permissions`.`tagfield`,
3934
           `marc_permissions`.`filter`,
3935
           `marc_permissions`.`on_new`,
3936
           `marc_permissions`.`on_appended`,
3937
           `marc_permissions`.`on_removed`,
3938
           `marc_permissions`.`on_deleted`,
3939
           `marc_permissions_modules`.`name` as  `module`,
3940
           `marc_permissions_modules`.`description`,
3941
           `marc_permissions_modules`.`specificity`
3942
    FROM `marc_permissions`
3943
    LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id`
3944
    ORDER BY `marc_permissions_modules`.`specificity`, `id`
3945
    ';
3946
    my $sth = $dbh->prepare($query);
3947
    $sth->execute();
3948
    while ( my $row = $sth->fetchrow_hashref ) {
3949
        push(@rules, $row);
3950
    }
3951
3952
    return wantarray ? @rules : \@rules;
3953
}
3954
3955
=head2 GetMarcPermissionsModules
3956
3957
    my $modules = GetMarcPermissionsModules()
3958
3959
Returns:
3960
3961
=over 4
3962
3963
=item C<$modules>
3964
3965
array (in list context, arrayref otherwise) of hashrefs from
3966
marc_permissions_modules table in order of specificity.
3967
3968
=back
3969
3970
=cut
3971
3972
sub GetMarcPermissionsModules {
3973
    my $dbh = C4::Context->dbh;
3974
    my @modules = ();
3975
3976
    my $query = '
3977
    SELECT *
3978
    FROM `marc_permissions_modules`
3979
    ORDER BY `specificity` DESC
3980
    ';
3981
    my $sth = $dbh->prepare($query);
3982
    $sth->execute();
3983
    while ( my $row = $sth->fetchrow_hashref ) {
3984
        push(@modules, $row);
3985
    }
3986
3987
    return wantarray ? @modules : \@modules;
3988
}
3989
3990
sub GetMarcPermissionEvents {
3991
    return ('on_new', 'on_appended', 'on_removed', 'on_deleted');
3992
}
3993
3994
=head2 ModMarcPermissionsRule
3995
3996
    my $success = ModMarcPermissionsRule($id, $fields)
3997
3998
Modifies rule in the marc_permissions table.
3999
4000
=over 4
4001
4002
=item C<$id>
4003
4004
rule id to modify
4005
4006
=item C<$fields>
4007
4008
hashref defining the table fields
4009
4010
      * tagfield - required
4011
      * module - required
4012
      * filter - required
4013
      * on_new - required
4014
      * on_appended - required
4015
      * on_removed - required
4016
      * on_deleted - required
4017
4018
=back
4019
4020
Returns:
4021
4022
=over 4
4023
4024
=item C<$success>
4025
4026
undef if an error occurs, otherwise true.
4027
4028
=back
4029
4030
=cut
4031
4032
sub ModMarcPermissionsRule {
4033
    my ($id, $f) = @_;
4034
    my $dbh = C4::Context->dbh;
4035
4036
    my $query = '
4037
    UPDATE `marc_permissions`
4038
    SET
4039
      tagfield = ?,
4040
      module = ?,
4041
      filter = ?,
4042
      on_new = ?,
4043
      on_appended = ?,
4044
      on_removed = ?,
4045
      on_deleted = ?
4046
    WHERE
4047
      id = ?
4048
    ';
4049
    my $sth = $dbh->prepare($query);
4050
    my $result = $sth->execute (
4051
        $f->{tagfield},
4052
        $f->{module},
4053
        $f->{filter},
4054
        $f->{on_new},
4055
        $f->{on_appended},
4056
        $f->{on_removed},
4057
        $f->{on_deleted},
4058
        $id
4059
    );
4060
    ClearMarcPermissionsRulesCache();
4061
    return $result;
4062
}
4063
4064
sub ClearMarcPermissionsRulesCache {
4065
    my $cache = Koha::Caches->get_instance();
4066
    $cache->clear_from_cache('marc_permissions');
4067
}
4068
4069
=head2 AddMarcPermissionsRule
4070
4071
    my $success = AddMarcPermissionsRule($fields)
4072
4073
Add rule to the marc_permissions table.
4074
4075
=over 4
4076
4077
=item C<$fields>
4078
4079
hashref defining the table fields
4080
4081
      tagfield - required
4082
      module - required
4083
      filter - required
4084
      on_new - required
4085
      on_appended - required
4086
      on_removed - required
4087
      on_deleted - required
4088
4089
=back
4090
4091
Returns:
4092
4093
=over 4
4094
4095
=item C<$success>
4096
4097
undef if an error occurs, otherwise true.
4098
4099
=back
4100
4101
=cut
4102
4103
sub AddMarcPermissionsRule {
4104
    my $f = shift;
4105
    my $dbh = C4::Context->dbh;
4106
    my $query = '
4107
    INSERT INTO `marc_permissions`
4108
    (
4109
      tagfield,
4110
      module,
4111
      filter,
4112
      on_new,
4113
      on_appended,
4114
      on_removed,
4115
      on_deleted
4116
    )
4117
    VALUES (?, ?, ?, ?, ?, ?, ?)
4118
    ';
4119
    my $sth = $dbh->prepare($query);
4120
    my $result = $sth->execute (
4121
        $f->{tagfield},
4122
        $f->{module},
4123
        $f->{filter},
4124
        $f->{on_new},
4125
        $f->{on_appended},
4126
        $f->{on_removed},
4127
        $f->{on_deleted}
4128
    );
4129
    ClearMarcPermissionsRulesCache();
4130
    return $result;
4131
}
4132
4133
=head2 DelMarcPermissionsRule
4134
4135
    my $success = DelMarcPermissionsRule($id)
4136
4137
Deletes rule from the marc_permissions table.
4138
4139
=over 4
4140
4141
=item C<$id>
4142
4143
rule id to delete
4144
4145
=back
4146
4147
Returns:
4148
4149
=over 4
4150
4151
=item C<$success>
4152
4153
undef if an error occurs, otherwise true.
4154
4155
=back
4156
4157
=cut
4158
4159
sub DelMarcPermissionsRule {
4160
    my $id = shift;
4161
    my $dbh = C4::Context->dbh;
4162
    my $query = '
4163
    DELETE FROM `marc_permissions`
4164
    WHERE
4165
      id = ?
4166
    ';
4167
    my $sth = $dbh->prepare($query);
4168
    my $result = $sth->execute($id);
4169
    ClearMarcPermissionsRulesCache();
4170
    return $result;
4171
}
4172
1;
3656
1;
4173
3657
4174
__END__
3658
__END__
(-)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 (+291 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
22
use parent qw(Koha::Objects);
23
24
sub operations {
25
    return ('add', 'append', 'remove', 'delete');
26
}
27
28
my $cache = Koha::Caches->get_instance();
29
30
=head1 NAME
31
32
Koha::MarcMergeRules - Koha MarcMergeRules Object set class
33
34
=head1 API
35
36
=head2 Class Methods
37
38
=head3 context_rules
39
40
    my $rules = Koha::MarcMergeRules->context_rules($context);
41
42
Gets all MARC merge rules for the supplied C<$context> (hashref with { module => filter, ... } values).
43
44
=cut
45
46
sub context_rules {
47
    my ($self, $context) = @_;
48
49
    return unless %{$context};
50
51
    my $rules = $cache->get_from_cache('marc_merge_rules', { unsafe => 1 });
52
53
    if (!$rules || 1) {
54
        $rules = {};
55
        my @rules_rows = $self->_resultset()->search(
56
            undef,
57
            {
58
                prefetch => 'module',
59
                order_by => { -desc => [qw/module.specificity me.id/] }
60
            }
61
        );
62
        foreach my $rule_row (@rules_rows) {
63
            my %rule = $rule_row->get_columns();
64
            my $operations = {};
65
66
            foreach my $operation ($self->operations) {
67
                $operations->{$operation} = { allow => $rule{$operation}, rule => $rule{id} };
68
            }
69
70
            # TODO: Remove unless check and validate on saving rules?
71
            if ($rule{tag} eq '*') {
72
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{'*'}) {
73
                    $rules->{$rule{module}}->{$rule{filter}}->{'*'} = $operations;
74
                }
75
            }
76
            elsif ($rule{tag} =~ /^(\d{3})$/) {
77
                unless (exists $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}}) {
78
                    $rules->{$rule{module}}->{$rule{filter}}->{tags}->{$rule{tag}} = $operations;
79
                }
80
            }
81
            else {
82
                my $regexps = ($rules->{$rule{module}}->{$rule{filter}}->{regexps} //= []);
83
                push @{$regexps}, [$rule{tag}, $operations];
84
            }
85
        }
86
        $cache->set_in_cache('marc_merge_rules', $rules);
87
    }
88
89
    my $context_rules = undef;
90
    foreach my $module_name (keys %{$context}) {
91
        if (
92
            exists $rules->{$module_name} &&
93
            exists $rules->{$module_name}->{$context->{$module_name}}
94
        ) {
95
            $context_rules = $rules->{$module_name}->{$context->{$module_name}};
96
            last;
97
        }
98
    }
99
    if (!$context_rules) {
100
        # No perms matching specific context conditions found, try wildcard value for each active context
101
        foreach my $module_name (keys %{$context}) {
102
            if (exists $rules->{$module_name}->{'*'}) {
103
                $context_rules = $rules->{$module_name}->{'*'};
104
                last;
105
            }
106
        }
107
    }
108
    return $context_rules;
109
}
110
111
=head3 merge_records
112
113
    my $merged_record = Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $context);
114
115
Merge C<$old_record> with C<$incoming_record> applying merge rules for C<$context>.
116
Returns merged record C<$merged_record>. C<$old_record>, C<$incoming_record> and
117
C<$merged_record> are all MARC::Record objects.
118
119
=cut
120
121
sub merge_records {
122
    my ($self, $old_record, $incoming_record, $context) = @_;
123
124
    my $rules = $self->context_rules($context);
125
126
    # Default when no rules found is to overwrite with incoming record
127
    return $incoming_record unless $rules;
128
129
    my $fields_by_tag = sub {
130
        my ($record) = @_;
131
        my $fields = {};
132
        foreach my $field ($record->fields()) {
133
            $fields->{$field->tag()} //= [];
134
            push @{$fields->{$field->tag()}}, $field;
135
        }
136
        return $fields;
137
    };
138
139
    my $hash_field_data = sub {
140
        my ($field) = @_;
141
        my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
142
        return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields());
143
    };
144
145
    my $diff_by_key = sub {
146
        my ($a, $b) = @_;
147
        my @removed;
148
        my @intersecting;
149
        my @added;
150
        my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
151
        foreach my $key (keys %keys_index) {
152
            if ($a->{$key} && $b->{$key}) {
153
                push @intersecting, $a->{$key};
154
            }
155
            elsif ($a->{$key}) {
156
                push @removed, $a->{$key};
157
            }
158
            else {
159
                push @added, $b->{$key};
160
            }
161
        }
162
        return (\@removed, \@intersecting, \@added);
163
    };
164
165
    my $tag_rules = $rules->{tags} // {};
166
    my $default_rule = $rules->{'*'} // {
167
        add => { allow => 1, 'rule' => 0},
168
        append => { allow => 1, 'rule' => 0},
169
        delete => { allow => 1, 'rule' => 0},
170
        remove => { allow => 1, 'rule' => 0},
171
    };
172
173
    # Precompile regexps
174
    my @regexp_rules = map { { regexp => qr/^$_->[0]$/, actions => $_->[1] } } @{$rules->{regexps} // []};
175
176
    my $get_matching_field_rule = sub {
177
        my ($tag) = @_;
178
        # Exact match takes precedence, then regexp, then wildcard/defaults
179
        return $tag_rules->{$tag} //
180
            %{(first { $tag =~ $_->{regexp} } @regexp_rules) // {}}{actions} //
181
            $default_rule;
182
    };
183
184
    my %merged_record_fields;
185
186
    my $current_fields = $fields_by_tag->($old_record);
187
    my $incoming_fields = $fields_by_tag->($incoming_record);
188
189
    # First we get all new incoming fields
190
    my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
191
    foreach my $tag (@new_field_tags) {
192
        my $rule = $get_matching_field_rule->($tag);
193
        if ($rule->{add}->{allow}) {
194
            $merged_record_fields{$tag} //= [];
195
            push @{$merged_record_fields{$tag}}, @{$incoming_fields->{$tag}};
196
        }
197
    }
198
199
    # Then we get all fields no longer present in incoming fields
200
    my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
201
    foreach my $tag (@deleted_field_tags) {
202
        my $rule = $get_matching_field_rule->($tag);
203
        if (!$rule->{delete}->{allow}) {
204
            $merged_record_fields{$tag} //= [];
205
            push @{$merged_record_fields{$tag}}, @{$current_fields->{$tag}};
206
        }
207
    }
208
209
    # Then we get the intersection of control fields, present both in
210
    # current and incoming record (possibly to be overwritten)
211
    my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
212
    foreach my $tag (@common_field_tags) {
213
        my $rule = $get_matching_field_rule->($tag);
214
        # Compute intersection and diff using field data
215
        my $sort_weight = 0;
216
        my %current_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$current_fields->{$tag}};
217
218
        # Always put incoming fields after current fields
219
        my %incoming_fields_by_data = map { $hash_field_data->($_) => [$sort_weight++, $_] } @{$incoming_fields->{$tag}};
220
221
        my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
222
223
        my @merged_fields;
224
225
        # First add common fields (intersection)
226
        # Unchanged
227
        if (@{$common_fields}) {
228
            push @merged_fields, @{$common_fields};
229
        }
230
        # Removed
231
        if (@{$current_fields_only}) {
232
            if (!$rule->{remove}->{allow}) {
233
                push @merged_fields, @{$current_fields_only};
234
            }
235
        }
236
        # Appended
237
        if (@{$incoming_fields_only}) {
238
            if ($rule->{append}->{allow}) {
239
                push @merged_fields, @{$incoming_fields_only};
240
            }
241
        }
242
        $merged_record_fields{$tag} //= [];
243
244
        # Sort ascending according to weight (original order)
245
        push @{$merged_record_fields{$tag}}, map { $_->[1] } sort { $a->[0] <=> $b->[0] } @merged_fields;
246
    }
247
248
    my $merged_record = MARC::Record->new();
249
250
    # Leader is always overwritten, or kept???
251
    $merged_record->leader($incoming_record->leader());
252
253
    if (%merged_record_fields) {
254
        foreach my $tag (sort keys %merged_record_fields) {
255
            $merged_record->append_fields(@{$merged_record_fields{$tag}});
256
        }
257
    }
258
    return $merged_record;
259
}
260
261
sub _clear_caches {
262
    $cache->clear_from_cache('marc_merge_rules');
263
}
264
265
sub find_or_create {
266
    my $self = shift @_;
267
    $self->_clear_caches();
268
    $self->SUPER::find_or_create(@_);
269
}
270
271
sub update {
272
    my $self = shift @_;
273
    $self->_clear_caches();
274
    return $self->SUPER::update(@_);
275
}
276
277
sub delete {
278
    my $self = shift @_;
279
    $self->_clear_caches();
280
    return $self->SUPER::delete(@_);
281
}
282
283
sub _type {
284
    return 'MarcMergeRule';
285
}
286
287
sub object_class {
288
    return 'Koha::MarcMergeRule';
289
}
290
291
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 (+144 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
26
# Koha modules used
27
use C4::Context;
28
use C4::Koha;
29
use C4::Auth;
30
use C4::AuthoritiesMarc;
31
use C4::Output;
32
use C4::Biblio;
33
use C4::ImportBatch;
34
use C4::Matcher;
35
use C4::BackgroundJob;
36
use C4::Labels::Batch;
37
use Koha::MarcMergeRules;
38
use Koha::MarcMergeRule;
39
use Koha::MarcMergeRulesModules;
40
41
my $script_name = "/cgi-bin/koha/admin/marc-merge-rules.pl";
42
43
my $input = new CGI;
44
my $op = $input->param('op') || '';
45
my $errors = [];
46
47
my $rule_from_cgi = sub {
48
    my ($cgi) = @_;
49
50
    my %rule = map { $_ => scalar $cgi->param($_) } (
51
        'tag',
52
        'module',
53
        'filter',
54
        'add',
55
        'append',
56
        'remove',
57
        'delete'
58
    );
59
60
    my $id = $cgi->param('id');
61
    if ($id) {
62
        $rule{id} = $id;
63
    }
64
65
    return \%rule;
66
};
67
68
my ($template, $loggedinuser, $cookie) = get_template_and_user(
69
    {
70
        template_name   => "admin/marc-merge-rules.tt",
71
        query           => $input,
72
        type            => "intranet",
73
        authnotrequired => 0,
74
        flagsrequired   => { parameters => 'parameters_remaining_permissions' },
75
        debug           => 1,
76
    }
77
);
78
79
my %cookies = parse CGI::Cookie($cookie);
80
our $sessionID = $cookies{'CGISESSID'}->value;
81
82
my $get_rules = sub {
83
    # TODO: order?
84
    return [map { { $_->get_columns() } } Koha::MarcMergeRules->_resultset->all];
85
};
86
my $rules;
87
88
if ($op eq 'remove' || $op eq 'doremove') {
89
    my @remove_ids = $input->multi_param('batchremove');
90
    push @remove_ids, scalar $input->param('id') if $input->param('id');
91
    if ($op eq 'remove') {
92
        $template->{VARS}->{removeConfirm} = 1;
93
        my %remove_ids = map { $_ => undef } @remove_ids;
94
        $rules = $get_rules->();
95
        for my $rule (@{$rules}) {
96
            $rule->{'removemarked'} = 1 if exists $remove_ids{$rule->{id}};
97
        }
98
    }
99
    elsif ($op eq 'doremove') {
100
        my @remove_ids = $input->multi_param('batchremove');
101
        push @remove_ids, scalar $input->param('id') if $input->param('id');
102
        Koha::MarcMergeRules->search({ id => { in => \@remove_ids } })->delete();
103
        $rules = $get_rules->();
104
    }
105
}
106
elsif ($op eq 'edit') {
107
    $template->{VARS}->{edit} = 1;
108
    my $id = $input->param('id');
109
    $rules = $get_rules->();
110
    for my $rule(@{$rules}) {
111
        if ($rule->{id} == $id) {
112
            $rule->{'edit'} = 1;
113
            last;
114
        }
115
    }
116
}
117
elsif ($op eq 'doedit' || $op eq 'add') {
118
    my $rule_data = $rule_from_cgi->($input);
119
    if ($rule_data->{tag} ne '*') {
120
        eval { qr/$rule_data->{tag}/ };
121
        if ($@) {
122
            push @{$errors}, {
123
                type => 'error',
124
                code => 'invalid_tag_regexp',
125
                tag => $rule_data->{tag},
126
                message => $@
127
            };
128
        }
129
    }
130
    if (!@{$errors}) {
131
        my $rule = Koha::MarcMergeRules->find_or_create($rule_data);
132
        $rule->set($rule_data);
133
        $rule->store();
134
        $rules = $get_rules->();
135
    }
136
}
137
else {
138
    $rules = $get_rules->();
139
}
140
141
my $modules = Koha::MarcMergeRulesModules->as_list;
142
$template->param( rules => $rules, modules => $modules, messages => $errors );
143
144
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/admin/marc-permissions.pl (-115 lines)
Lines 1-115 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
26
# Koha modules used
27
use C4::Context;
28
use C4::Koha;
29
use C4::Auth;
30
use C4::AuthoritiesMarc;
31
use C4::Output;
32
use C4::Biblio;
33
use C4::ImportBatch;
34
use C4::Matcher;
35
use C4::BackgroundJob;
36
use C4::Labels::Batch;
37
38
my $script_name = "/cgi-bin/koha/admin/marc-permissions.pl";
39
40
my $input = new CGI;
41
my $op = $input->param('op') || '';
42
43
my $rule_from_cgi = sub {
44
    my ($cgi) = @_;
45
    my %rule = map { $_ => scalar $cgi->param($_) } (
46
        'tagfield',
47
        'module',
48
        'filter',
49
        'on_new',
50
        'on_appended',
51
        'on_removed',
52
        'on_deleted'
53
    );
54
    return \%rule;
55
};
56
57
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
58
    {
59
        template_name   => "admin/marc-permissions.tt",
60
        query           => $input,
61
        type            => "intranet",
62
        authnotrequired => 0,
63
        flagsrequired   => { parameters => 'parameters_remaining_permissions' },
64
        debug           => 1,
65
    }
66
);
67
68
my %cookies = parse CGI::Cookie($cookie);
69
our $sessionID = $cookies{'CGISESSID'}->value;
70
71
my $rules;
72
if ( $op eq "remove" ) {
73
    $template->{VARS}->{removeConfirm} = 1;
74
    my @removeIDs = $input->multi_param('batchremove');
75
    push( @removeIDs, scalar $input->param('id') ) if $input->param('id');
76
77
    $rules = GetMarcPermissionsRules();
78
    for my $removeID (@removeIDs) {
79
        map { $_->{'remove'} = 1 if $_->{'id'} == $removeID } @{$rules};
80
    }
81
}
82
elsif ( $op eq "doremove" ) {
83
    my @removeIDs = $input->multi_param('batchremove');
84
    push( @removeIDs, scalar $input->param('id') ) if $input->param('id');
85
    for my $removeID (@removeIDs) {
86
        DelMarcPermissionsRule($removeID);
87
    }
88
    $rules = GetMarcPermissionsRules();
89
}
90
elsif ( $op eq "edit" ) {
91
    $template->{VARS}->{edit} = 1;
92
    my $id = $input->param('id');
93
    $rules = GetMarcPermissionsRules();
94
    map { $_->{'edit'} = 1 if $_->{'id'} == $id } @{$rules};
95
96
}
97
elsif ( $op eq "doedit" ) {
98
    my $id = $input->param('id');
99
    my $rule = $rule_from_cgi->($input);
100
    ModMarcPermissionsRule($id, $rule);
101
    $rules = GetMarcPermissionsRules();
102
}
103
elsif ( $op eq "add" ) {
104
    my $rule = $rule_from_cgi->($input);
105
    AddMarcPermissionsRule($rule);
106
    $rules = GetMarcPermissionsRules();
107
108
}
109
else {
110
    $rules = GetMarcPermissionsRules();
111
}
112
my $modules = GetMarcPermissionsModules();
113
$template->param( rules => $rules, modules => $modules );
114
115
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/help.pl (-2 / +2 lines)
Lines 75-82 if ( $help_version =~ m|^(\d+)\.(\d{2}).*$| ) { Link Here
75
}
75
}
76
$template->param( helpVersion => $help_version );
76
$template->param( helpVersion => $help_version );
77
77
78
my $rules = GetMarcPermissionsRules();
78
my $rules = GetMarcMergeRules();
79
my $modules = GetMarcPermissionsModules();
79
my $modules = GetMarcMergeRulesModules();
80
$template->param( rules => $rules, modules => $modules );
80
$template->param( rules => $rules, modules => $modules );
81
81
82
output_html_with_http_headers $query, "", $template->output;
82
output_html_with_http_headers $query, "", $template->output;
(-)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-marc-permissions-syspref.sql (-2 lines)
Lines 1-2 Link Here
1
INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('MARCPermissions','0','','Use the MARC permissions system to decide what actions to take for each field when modifying records.','YesNo');
2
INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('MARCPermissionsLog','0','','Write MARC permissions rule evaluations to the system log when applying rules for MARC field modifications.','YesNo');
(-)a/installer/data/mysql/atomicupdate/bug_14957-marc-permissions.sql (-31 lines)
Lines 1-31 Link Here
1
-- DROP TABLE IF EXISTS `marc_permissions`;
2
DROP TABLE IF EXISTS `marc_permissions_modules`;
3
4
CREATE TABLE `marc_permissions_modules` (
5
    `id` int(11) NOT NULL auto_increment,
6
    `name` varchar(24) NOT NULL,
7
    `description` varchar(255),
8
    `specificity` int(11) NOT NULL DEFAULT 0, -- higher specificity will override rules with lower specificity
9
    PRIMARY KEY(`id`)
10
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
11
12
-- a couple of useful default filter modules
13
-- these are used in various scripts, so don't remove them if you don't know
14
-- what you're doing.
15
-- New filter modules can be added here when needed
16
INSERT INTO `marc_permissions_modules` VALUES(NULL, 'source', 'source from where modification request was sent', 0);
17
INSERT INTO `marc_permissions_modules` VALUES(NULL, 'category', 'categorycode of user who requested modification', 1);
18
INSERT INTO `marc_permissions_modules` VALUES(NULL, 'borrower', 'borrowernumber of user who requested modification', 2);
19
20
CREATE TABLE IF NOT EXISTS `marc_permissions` (
21
    `id` int(11) NOT NULL auto_increment,
22
    `tagfield` varchar(255) NOT NULL, -- can be regexe, so need > 3 chars
23
    `module` int(11) NOT NULL,
24
    `filter` varchar(255) NOT NULL,
25
    `on_new` ENUM('skip', 'add') DEFAULT NULL,
26
    `on_appended` ENUM('skip', 'append') DEFAULT NULL,
27
    `on_removed` ENUM('skip', 'remove') DEFAULT NULL,
28
    `on_deleted` ENUM('skip', 'delete') DEFAULT NULL,
29
    PRIMARY KEY(`id`),
30
    CONSTRAINT `marc_permissions_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_permissions_modules` (`id`) ON DELETE CASCADE
31
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
(-)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 / +1 lines)
Lines 40-46 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-permissions.pl">MARC field permissions</a></li>
43
    <li><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></li>
44
</ul>
44
</ul>
45
45
46
[% 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 / +2 lines)
Lines 93-100 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-permissions.pl">MARC field permissions</a></dt>
96
                    <dt><a href="/cgi-bin/koha/admin/marc-merge-rules.pl">MARC merge rules</a></dt>
97
                    <dd>Managed MARC field permissions</dd>
97
                    <dd>Managed MARC field merge rules</dd>
98
                </dl>
98
                </dl>
99
99
100
                [% 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 (+382 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
[% UNLESS Koha.Preference( 'MARCMergeRules' ) %]
212
    <div class="dialog message">
213
        The <b>MARCMergeRules</b> preference is not set, don't forget to enable it for rules to take effect.
214
    </div>
215
[% END %]
216
[% IF removeConfirm %]
217
<div class="dialog alert">
218
<h3>Remove rule?</h3>
219
<p>Are you sure you want to remove the selected rule(s)?</p>
220
221
<form action="[% script_name %]" method="GET">
222
    <input type="submit" value="No, do not remove" class="deny"/>
223
</form>
224
<input type="button" value="Yes, remove" class="approve" id="doremove" />
225
</div>
226
[% END %]
227
228
<form action="[% script_name %]" method="POST" id="marc-merge-rules-form">
229
<table id="marc-merge-rules">
230
    <thead><tr>
231
        <th>Rule</th>
232
        <th>Module</th>
233
        <th>Filter</th>
234
        <th>Tag</th>
235
        <th>Preset</th>
236
        <th>Added</th>
237
        <th>Appended</th>
238
        <th>Removed</th>
239
        <th>Deleted</th>
240
        <th>Actions</th>
241
        <th>&nbsp;</th>
242
    </tr></thead>
243
    [% UNLESS edit %]
244
    <tfoot>
245
        <tr class="rule-new">
246
            <th>&nbsp;</th>
247
            <th>
248
                <select name="module">
249
                    [% FOREACH module IN modules %]
250
                        <option value="[% module.id %]">[% module.name %]</option>
251
                    [% END %]
252
                </select>
253
            </th>
254
            <th><input type="text" size="5" name="filter"/></th>
255
            <th><input type="text" size="5" name="tag"/></th>
256
            <th>
257
                <select name="preset">
258
                    <option value="" selected>Custom</option>
259
                    [% FOR preset IN ['Protect', 'Overwrite', 'Protect existing', 'Add only', 'Protect from delete'] %]
260
                        <option value="[% preset %]">[% preset %]</option>
261
                    [% END %]
262
                </select>
263
            </th>
264
            <th class="rule-operation-action-edit">
265
                <select name="add">
266
                    <option value="0">Skip</option>
267
                    <option value="1">Add</option>
268
                </select>
269
            </th>
270
            <th class="rule-operation-action-edit">
271
                <select name="append">
272
                    <option value="0">Skip</option>
273
                    <option value="1">Append</option>
274
                </select>
275
            </th>
276
            <th class="rule-operation-action-edit">
277
                <select name="remove">
278
                    <option value="0">Skip</option>
279
                    <option value="1">Remove</option>
280
                </select>
281
            </th>
282
            <th class="rule-operation-action-edit">
283
                <select name="delete">
284
                    <option value="0">Skip</option>
285
                    <option value="1">Delete</option>
286
                </select>
287
            </th>
288
            <th><button class="btn btn-small" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
289
            <th><button id="btn_batchremove" disabled="disabled" class="btn btn-small" title="Batch remove"><i class="fa fa-trash"></i> Delete selected</button></th>
290
        </tr>
291
    </tfoot>
292
    [% END %]
293
    <tbody>
294
        [% FOREACH rule IN rules %]
295
            <tr id="[% rule.id %]" class="rule[% IF rule.edit %]-edit[% END %]">
296
            [% IF rule.edit %]
297
                <td>[% rule.id %]</td>
298
                <td>
299
                    <select name="module">
300
                        [% FOREACH module IN modules %]
301
                            [% IF module.name == rule.module %]
302
                                <option value="[% module.id %]" selected="selected">[% module.name %]</option>
303
                            [% ELSE %]
304
                                <option value="[% module.id %]">[% module.name %]</option>
305
                            [% END %]
306
                        [% END %]
307
                    </select>
308
                </td>
309
                <td><input type="text" size="5" name="filter" value="[% rule.filter %]"/></td>
310
                <td><input type="text" size="3" name="tag" value="[% rule.tag %]"/></td>
311
                <th>
312
                    <select name="preset">
313
                        <option value="" selected>Custom</option>
314
                        [% FOR preset IN ['Protect', 'Overwrite', 'Protect existing', 'Add only', 'Protect from delete'] %]
315
                            <option value="[% preset %]">[% preset %]</option>
316
                        [% END %]
317
                    </select>
318
                </th>
319
                <td class="rule-operation-action-edit">
320
                    <select name="add">
321
                        <option value="0"[% IF !rule.add %] selected="selected"[% END %]>Skip</option>
322
                        <option value="1"[% IF rule.add %] selected="selected"[% END %]>Add</option>
323
                    </select>
324
                </td>
325
                <td class="rule-operation-action-edit">
326
                    <select name="append">
327
                        <option value="0"[% IF !rule.append %] selected="selected"[% END %]>Skip</option>
328
                        <option value="1"[% IF rule.append %] selected="selected"[% END %]>Append</option>
329
                    </select>
330
                </td>
331
                <td class="rule-operation-action-edit">
332
                    <select name="remove">
333
                        <option value="0"[% IF !rule.remove %] selected="selected"[% END %]>Skip</option>
334
                        <option value="1"[% IF rule.remove %] selected="selected"[% END %]>Remove</option>
335
                    </select>
336
                </td>
337
                <td class="rule-operation-action-edit">
338
                    <select name="delete">
339
                        <option value="0"[% IF !rule.delete %] selected="selected"[% END %]>Skip</option>
340
                        <option value="1"[% IF rule.delete %] selected="selected"[% END %]>Delete</option>
341
                    </select>
342
                </td>
343
                <td class="actions">
344
                    <button class="btn btn-mini" title="Save" id="doedit" value="[% rule.id %]"><i class="fa fa-check"></i> Save</button>
345
                    <a href="?"><button class="btn btn-mini" title="Cancel" ><i class="fa fa-times"></i> Cancel</button></a>
346
                </td>
347
                <td></td>
348
            [% ELSE %]
349
                <td>[% rule.id %]</td>
350
                <td>[% rule.module %]</td>
351
                <td>[% rule.filter %]</td>
352
                <td>[% rule.tag %]</td>
353
                <td class="rule-preset"></td>
354
                <td class="rule-operation-action" data-operation="add">[% IF rule.add %]Add[% ELSE %]Skip[% END %]</td>
355
                <td class="rule-operation-action" data-operation="append">[% IF rule.append %]Append[% ELSE %]Skip[% END %]</td>
356
                <td class="rule-operation-action" data-operation="remove">[% IF rule.remove %]Remove[% ELSE %]Skip[% END %]</td>
357
                <td class="rule-operation-action" data-operation="delete">[% IF rule.delete %]Delete[% ELSE %]Skip[% END %]</td>
358
                <td class="actions">
359
                    <a href="?op=remove&id=[% rule.id %]" title="Delete" class="btn btn-mini"><i class="fa fa-trash"></i> Delete</a>
360
                    <a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-mini"><i class="fa fa-pencil"></i> Edit</a>
361
                </td>
362
                <td>
363
                    <input type="checkbox" name="batchremove" value="[% rule.id %]"[% IF rule.removemarked %] checked="checked"[% END %]/>
364
                </td>
365
            [% END %]
366
            </tr>
367
        [% END %]
368
    </tbody>
369
</table>
370
</form>
371
372
<form action="[% script_name %]" method="post">
373
<input type="hidden" name="op" value="redo-matching" />
374
</form>
375
376
</div>
377
</div>
378
<div class="yui-b">
379
[% INCLUDE 'admin-menu.inc' %]
380
</div>
381
</div>
382
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc-permissions.tt (-320 lines)
Lines 1-320 Link Here
1
[% USE Koha %]
2
[% INCLUDE 'doc-head-open.inc' %]
3
<title>Koha &rsaquo; Administration &rsaquo; MARC field permissions</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
//<![CDATA[
16
function doSubmit(op, id) {
17
    $('<input type="hidden"/>')
18
    .attr('name', 'op')
19
    .attr('value', op)
20
    .appendTo('#marc-permissions-form');
21
22
    if(id) {
23
        $('<input type="hidden"/>')
24
        .attr('name', 'id')
25
        .attr('value', id)
26
        .appendTo('#marc-permissions-form');
27
    }
28
29
    var valid = true;
30
    if( op == 'add' || op == 'edit') {
31
        var validate = [
32
                        $('#marc-permissions-form input[name="filter"]'),
33
                        $('#marc-permissions-form input[name="tagfield"]')
34
                       ];
35
        for(var i=0; i < validate.length; i++) {
36
            if(validate[i].val().length == 0) {
37
                validate[i].addClass('required');
38
                valid = false;
39
            } else {
40
                validate[i].removeClass('required');
41
            }
42
        }
43
    }
44
45
    if(valid ) {
46
        $('#marc-permissions-form').submit();
47
    }
48
49
    return valid;
50
}
51
52
$(document).ready(function(){
53
    $('#doremove').on("click",function(){
54
        doSubmit('doremove');
55
    });
56
    $('#doedit').on("click",function(){
57
        doSubmit('doedit', $("#doedit").attr('value'));
58
    });
59
    $('#add').on("click", function(){
60
        doSubmit('add');
61
        return false;
62
    });
63
    $('#btn_batchremove').on("click", function(){
64
        doSubmit('remove');
65
    });
66
67
    /* disable batch remove unless one or more checkboxes are checked */
68
    $('input[name="batchremove"]').change(function() {
69
        if($('input[name="batchremove"]:checked').length > 0) {
70
            $('#btn_batchremove').removeAttr('disabled');
71
        } else {
72
            $('#btn_batchremove').attr('disabled', 'disabled');
73
        }
74
    });
75
76
    /* check validity of regexes in field */
77
    $('input[name="tagfield"]').change(function() {
78
        $(this).removeClass('required');
79
        $(this).attr('title', '');
80
        if($(this).val() != '*') {
81
            try {
82
                new RegExp($(this).val())
83
            } catch ( e ) {
84
                $(this).addClass('required');
85
                $(this).attr('title', 'Invalid regular expression: ' + e.message);
86
            }
87
        }
88
    });
89
90
    $.fn.dataTable.ext.order['dom-input'] = function (settings, col) {
91
        return this.api().column(col, { order: 'index' }).nodes()
92
            .map(function (td, i) {
93
                if($('input', td).val() != undefined) {
94
                    return $('input', td).val();
95
                } else if($('select', td).val() != undefined) {
96
                    return $('option[selected="selected"]', td).val();
97
                } else {
98
                    return $(td).html();
99
                }
100
            });
101
    }
102
103
    $('#marc-permissions').dataTable($.extend(true, {}, dataTablesDefaults, {
104
        "aoColumns": [
105
            {"bSearchable": false, "bSortable": false},
106
            {"sSortDataType": "dom-input"},
107
            {"sSortDataType": "dom-input"},
108
            {"bSearchable": false, "sSortDataType": "dom-input"},
109
            {"bSearchable": false, "sSortDataType": "dom-input"},
110
            {"bSearchable": false, "sSortDataType": "dom-input"},
111
            {"bSearchable": false, "sSortDataType": "dom-input"},
112
            {"bSearchable": false, "sSortDataType": "dom-input"},
113
            {"bSearchable": false, "sSortDataType": "dom-input"},
114
            {"bSearchable": false, "bSortable": false},
115
            {"bSearchable": false, "bSortable": false}
116
        ],
117
        "sPaginationType": "four_button"
118
    }));
119
120
});
121
//]]>
122
</script>
123
</head>
124
<body id="admin_marc-permissions" class="admin">
125
[% INCLUDE 'header.inc' %]
126
[% INCLUDE 'cat-search.inc' %]
127
128
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
129
 &rsaquo; MARC field permissions
130
</div>
131
132
<div id="doc3" class="yui-t2">
133
   <div id="bd">
134
    <div id="yui-main">
135
    <div class="yui-b">
136
137
<h1>Manage MARC field permissions</h1>
138
139
[% UNLESS Koha.Preference( 'MARCPermissions' ) %]
140
    <div class="dialog message">
141
        The <b>MARCPermissions</b> preference is not set, don't forget to enable it for rules to take effect.
142
    </div>
143
[% END %]
144
[% IF removeConfirm %]
145
<div class="dialog alert">
146
<h3>Remove rule?</h3>
147
<p>Are you sure you want to remove the selected rule(s)?</p>
148
149
<form action="[% script_name %]" method="GET">
150
    <input type="submit" value="No, do not remove" class="deny"/>
151
</form>
152
<input type="button" value="Yes, remove" class="approve" id="doremove" />
153
</div>
154
[% END %]
155
156
<form action="[% script_name %]" method="POST" id="marc-permissions-form">
157
<table id="marc-permissions">
158
    <thead><tr>
159
        <th>Rule</th>
160
        <th>Tag</th>
161
        <th>Module</th>
162
        <th>Filter</th>
163
        <th>On New</th>
164
        <th>On Appended</th>
165
        <th>On Removed</th>
166
        <th>On Deleted</th>
167
        <th>Actions</th>
168
        <th>&nbsp;</th>
169
    </tr></thead>
170
    [% UNLESS edit %]
171
    <tfoot>
172
        <tr>
173
            <th>&nbsp;</th>
174
            <th><input type="text" size="5" name="tagfield"/></th>
175
            <th>
176
                <select name="module">
177
                    [% FOREACH module IN modules %]
178
                    <option value="[% module.id %]">[% module.name %]</option>
179
                    [% END %]
180
                </select>
181
            </th>
182
            <th><input type="text" size="5" name="filter"/></th>
183
            <th>
184
                <select name="on_new">
185
                [% FOR on_new IN ['skip', 'add'] %]
186
                    <option value="[% on_new %]">[% on_new %]</option>
187
                [% END %]
188
                </select>
189
            </th>
190
            <th>
191
                <select name="on_appended">
192
                [% FOR on_appended IN ['skip', 'append'] %]
193
                    <option value="[% on_appended %]">[% on_appended %]</option>
194
                [% END %]
195
                </select>
196
            </th>
197
            <th>
198
                <select name="on_removed">
199
                [% FOR on_removed IN ['skip', 'remove'] %]
200
                    <option value="[% on_removed %]">[% on_removed %]</option>
201
                [% END %]
202
                </select>
203
            </th>
204
            <th>
205
                <select name="on_deleted">
206
                [% FOR on_deleted IN ['skip', 'delete'] %]
207
                    <option value="[% on_deleted %]">[% on_deleted %]</option>
208
                [% END %]
209
                </select>
210
            </th>
211
            <th><button class="btn btn-small" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
212
            <th><button id="btn_batchremove" disabled="disabled" class="btn btn-small" title="Batch remove"><i class="fa fa-trash"></i> Delete selected</button></th>
213
        </tr>
214
    </tfoot>
215
    [% END %]
216
    <tbody>
217
        [% FOREACH rule IN rules %]
218
            <tr id="[% rule.id %]">
219
            [% IF rule.edit %]
220
                <td>[% rule.id %]</td>
221
                <td><input type="text" size="3" name="tagfield" value="[% rule.tagfield %]"/></td>
222
                <td>
223
                    <select name="module">
224
                        [% FOREACH module IN modules %]
225
                            [% IF module.name == rule.module %]
226
                                <option value="[% module.id %]" selected="selected">[% module.name %]</option>
227
                            [% ELSE %]
228
                                <option value="[% module.id %]">[% module.name %]</option>
229
                            [% END %]
230
                        [% END %]
231
                    </select>
232
                </td>
233
                <td><input type="text" size="5" name="filter" value="[% rule.filter %]"/></td>
234
                <td>
235
                    <select name="on_new">
236
                        [% FOR on_new IN ['skip', 'add'] %]
237
                            [% IF on_new == rule.on_new %]
238
                                <option value="[% on_new %]" selected="selected">[% on_new %]</option>
239
                            [% ELSE %]
240
                                <option value="[% on_new %]">[% on_new %]</option>
241
                            [% END %]
242
                        [% END %]
243
                    </select>
244
                </td>
245
                <td>
246
                    <select name="on_appended">
247
                        [% FOR on_appended IN ['skip', 'append'] %]
248
                            [% IF on_appended == rule.on_append %]
249
                                <option value="[% on_appended %]" selected="selected">[% on_appended %]</option>
250
                            [% ELSE %]
251
                                <option value="[% on_appended %]">[% on_appended %]</option>
252
                            [% END %]
253
                        [% END %]
254
                    </select>
255
                </td>
256
                <td>
257
                    <select name="on_removed">
258
                        [% FOR on_removed IN ['skip', 'remove'] %]
259
                            [% IF on_removed == rule.on_removed %]
260
                                <option value="[% on_removed %]" selected="selected">[% on_removed %]</option>
261
                            [% ELSE %]
262
                                <option value="[% on_removed %]">[% on_removed %]</option>
263
                            [% END %]
264
                        [% END %]
265
                    </select>
266
                </td>
267
                <td>
268
                    <select name="on_deleted">
269
                        [% FOR on_deleted IN ['skip', 'delete'] %]
270
                            [% IF on_deleted == rule.on_deleted %]
271
                                <option value="[% on_deleted %]" selected="selected">[% on_deleted %]</option>
272
                            [% ELSE %]
273
                                <option value="[% on_deleted %]">[% on_deleted %]</option>
274
                            [% END %]
275
                        [% END %]
276
                    </select>
277
                </td>
278
                <td class="actions">
279
                    <button class="btn btn-mini" title="Save" id="doedit" value="[% rule.id %]"><i class="fa fa-check"></i> Save</button>
280
                    <a href="?"><button class="btn btn-mini" title="Cancel" ><i class="fa fa-times"></i> Cancel</button></a>
281
                </td>
282
                <td></td>
283
            [% ELSE %]
284
                <td>[% rule.id %]</td>
285
                <td>[% rule.tagfield %]</td>
286
                <td>[% rule.module %]</td>
287
                <td>[% rule.filter %]</td>
288
                <td>[% rule.on_new %]</td>
289
                <td>[% rule.on_appended %]</td>
290
                <td>[% rule.on_removed %]</td>
291
                <td>[% rule.on_deleted %]</td>
292
                <td class="actions">
293
                    <a href="?op=remove&id=[% rule.id %]" title="Delete" class="btn btn-mini"><i class="fa fa-trash"></i> Delete</a>
294
                    <a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-mini"><i class="fa fa-pencil"></i> Edit</a>
295
                </td>
296
                <td>
297
                    [% IF rule.remove %]
298
                    <input type="checkbox" name="batchremove" value="[% rule.id %]" checked="checked"/>
299
                    [% ELSE %]
300
                    <input type="checkbox" name="batchremove" value="[% rule.id %]"/>
301
                    [% END %]
302
                </td>
303
            [% END %]
304
            </tr>
305
        [% END %]
306
    </tbody>
307
</table>
308
</form>
309
310
<form action="[% script_name %]" method="post">
311
<input type="hidden" name="op" value="redo-matching" />
312
</form>
313
314
</div>
315
</div>
316
<div class="yui-b">
317
[% INCLUDE 'admin-menu.inc' %]
318
</div>
319
</div>
320
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref (-2 / +2 lines)
Lines 278-285 Cataloging: Link Here
278
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
278
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
279
        -
279
        -
280
            - When importing records
280
            - When importing records
281
            - pref: MARCPermissions
281
            - pref: MARCMergeRules
282
              choices:
282
              choices:
283
                  yes: "use"
283
                  yes: "use"
284
                  no: "don't use"
284
                  no: "don't use"
285
            - MARC permissions rules to decide which action to take for each field.
285
            - MARC merge rules to decide which action to take for each field.
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/logs.pref (-6 lines)
Lines 72-83 Logging: Link Here
72
                  on: Log
72
                  on: Log
73
                  off: "Don't log"
73
                  off: "Don't log"
74
            - when reports are added, deleted or changed.
74
            - when reports are added, deleted or changed.
75
        -
76
            - pref: MARCPermissionsLog
77
              choices:
78
                  on: Log
79
                  off: "Don't log"
80
            - MARC permissions rule evaluations when applying rules for MARC field modifications.
81
    Debugging:
75
    Debugging:
82
        -
76
        -
83
            - pref: DumpTemplateVarsIntranet
77
            - pref: DumpTemplateVarsIntranet
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/help/marc-merge-rules.tt (+144 lines)
Line 0 Link Here
1
[% INCLUDE 'help-top.inc' %]
2
3
<h1>Manage MARC merge rules</h1>
4
5
<h3>Rule evaluation</h3>
6
<p>Rule evaluation begins with determining the active context. A context is a combination of a module and filter value. Depending on where in Koha marc merge rules are applied, different modules and filter values may be passed.</>
7
8
<p>A exact (non wildcard) filter match for an active module has the highest precedence. If there are multiple matches the context with the module with highest specificity is selected. If no exact filter matches are round, wildcard (<b>*</b>) matches are considered. A filter wildcard will match regardless of filter value. If multiple contexts matches, the context with highest module specificity is selected. If none are found we fallback to default actions (see below).</p>
9
10
<p>Only rules for one context (module and filter combination) are ever selected. So if there is a set of rules with module "source" and filter "*", and another set of rules with module "source" and filter "bulkmarcimport". If the active context is module "source" and filter "bulkmarcimport" (as in bulkmarcimport.pl), only the rules for "source" and "bulkmarcimport" will be selected. The rules for module "source" filter "*" will NOT be included in rules selection.</p>
11
12
<p>The selected rules are then applied. Rules are selected from most to least specific. This means that a more specific rule will override a less specific rule. <b>*</b> is less specific than a regular expression and regular expression is less specific than a normal field name (e.g. <b>245</b>).</p>
13
14
<h4>Defaults</h4>
15
<p>Default action when no matching rule is found is to overwrite field (<b>on_new = add, on_appended = append, on_removed = remove, on_deleted = delete</b>). If you wish to changed the default actions for fields and subfields, please add wildcard rules.</p>
16
17
<h4>Wildcards</h4>
18
<p><b>*</b> can be used as a wildcard for <i>Tag</i> and <i>Filter</i>. <b>*</b> is considered less specific than a non wildcard value, and thus will be overridden by a non wildcard value (e.g. "100", "a", etc).</p>
19
20
<h4>Regular expressions</h4>
21
<p>Regular expressions can be used in the <i>Tag</i> field. Beware though, using regular expressions may create overlapping matches, in which case the first matching rule will be applied (in order of rule id).</p>
22
23
<h4>Actions</h4>
24
25
<p>In the following discussion "incoming record" refers to the new record to be merged with "old record" currently present in the Koha database. When merging fields Koha MARC merge rules are applied per field tag and differentiates between four different situations.</p>
26
27
<p>For a specific field tag:</p>
28
29
<p>A field is "added" (new) when old record has no fields with current tag.</p>
30
31
<p>A field is "appended" when old record has fields with same tag but with different data.</p>
32
33
<p>A field is "removed" when field is not present in incoming record but not all fields (with current tag) have been deleted.</p>
34
35
<p>A field is "deleted" all fields with current tag has been deleted.</p>
36
37
<h4>Example rules</h4>
38
<p>Following is matching conditions in order of specificity.</p>
39
40
<table>
41
    <thead><tr>
42
        <th>Tag</th>
43
        <th>Module</th>
44
        <th>Filter</th>
45
        <th>Description</th>
46
    </tr></thead>
47
    <tbody>
48
    <tr>
49
        <td>*</td>
50
        <td>[% modules.0.name %]</td>
51
        <td>*</td>
52
        <td><i>Match any field regardless of [% modules.0.name %]</i></td>
53
    </tr>
54
    <tr>
55
        <td>245</td>
56
        <td>[% modules.0.name %]</td>
57
        <td>*</td>
58
        <td><i>Match field <b>245</b> regardless of [% modules.0.name %]</i></td>
59
    </tr>
60
    <tr>
61
        <td>*</td>
62
        <td>[% modules.0.name %]</td>
63
        <td>z39.50</td>
64
        <td><i>Match any field when [% modules.0.name %] is <b>z39.50</b></i></td>
65
    </tr>
66
    <tr>
67
        <td>5[0-9]{2}</td>
68
        <td>[% modules.0.name %]</td>
69
        <td>z39.50</td>
70
        <td><i>Match fields matching the regular expression <b>5[0-9]{2}</b> (500-599) when [% modules.0.name %] is <b>z39.50</b></i></td>
71
    </tr>
72
    <tr>
73
        <td>245</td>
74
        <td>[% modules.0.name %]</td>
75
        <td>z39.50</td>
76
        <td><i>Match field <b>245</b> when [% modules.0.name %] is <b>z39.50</b></i></td>
77
    </tr>
78
    </tbody>
79
</table>
80
81
<br>
82
83
<h3>Available filter modules</h3>
84
<p>Filters cannot be regular expressions. Please use a single <b>*</b> as a wildcard if need.</p>
85
86
<table>
87
    <thead><tr>
88
        <th>Specificity</th>
89
        <th>Module</th>
90
        <th>Description</th>
91
    </tr></thead>
92
    <tbody>
93
    [% FOREACH module IN modules %]
94
        <tr>
95
            <td>[% module.specificity %]</td>
96
            <td>[% module.name %]</td>
97
            <td>[% module.description %]</td>
98
        </tr>
99
    [% END %]
100
    </tbody>
101
</table>
102
103
<br>
104
105
<h3>Available sources</h3>
106
<p>The following sources currently implement MARCMergeRules.</p>
107
108
<table>
109
    <thead><tr>
110
        <th>Name</th>
111
        <th>Description</th>
112
    </tr></thead>
113
    <tbody><tr>
114
        <td>bulkmarcimport</td>
115
        <td>bin/migration_tools/bulkmarcimport.pl</td>
116
    </tr>
117
    <tr>
118
        <td>import_lexile</td>
119
        <td>bin/migration_tools/import_lexile.pl</td>
120
    </tr>
121
    <tr>
122
        <td>z39.50</td>
123
        <td>Import from Z39.50 search in browser</td>
124
    </tr>
125
    <tr>
126
        <td>intranet</td>
127
        <td>Modifications from intranet in browser</td>
128
    </tr>
129
    <tr>
130
        <td>batchmod</td>
131
        <td>Batch record modification in browser</td>
132
    </tr>
133
    <tr>
134
        <td>batchimport</td>
135
        <td>Batch import of staged MARC records</td>
136
    </tr>
137
    </tbody>
138
</table>
139
140
<br>
141
142
<p><strong>See the full documentation for Koha in the <a href="http://manual.koha-community.org/[% helpVersion %]/en/">manual</a> (online).</strong></p>
143
144
[% INCLUDE 'help-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/help/marc-permissions.tt (-148 lines)
Lines 1-148 Link Here
1
[% INCLUDE 'help-top.inc' %]
2
3
<h1>Manage MARC field permissions</h1>
4
5
<h3>Rule evaluation</h3>
6
<p>Rules are evaluated from least specific to most specific. This means that a more specific rule will override a less specific rule. <b>*</b> is less specific than a regular expression. A regular expression is less specific than a normal field name (e.g. <b>245</b>). A <i>Subfield tag</i> is more specific than a <i>Tag</i>.</p>
7
<p>To add a field-specific rule (i.e. not a subfield), leave the <i>Subfield tag</i> field blank.</p>
8
9
<h4>Defaults</h4>
10
<p>Default action when no rules exist is to replace the old record with the new record. Same as if <b>MARCPermissions</b> is disabled.</p>
11
<p>Default action when no matching rule is found is to leave a field unchanged (<b>skip</b>). If you wish to changed the default actions for fields and subfields, please add wildcard rules.</p>
12
13
<h4>Wildcards</h4>
14
<p><b>*</b> can be used as a wildcard for <i>Tag</i>, <i>Subfield tag</i> and <i>Filter</i>. <b>*</b> is considered less specific than a non wildcard value, and thus will be overridden by a non wildcard value (e.g. "100", "a", etc).</p>
15
16
<h4>Regular expressions</h4>
17
<p>Regular expressions can be used in both the <i>Tag</i> and <i>Subfield tag</i> fields. Beware though, using regular expressions may create overlapping matches, in which case they will be applied in a sorted order.</p>
18
19
<h4>Example rules</h4>
20
<p>Following is an example rule set in order of specificity.</p>
21
22
<table>
23
    <thead><tr>
24
        <th>Tag</th>
25
        <th>Subfield tag</th>
26
        <th>Module</th>
27
        <th>Filter</th>
28
        <th>Description</th>
29
    </tr></thead>
30
    <tbody><tr>
31
        <td>*</td>
32
        <td></td>
33
        <td>[% modules.0.name %]</td>
34
        <td>*</td>
35
        <td><i>Match any field regardless of [% modules.0.name %]</i></td>
36
    </tr>
37
    <tr>
38
        <td>*</td>
39
        <td>*</td>
40
        <td>[% modules.0.name %]</td>
41
        <td>*</td>
42
        <td><i>Match any subfield regardless of [% modules.0.name %]</i></td>
43
    </tr>
44
    <tr>
45
        <td>245</td>
46
        <td>*</td>
47
        <td>[% modules.0.name %]</td>
48
        <td>z39.50</td>
49
        <td><i>Match any subfield under <b>245</b> when [% modules.0.name %] is <b>z39.50</b></i></td>
50
    </tr>
51
    <tr>
52
        <td>*</td>
53
        <td>b</td>
54
        <td>[% modules.0.name %]</td>
55
        <td>z39.50</td>
56
        <td><i>Match subfield <b>b</b> regardless of field when [% modules.0.name %] is <b>z39.50</b></i></td>
57
    </tr>
58
    <tr>
59
        <td>500</td>
60
        <td>[a-d]</td>
61
        <td>[% modules.0.name %]</td>
62
        <td>z39.50</td>
63
        <td><i>Match subfields <b>a, b, c, d</b> when field is 500 and [% modules.0.name %] is <b>z39.50</b></i></td>
64
    </tr>
65
    <tr>
66
        <td>5..</td>
67
        <td>a</td>
68
        <td>[% modules.0.name %]</td>
69
        <td>z39.50</td>
70
        <td><i>Match subfield <b>a</b> when field matches the regular expression <b>5..</b> (500-599) and [% modules.0.name %] is <b>z39.50</b></i></td>
71
    </tr>
72
    <tr>
73
        <td>245</td>
74
        <td>a</td>
75
        <td>[% modules.0.name %]</td>
76
        <td>z39.50</td>
77
        <td><i>Match subfield <b>a</b> when field is <b>500</b> and [% modules.0.name %] is <b>z39.50</b></i></td>
78
    </tr></tbody>
79
</table>
80
81
<br>
82
83
<h3>Available filter modules</h3>
84
<p>Filters cannot be regular expressions. Please use a single <b>*</b> as a wildcard if need.</p>
85
86
<table>
87
    <thead><tr>
88
        <th>Specificity</th>
89
        <th>Module</th>
90
        <th>Description</th>
91
    </tr></thead>
92
    <tbody>
93
    [% FOREACH module IN modules %]
94
        <tr>
95
            <td>[% module.specificity %]</td>
96
            <td>[% module.name %]</td>
97
            <td>[% module.description %]</td>
98
        </tr>
99
    [% END %]
100
    </tbody>
101
</table>
102
103
<br>
104
105
<h3>Available sources</h3>
106
<p>The following sources currently implement MARCPermissions.</p>
107
108
<table>
109
    <thead><tr>
110
        <th>Name</th>
111
        <th>Description</th>
112
    </tr></thead>
113
    <tbody><tr>
114
        <td>bulkmarcimport</td>
115
        <td>bin/migration_tools/bulkmarcimport.pl</td>
116
    </tr>
117
    <tr>
118
        <td>import_lexile</td>
119
        <td>bin/migration_tools/import_lexile.pl</td>
120
    </tr>
121
    <tr>
122
        <td>z39.50</td>
123
        <td>Import from Z39.50 search in browser</td>
124
    </tr>
125
    <tr>
126
        <td>intranet</td>
127
        <td>Modifications from intranet in browser</td>
128
    </tr>
129
    <tr>
130
        <td>batchmod</td>
131
        <td>Batch record modification in browser</td>
132
    </tr></tbody>
133
</table>
134
135
<br>
136
137
<h3>Indicators</h3>
138
<p>Indicators can be addressed as <b>i1</b> and <b>i2</b> in the <i>Subfield tag</i> field. Some actions might not be available for indicators, in which case they will be disabled.</p>
139
140
<br>
141
142
<h3>Logging</h3>
143
<p>If <b>MARCPermissionsLog</b> is enabled, log entries for each record modification will be available in the <b>Modification log</b> in the <b>Catalog</b> module under the <b>Modify</b> action. This can be very helpful when debugging rule sets.</p>
144
<p>The administration area is where you set all of your preferences for the system. Preference are broken down into several categories, detailed below.</p>
145
146
<p><strong>See the full documentation for Koha in the <a href="http://manual.koha-community.org/[% helpVersion %]/en/">manual</a> (online).</strong></p>
147
148
[% INCLUDE 'help-bottom.inc' %]
(-)a/t/db_dependent/Biblio/MARCPermissions.t (-279 lines)
Lines 1-279 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Test::More tests => 16;
21
use Test::MockModule;
22
23
use MARC::Record;
24
use Data::Dumper;
25
26
BEGIN {
27
    use_ok('C4::Biblio');
28
}
29
30
# Start transaction
31
my $dbh = C4::Context->dbh;
32
$dbh->{AutoCommit} = 0;
33
$dbh->{RaiseError} = 1;
34
35
C4::Context->set_preference( 'MARCPermissions', '1');
36
C4::Context->set_preference( 'MARCPermissionsLog', '1');
37
38
# Create a record
39
my $record = MARC::Record->new();
40
$record->append_fields (
41
    MARC::Field->new('008', '12345'),
42
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
43
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
44
    MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
45
    MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'),
46
    MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'),
47
);
48
49
# Add record to DB
50
my ($biblionumber, $biblioitemnumber) = AddBiblio($record, '');
51
52
my $modules = GetMarcPermissionsModules();
53
54
##############################################################################
55
# Test overwrite rule
56
my $mod_record = MARC::Record->new();
57
$mod_record->append_fields (
58
    MARC::Field->new('008', '12345'),
59
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
60
    MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
61
    MARC::Field->new('500', '1','1', 'a' => 'this field has now been changed'),
62
    MARC::Field->new('500', '2','2', 'a' => 'and so have this field'),
63
);
64
65
# Clear MARC permission rules from DB
66
DelMarcPermissionsRule($_->{id}) for GetMarcPermissionsRules();
67
68
# Add MARC permission rules to DB
69
AddMarcPermissionsRule({
70
    module => $modules->[0]->{'id'},
71
    tagfield => '*',
72
    tagsubfield => '',
73
    filter => '*',
74
    on_existing => 'overwrite',
75
    on_new => 'add',
76
    on_removed => 'remove'
77
});
78
79
my @log = ();
80
my $new_record = ApplyMarcPermissions({
81
        biblionumber => $biblionumber,
82
        record => $mod_record,
83
        frameworkcode => '',
84
        filter => {$modules->[0]->{'name'} => 'foo'},
85
        log => \@log
86
    });
87
88
my @a500 = $new_record->field('500');
89
is ($a500[0]->subfield('a'), 'this field has now been changed', 'old field is replaced when overwrite');
90
is ($a500[1]->subfield('a'), 'and so have this field', 'old field is replaced when overwrite');
91
92
##############################################################################
93
# Test remove rule
94
is ($new_record->field('250'), undef, 'removed field is removed');
95
96
##############################################################################
97
# Test skip rule
98
$mod_record = MARC::Record->new();
99
$mod_record->append_fields (
100
    MARC::Field->new('008', '12345'),
101
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
102
    MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
103
    MARC::Field->new('500', '1','1', 'a' => 'this should not show'),
104
    MARC::Field->new('500', '2','2', 'a' => 'and neither should this'),
105
);
106
107
AddMarcPermissionsRule({
108
    module => $modules->[0]->{'id'},
109
    tagfield => '500',
110
    tagsubfield => '*',
111
    filter => '*',
112
    on_existing => 'skip',
113
    on_new => 'skip',
114
    on_removed => 'skip'
115
});
116
117
@log = ();
118
$new_record = ApplyMarcPermissions({
119
        biblionumber => $biblionumber,
120
        record => $mod_record,
121
        frameworkcode => '',
122
        filter => {$modules->[0]->{'name'} => 'foo'},
123
        log => \@log
124
    });
125
126
@a500 = $new_record->field('500');
127
is ($a500[0]->subfield('a'), 'the lazy programmer jumps over the quick brown tests', 'old field is kept when skip');
128
is ($a500[1]->subfield('a'), 'the quick brown test jumps over the lazy programmers', 'old field is kept when skip');
129
130
##############################################################################
131
# Test add rule
132
$mod_record = MARC::Record->new();
133
$mod_record->append_fields (
134
    MARC::Field->new('008', '12345'),
135
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
136
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
137
    #MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
138
    MARC::Field->new('245', '1','2', 'a' => 'some new fun value'),
139
    MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'),
140
    MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'),
141
);
142
143
AddMarcPermissionsRule({
144
    module => $modules->[0]->{'id'},
145
    tagfield => '245',
146
    tagsubfield => '*',
147
    filter => '*',
148
    on_existing => 'add',
149
    on_new => 'add',
150
    on_removed => 'skip'
151
});
152
153
154
@log = ();
155
$new_record = ApplyMarcPermissions({
156
        biblionumber => $biblionumber,
157
        record => $mod_record,
158
        frameworkcode => '',
159
        filter => {$modules->[0]->{'name'} => 'foo'},
160
        log => \@log
161
    });
162
163
my @a245 = $new_record->field('245')->subfield('a');
164
is ($a245[0], 'field data for 245 a with indicators 12', 'old field is kept when adding new');
165
is ($a245[1], 'some new fun value', 'new field is added');
166
167
##############################################################################
168
# Test add_or_correct rule
169
$mod_record = MARC::Record->new();
170
$mod_record->append_fields (
171
    MARC::Field->new('008', '12345'),
172
    #MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
173
    MARC::Field->new('100', '','', 'a' => 'a very different value'),
174
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
175
    #MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
176
    MARC::Field->new('245', '1','2', 'a' => 'Field data for 245 a with indicators 12', 'a' => 'some very different value'),
177
    MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'),
178
    MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'),
179
);
180
181
AddMarcPermissionsRule({
182
    module => $modules->[0]->{'id'},
183
    tagfield => '(100|245)',
184
    tagsubfield => '*',
185
    filter => '*',
186
    on_existing => 'add_or_correct',
187
    on_new => 'add',
188
    on_removed => 'skip'
189
});
190
191
@log = ();
192
$new_record = ApplyMarcPermissions({
193
        biblionumber => $biblionumber,
194
        record => $mod_record,
195
        frameworkcode => '',
196
        filter => {$modules->[0]->{'name'} => 'foo'},
197
        log => \@log
198
    });
199
200
@a245 = $new_record->field('245')->subfield('a');
201
is ($a245[0], 'Field data for 245 a with indicators 12', 'add_or_correct modifies field when a correction');
202
is ($a245[1], 'some very different value', 'add_or_correct adds field when not a correction');
203
204
my @a100 = $new_record->field('100')->subfield('a');
205
is ($a100[0], 'field data for 100 a without indicators', 'add_or_correct keeps old field when not a correction');
206
is ($a100[1], 'a very different value', 'add_or_correct adds field when not a correction');
207
208
##############################################################################
209
# Test rule evaluation order
210
$mod_record = MARC::Record->new();
211
$mod_record->append_fields (
212
    MARC::Field->new('008', '12345'),
213
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
214
    MARC::Field->new('250', '','', 'a' => 'take one down, pass it around'),
215
    MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
216
    MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'),
217
    MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'),
218
);
219
220
221
DelMarcPermissionsRule($_->{id}) for GetMarcPermissionsRules();
222
223
AddMarcPermissionsRule({
224
    module => $modules->[0]->{'id'},
225
    tagfield => '*',
226
    tagsubfield => '*',
227
    filter => '*',
228
    on_existing => 'skip',
229
    on_new => 'skip',
230
    on_removed => 'skip'
231
});
232
AddMarcPermissionsRule({
233
    module => $modules->[0]->{'id'},
234
    tagfield => '250',
235
    tagsubfield => '*',
236
    filter => '*',
237
    on_existing => 'overwrite',
238
    on_new => 'skip',
239
    on_removed => 'skip'
240
});
241
AddMarcPermissionsRule({
242
    module => $modules->[0]->{'id'},
243
    tagfield => '*',
244
    tagsubfield => 'a',
245
    filter => '*',
246
    on_existing => 'add_or_correct',
247
    on_new => 'skip',
248
    on_removed => 'skip'
249
});
250
AddMarcPermissionsRule({
251
    module => $modules->[0]->{'id'},
252
    tagfield => '250',
253
    tagsubfield => 'a',
254
    filter => '*',
255
    on_existing => 'add',
256
    on_new => 'skip',
257
    on_removed => 'skip'
258
});
259
260
@log = ();
261
$new_record = ApplyMarcPermissions({
262
        biblionumber => $biblionumber,
263
        record => $mod_record,
264
        frameworkcode => '',
265
        filter => {$modules->[0]->{'name'} => 'foo'},
266
        log => \@log
267
    });
268
269
my @rule = grep { $_->{tag} eq '250' and $_->{subfieldcode} eq 'a' } @log;
270
is(scalar @rule, 1, 'only one rule applied');
271
is($rule[0]->{event}.':'.$rule[0]->{action}, 'existing:add', 'most specific rule used');
272
273
my @a250 = $new_record->field('250')->subfield('a');
274
is ($a250[0], '250 bottles of beer on the wall', 'most specific rule is applied, original field kept');
275
is ($a250[1], 'take one down, pass it around', 'most specific rule is applied, new field added');
276
277
$dbh->rollback;
278
279
1;
(-)a/t/db_dependent/Biblio/MarcMergeRules.t (-1 / +689 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use MARC::Record;
21
22
use C4::Context;
23
use C4::Biblio;
24
use Koha::Database; #??
25
26
use Test::More tests => 20;
27
use Test::MockModule;
28
29
use Koha::MarcMergeRules;
30
use Koha::MarcMergeRulesModules;
31
32
use t::lib::Mocks;
33
34
my $schema = Koha::Database->schema;
35
$schema->storage->txn_begin;
36
37
t::lib::Mocks::mock_preference('MARCMergeRules', '1');
38
39
# Create a record
40
my $orig_record = MARC::Record->new();
41
$orig_record->append_fields (
42
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
43
    MARC::Field->new('250', '','', 'a' => '256 bottles of beer on the wall'),
44
    MARC::Field->new('500', '','', 'a' => 'One bottle of beer in the fridge'),
45
);
46
47
# Order modules by specificity and get first two
48
my $modules_rs = Koha::MarcMergeRulesModules->search(undef, { order_by => { -desc => 'specificity' } });
49
my @modules;
50
push @modules, $modules_rs->next;
51
push @modules, $modules_rs->next;
52
53
54
my $incoming_record = MARC::Record->new();
55
$incoming_record->append_fields(
56
    MARC::Field->new('250', '', '', 'a' => '256 bottles of beer on the wall'), # Unchanged
57
    MARC::Field->new('250', '', '', 'a' => '251 bottles of beer on the wall'), # Appended
58
    # MARC::Field->new('250', '', '', 'a' => '250 bottles of beer on the wall'), # Removed
59
    # MARC::Field->new('500', '', '', 'a' => 'One bottle of beer in the fridge'), # Deleted
60
    MARC::Field->new('501', '', '', 'a' => 'One cold bottle of beer in the fridge'), # Added
61
    MARC::Field->new('501', '', '', 'a' => 'Two cold bottles of beer in the fridge'), # Added
62
);
63
64
# Test default behavior when MARCMergeRules is enabled, but no rules defined (overwrite)
65
subtest 'Record fields has been overwritten when no merge rules are defined' => sub {
66
    plan tests => 4;
67
68
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
69
70
    my @all_fields = $merged_record->fields();
71
72
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
73
    is_deeply(
74
        [map { $_->subfield('a') } $merged_record->field('250') ],
75
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
76
        '"250" fields has been appended and removed'
77
    );
78
79
    my @fields = $merged_record->field('500');
80
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
81
82
    is_deeply(
83
        [map { $_->subfield('a') } $merged_record->field('501') ],
84
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
85
        '"501" fields has been added'
86
    );
87
};
88
89
my $rule =  Koha::MarcMergeRules->find_or_create({
90
    tag => '*',
91
    module => $modules[0]->id,
92
    filter => '*',
93
    add => 0,
94
    append => 0,
95
    remove => 0,
96
    delete => 0
97
});
98
99
subtest 'Record fields has been protected when matched merge all rule operations are set to "0"' => sub {
100
    plan tests => 3;
101
102
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
103
104
    my @all_fields = $merged_record->fields();
105
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
106
107
    is_deeply(
108
        [map { $_->subfield('a') } $merged_record->field('250') ],
109
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
110
        '"250" fields has retained their original value'
111
    );
112
    is_deeply(
113
        [map { $_->subfield('a') } $merged_record->field('500') ],
114
        ['One bottle of beer in the fridge'],
115
        '"500" field has retained it\'s original value'
116
    );
117
};
118
119
120
subtest 'Only new fields has been added when add = 1, append = 0, remove = 0, delete = 0' => sub {
121
    plan tests => 4;
122
123
    $rule->set(
124
        {
125
            'add' => 1,
126
            'append' => 0,
127
            'remove' => 0,
128
            'delete' => 0,
129
        }
130
    );
131
    $rule->store();
132
133
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
134
135
    my @all_fields = $merged_record->fields();
136
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
137
138
    is_deeply(
139
        [map { $_->subfield('a') } $merged_record->field('250') ],
140
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
141
        '"250" fields retain their original value'
142
    );
143
144
    is_deeply(
145
        [map { $_->subfield('a') } $merged_record->field('500') ],
146
        ['One bottle of beer in the fridge'],
147
        '"500" field retain it\'s original value'
148
    );
149
150
    is_deeply(
151
        [map { $_->subfield('a') } $merged_record->field('501') ],
152
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
153
        '"501" fields has been added'
154
    );
155
};
156
157
subtest 'Only appended fields has been added when add = 0, append = 1, remove = 0, delete = 0' => sub {
158
    plan tests => 3;
159
160
    $rule->set(
161
        {
162
            'add' => 0,
163
            'append' => 1,
164
            'remove' => 0,
165
            'delete' => 0,
166
        }
167
    );
168
    $rule->store();
169
170
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
171
172
    my @all_fields = $merged_record->fields();
173
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
174
175
    is_deeply(
176
        [map { $_->subfield('a') } $merged_record->field('250') ],
177
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
178
        '"251" field has been appended'
179
    );
180
181
    is_deeply(
182
        [map { $_->subfield('a') } $merged_record->field('500') ],
183
        ['One bottle of beer in the fridge'],
184
        '"500" field has retained it\'s original value'
185
    );
186
187
};
188
189
subtest 'Appended and added fields has been added when add = 1, append = 1, remove = 0, delete = 0' => sub {
190
    plan tests => 4;
191
192
    $rule->set(
193
        {
194
            'add' => 1,
195
            'append' => 1,
196
            'remove' => 0,
197
            'delete' => 0,
198
        }
199
    );
200
    $rule->store();
201
202
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
203
204
    my @all_fields = $merged_record->fields();
205
    cmp_ok(scalar @all_fields, '==', 6, "Record has the expected number of fields");
206
207
    is_deeply(
208
        [map { $_->subfield('a') } $merged_record->field('250') ],
209
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
210
        '"251" field has been appended'
211
    );
212
213
    is_deeply(
214
        [map { $_->subfield('a') } $merged_record->field('500') ],
215
        ['One bottle of beer in the fridge'],
216
        '"500" field has retained it\'s original value'
217
    );
218
219
    is_deeply(
220
        [map { $_->subfield('a') } $merged_record->field('501') ],
221
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
222
        '"501" fields has been added'
223
    );
224
};
225
226
subtest 'Record fields has been only removed when add = 0, append = 0, remove = 1, delete = 0' => sub {
227
    plan tests => 3;
228
229
    $rule->set(
230
        {
231
            'add' => 0,
232
            'append' => 0,
233
            'remove' => 1,
234
            'delete' => 0,
235
        }
236
    );
237
    $rule->store();
238
239
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
240
241
    my @all_fields = $merged_record->fields();
242
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
243
244
    is_deeply(
245
        [map { $_->subfield('a') } $merged_record->field('250') ],
246
        ['256 bottles of beer on the wall'],
247
        '"250" field has been removed'
248
    );
249
    is_deeply(
250
        [map { $_->subfield('a') } $merged_record->field('500') ],
251
        ['One bottle of beer in the fridge'],
252
        '"500" field has retained it\'s original value'
253
    );
254
};
255
256
subtest 'Record fields has been added and removed when add = 1, append = 0, remove = 1, delete = 0' => sub {
257
    plan tests => 4;
258
259
    $rule->set(
260
        {
261
            'add' => 1,
262
            'append' => 0,
263
            'remove' => 1,
264
            'delete' => 0,
265
        }
266
    );
267
    $rule->store();
268
269
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
270
271
    my @all_fields = $merged_record->fields();
272
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
273
274
    is_deeply(
275
        [map { $_->subfield('a') } $merged_record->field('250') ],
276
        ['256 bottles of beer on the wall'],
277
        '"250" field has been removed'
278
    );
279
    is_deeply(
280
        [map { $_->subfield('a') } $merged_record->field('500') ],
281
        ['One bottle of beer in the fridge'],
282
        '"500" field has retained it\'s original value'
283
    );
284
285
    is_deeply(
286
        [map { $_->subfield('a') } $merged_record->field('501') ],
287
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
288
        '"501" fields has been added'
289
    );
290
};
291
292
subtest 'Record fields has been appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub {
293
    plan tests => 3;
294
295
    $rule->set(
296
        {
297
            'add' => 0,
298
            'append' => 1,
299
            'remove' => 1,
300
            'delete' => 0,
301
        }
302
    );
303
    $rule->store();
304
305
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
306
307
    my @all_fields = $merged_record->fields();
308
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
309
310
    is_deeply(
311
        [map { $_->subfield('a') } $merged_record->field('250') ],
312
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
313
        '"250" fields has been appended and removed'
314
    );
315
    is_deeply(
316
        [map { $_->subfield('a') } $merged_record->field('500') ],
317
        ['One bottle of beer in the fridge'],
318
        '"500" field has retained it\'s original value'
319
    );
320
};
321
322
subtest 'Record fields has been added, appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub {
323
    plan tests => 4;
324
325
    $rule->set(
326
        {
327
            'add' => 1,
328
            'append' => 1,
329
            'remove' => 1,
330
            'delete' => 0,
331
        }
332
    );
333
    $rule->store();
334
335
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
336
337
    my @all_fields = $merged_record->fields();
338
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
339
340
    is_deeply(
341
        [map { $_->subfield('a') } $merged_record->field('250') ],
342
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
343
        '"250" fields has been appended and removed'
344
    );
345
346
    is_deeply(
347
        [map { $_->subfield('a') } $merged_record->field('500') ],
348
        ['One bottle of beer in the fridge'],
349
        '"500" field has retained it\'s original value'
350
    );
351
352
    is_deeply(
353
        [map { $_->subfield('a') } $merged_record->field('501') ],
354
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
355
        '"501" fields has been added'
356
    );
357
};
358
359
subtest 'Record fields has been deleted when add = 0, append = 0, remove = 0, delete = 1' => sub {
360
    plan tests => 2;
361
362
    $rule->set(
363
        {
364
            'add' => 0,
365
            'append' => 0,
366
            'remove' => 0,
367
            'delete' => 1,
368
        }
369
    );
370
    $rule->store();
371
372
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
373
374
    my @all_fields = $merged_record->fields();
375
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
376
377
    is_deeply(
378
        [map { $_->subfield('a') } $merged_record->field('250') ],
379
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
380
        '"250" fields has retained their original value'
381
    );
382
};
383
384
subtest 'Record fields has been added and deleted when add = 1, append = 0, remove = 0, delete = 1' => sub {
385
    plan tests => 3;
386
387
    $rule->set(
388
        {
389
            'add' => 1,
390
            'append' => 0,
391
            'remove' => 0,
392
            'delete' => 1,
393
        }
394
    );
395
    $rule->store();
396
397
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
398
399
    my @all_fields = $merged_record->fields();
400
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
401
402
    is_deeply(
403
        [map { $_->subfield('a') } $merged_record->field('250') ],
404
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall'],
405
        '"250" fields has retained their original value'
406
    );
407
408
    is_deeply(
409
        [map { $_->subfield('a') } $merged_record->field('501') ],
410
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
411
        '"501" fields has been added'
412
    );
413
};
414
415
subtest 'Record fields has been appended and deleted when add = 0, append = 1, remove = 0, delete = 1' => sub {
416
    plan tests => 2;
417
418
    $rule->set(
419
        {
420
            'add' => 0,
421
            'append' => 1,
422
            'remove' => 0,
423
            'delete' => 1,
424
        }
425
    );
426
    $rule->store();
427
428
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
429
430
    my @all_fields = $merged_record->fields();
431
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
432
433
    is_deeply(
434
        [map { $_->subfield('a') } $merged_record->field('250') ],
435
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
436
        '"250" field has been appended'
437
    );
438
};
439
440
subtest 'Record fields has been added, appended and deleted when add = 1, append = 1, remove = 0, delete = 1' => sub {
441
    plan tests => 3;
442
443
    $rule->set(
444
        {
445
            'add' => 1,
446
            'append' => 1,
447
            'remove' => 0,
448
            'delete' => 1,
449
        }
450
    );
451
    $rule->store();
452
453
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
454
455
    my @all_fields = $merged_record->fields();
456
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
457
458
    is_deeply(
459
        [map { $_->subfield('a') } $merged_record->field('250') ],
460
        ['250 bottles of beer on the wall', '256 bottles of beer on the wall', '251 bottles of beer on the wall'],
461
        '"250" field has been appended'
462
    );
463
464
    is_deeply(
465
        [map { $_->subfield('a') } $merged_record->field('501') ],
466
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
467
        '"501" fields has been added'
468
    );
469
};
470
471
subtest 'Record fields has been removed and deleted when add = 0, append = 0, remove = 1, delete = 1' => sub {
472
    plan tests => 2;
473
474
    $rule->set(
475
        {
476
            'add' => 0,
477
            'append' => 0,
478
            'remove' => 1,
479
            'delete' => 1,
480
        }
481
    );
482
    $rule->store();
483
484
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
485
486
    my @all_fields = $merged_record->fields();
487
    cmp_ok(scalar @all_fields, '==', 1, "Record has the expected number of fields");
488
489
    is_deeply(
490
        [map { $_->subfield('a') } $merged_record->field('250') ],
491
        ['256 bottles of beer on the wall'],
492
        '"250" field has been removed'
493
    );
494
};
495
496
subtest 'Record fields has been added, removed and deleted when add = 1, append = 0, remove = 1, delete = 1' => sub {
497
    plan tests => 3;
498
499
    $rule->set(
500
        {
501
            'add' => 1,
502
            'append' => 0,
503
            'remove' => 1,
504
            'delete' => 1,
505
        }
506
    );
507
    $rule->store();
508
509
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
510
511
    my @all_fields = $merged_record->fields();
512
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
513
514
    is_deeply(
515
        [map { $_->subfield('a') } $merged_record->field('250') ],
516
        ['256 bottles of beer on the wall'],
517
        '"250" field has been removed'
518
    );
519
520
    is_deeply(
521
        [map { $_->subfield('a') } $merged_record->field('501') ],
522
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
523
        '"501" fields has been added'
524
    );
525
};
526
527
subtest 'Record fields has been appended, removed and deleted when add = 0, append = 1, remove = 1, delete = 1' => sub {
528
    plan tests => 2;
529
530
    $rule->set(
531
        {
532
            'add' => 0,
533
            'append' => 1,
534
            'remove' => 1,
535
            'delete' => 1,
536
        }
537
    );
538
    $rule->store();
539
540
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
541
542
    my @all_fields = $merged_record->fields();
543
    cmp_ok(scalar @all_fields, '==', 2, "Record has the expected number of fields");
544
545
    is_deeply(
546
        [map { $_->subfield('a') } $merged_record->field('250') ],
547
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
548
        '"250" fields has been appended and removed'
549
    );
550
};
551
552
subtest 'Record fields has been overwritten when add = 1, append = 1, remove = 1, delete = 1' => sub {
553
    plan tests => 4;
554
555
    $rule->set(
556
        {
557
            'add' => 1,
558
            'append' => 1,
559
            'remove' => 1,
560
            'delete' => 1,
561
        }
562
    );
563
    $rule->store();
564
565
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
566
567
    my @all_fields = $merged_record->fields();
568
569
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
570
    is_deeply(
571
        [map { $_->subfield('a') } $merged_record->field('250') ],
572
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
573
        '"250" fields has been appended and removed'
574
    );
575
576
    my @fields = $merged_record->field('500');
577
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
578
579
    is_deeply(
580
        [map { $_->subfield('a') } $merged_record->field('501') ],
581
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
582
        '"501" fields has been added'
583
    );
584
};
585
586
# Test rule tag specificity
587
588
# Protect field 500 with more specific tag value
589
my $skip_all_rule = Koha::MarcMergeRules->find_or_create({
590
    tag => '500',
591
    module => $modules[0]->id,
592
    filter => '*',
593
    add => 0,
594
    append => 0,
595
    remove => 0,
596
    delete => 0
597
});
598
599
subtest '"500" field has been protected when rule matching on tag "500" is add = 0, append = 0, remove = 0, delete = 0' => sub {
600
    plan tests => 4;
601
602
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
603
604
    my @all_fields = $merged_record->fields();
605
606
    cmp_ok(scalar @all_fields, '==', 5, "Record has the expected number of fields");
607
    is_deeply(
608
        [map { $_->subfield('a') } $merged_record->field('250') ],
609
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
610
        '"250" fields has been appended and removed'
611
    );
612
613
    is_deeply(
614
        [map { $_->subfield('a') } $merged_record->field('500') ],
615
        ['One bottle of beer in the fridge'],
616
        '"500" field has retained it\'s original value'
617
    );
618
619
    is_deeply(
620
        [map { $_->subfield('a') } $merged_record->field('501') ],
621
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
622
        '"501" fields has been added'
623
    );
624
};
625
626
# Test regexp matching
627
subtest '"5XX" fields has been protected when rule matching on regexp "5\d{2}" is add = 0, append = 0, remove = 0, delete = 0' => sub {
628
    plan tests => 3;
629
630
    $skip_all_rule->set(
631
        {
632
            'tag' => '5\d{2}',
633
        }
634
    );
635
    $skip_all_rule->store();
636
637
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
638
639
    my @all_fields = $merged_record->fields();
640
641
    cmp_ok(scalar @all_fields, '==', 3, "Record has the expected number of fields");
642
    is_deeply(
643
        [map { $_->subfield('a') } $merged_record->field('250') ],
644
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
645
        '"250" fields has been appended and removed'
646
    );
647
648
    is_deeply(
649
        [map { $_->subfield('a') } $merged_record->field('500') ],
650
        ['One bottle of beer in the fridge'],
651
        '"500" field has retained it\'s original value'
652
    );
653
};
654
655
# Test module specificity, the 0 all rule should no longer be included in set of applied rules
656
subtest 'Record fields has been overwritten when non wild card rule with filter match is add = 1, append = 1, remove = 1, delete = 1' => sub {
657
    plan tests => 4;
658
659
    $rule->set(
660
        {
661
            'filter' => 'test',
662
        }
663
    );
664
    $rule->store();
665
666
    my $merged_record = Koha::MarcMergeRules->merge_records($orig_record, $incoming_record, { $modules[0]->name => 'test' });
667
668
    my @all_fields = $merged_record->fields();
669
670
    cmp_ok(scalar @all_fields, '==', 4, "Record has the expected number of fields");
671
    is_deeply(
672
        [map { $_->subfield('a') } $merged_record->field('250') ],
673
        ['256 bottles of beer on the wall', '251 bottles of beer on the wall'],
674
        '"250" fields has been appended and removed'
675
    );
676
677
    my @fields = $merged_record->field('500');
678
    cmp_ok(scalar @fields, '==', 0, '"500" field has been deleted');
679
680
    is_deeply(
681
        [map { $_->subfield('a') } $merged_record->field('501') ],
682
        ['One cold bottle of beer in the fridge', 'Two cold bottles of beer in the fridge'],
683
        '"501" fields has been added'
684
    );
685
};
686
687
$schema->storage->txn_rollback;
688
689
1;

Return to bug 14957