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

(-)a/C4/Biblio.pm (-550 / +34 lines)
Lines 57-67 BEGIN { Link Here
57
        PrepHostMarcField
57
        PrepHostMarcField
58
        CountItemsIssued
58
        CountItemsIssued
59
        CountBiblioInOrders
59
        CountBiblioInOrders
60
        GetMarcPermissionsRules
60
        GetMarcMergeRules
61
        GetMarcPermissionsModules
61
        GetMarcMergeModules
62
        ModMarcPermissionsRule
62
        ModMarcMergeRule
63
        AddMarcPermissionsRule
63
        AddMarcMergeRule
64
        DelMarcPermissionsRule
64
        DelMarcMergeRule
65
        ModBiblio
65
        ModBiblio
66
        ModZebra
66
        ModZebra
67
        UpdateTotalIssues
67
        UpdateTotalIssues
Lines 69-75 BEGIN { Link Here
69
        DelBiblio
69
        DelBiblio
70
        BiblioAutoLink
70
        BiblioAutoLink
71
        LinkBibHeadingsToAuthorities
71
        LinkBibHeadingsToAuthorities
72
        ApplyMarcPermissions
72
        ApplyMarcMergeRules
73
        TransformMarcToKoha
73
        TransformMarcToKoha
74
        TransformHtmlToMarc
74
        TransformHtmlToMarc
75
        TransformHtmlToXml
75
        TransformHtmlToXml
Lines 300-311 sub ModBiblio { Link Here
300
300
301
    _strip_item_fields($record, $frameworkcode);
301
    _strip_item_fields($record, $frameworkcode);
302
302
303
    # apply permissions
303
    # apply merge rules
304
    if (C4::Context->preference('MARCPermissions') && $biblionumber && defined $options && exists $options->{'context'}) {
304
    if (C4::Context->preference('MARCMergeRules') && $biblionumber && defined $options && exists $options->{'context'}) {
305
        $record = ApplyMarcPermissions({
305
        $record = ApplyMarcMergeRules({
306
                biblionumber => $biblionumber,
306
                biblionumber => $biblionumber,
307
                record => $record,
307
                record => $record,
308
                filter => $options->{'context'},
308
                context => $options->{'context'},
309
            }
309
            }
310
        );
310
        );
311
    }
311
    }
Lines 3567-3579 sub RemoveAllNsb { Link Here
3567
    return $record;
3567
    return $record;
3568
}
3568
}
3569
3569
3570
=head2 ApplyMarcPermissions
3570
=head2 ApplyMarcMergeRules
3571
3571
3572
    my $record = ApplyMarcPermissions($arguments)
3572
    my $record = ApplyMarcMergeRules($params)
3573
3573
3574
Applies marc permission rules to a record.
3574
Applies marc merge rules to a record.
3575
3575
3576
C<$arguments> is expected to be a hashref with below keys defined.
3576
C<$params> is expected to be a hashref with below keys defined.
3577
3577
3578
=over 4
3578
=over 4
3579
3579
Lines 3581-3606 C<$arguments> is expected to be a hashref with below keys defined. Link Here
3581
biblionumber of old record
3581
biblionumber of old record
3582
3582
3583
=item C<record>
3583
=item C<record>
3584
record that will modify old record
3584
Incoming record that will be merged with old record
3585
3585
3586
=item C<frameworkcode>
3586
=item C<context>
3587
only tags included in framework will be processed
3587
hashref containing at least one module from marc_merge_rules_modules table
3588
3588
with filter value on the form {module => filter, ...}. Three predefined
3589
=item C<filter>
3589
context modules exists:
3590
hashref containing at least one filter module from the marc_permissions_modules
3591
table in form {module => filter}. Three predefined filter modules exists:
3592
3590
3593
    * source
3591
    * source
3594
    * category
3592
    * category
3595
    * borrower
3593
    * borrower
3596
3594
3597
=item C<log>
3598
optional reference to array that will be filled with rule evaluation log
3599
entries.
3600
3601
=item C<nolog>
3602
optional boolean which when true disables logging to action log.
3603
3604
=back
3595
=back
3605
3596
3606
Returns:
3597
Returns:
Lines 3609-4153 Returns: Link Here
3609
3600
3610
=item C<$record>
3601
=item C<$record>
3611
3602
3612
new MARC record based on C<record> with C<filter> applied. If no old
3603
Merged MARC record based with merge rules for C<context> applied. If no old
3613
record for C<biblionumber> can be found, C<record> is returned unchanged.
3604
record for C<biblionumber> can be found, C<record> is returned unchanged.
3614
Default action when no matching filter found is to leave old record unchanged.
3605
Default action when no matching context is found to return C<record> unchanged.
3606
If no rules are found for a certain field tag the default is to overwrite with
3607
fields with this field tag from C<record>.
3615
3608
3616
=back
3609
=back
3617
3610
3618
=cut
3611
=cut
3619
3612
3620
sub ApplyMarcPermissions {
3613
sub ApplyMarcMergeRules {
3621
    my ($arguments) = @_;
3614
    my ($params) = @_;
3622
    my $biblionumber = $arguments->{biblionumber};
3615
    my $biblionumber = $params->{biblionumber};
3623
    my $incoming_record = $arguments->{record};
3616
    my $incoming_record = $params->{record};
3624
3617
3625
    if ( !$biblionumber ) {
3618
    if (!$biblionumber) {
3626
        carp 'ApplyMarcPermissions called on undefined biblionumber';
3619
        carp 'ApplyMarcMergeRules called on undefined biblionumber';
3627
        return;
3620
        return;
3628
    }
3621
    }
3629
    if ( !$incoming_record ) {
3622
    if (!$incoming_record) {
3630
        carp 'ApplyMarcPermissions called on undefined record';
3623
        carp 'ApplyMarcMergeRules called on undefined record';
3631
        return;
3624
        return;
3632
    }
3625
    }
3633
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3626
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3634
3627
3635
    my $merge_rules = undef;
3628
    # Skip merge rules if called with no context
3636
    if ($old_record && $arguments->{filter} && ($merge_rules = GetMarcPermissions($arguments->{filter}))) {
3629
    if ($old_record && defined $params->{context}) {
3637
        return MergeRecords($old_record, $incoming_record, $merge_rules);
3630
        return Koha::MarcMergeRules->merge_records($old_record, $incoming_record, $params->{context});
3638
    }
3631
    }
3639
    return $incoming_record;
3632
    return $incoming_record;
3640
}
3633
}
3641
3634
3642
sub MergeRecords {
3643
    my ($old_record, $incoming_record, $merge_rules) = @_;
3644
    my $is_matching_regex = sub {
3645
        my ( $tag, $m ) = @_;
3646
3647
        # tag is not exactly same as possible regex
3648
        $tag ne $m &&
3649
3650
        # wildcard
3651
        $m ne '*' &&
3652
3653
        # valid tagDataType
3654
        $m !~ /^(0[1-9A-z][\dA-Z]) |
3655
        ([1-9A-z][\dA-z]{2})$/x &&
3656
3657
        # nor valid controltagDataType
3658
        $m !~ /00[1-9A-Za-z]{1}/ &&
3659
3660
        # so we try it as a regex
3661
        $tag =~ /^$m$/
3662
    };
3663
3664
    my $fields_by_tag = sub {
3665
        my ($record) = @_;
3666
        my $fields = {};
3667
        foreach my $field ($record->fields()) {
3668
            $fields->{$field->tag()} //= [];
3669
            push @{$fields->{$field->tag()}}, $field;
3670
        }
3671
        return $fields;
3672
    };
3673
3674
    my $hash_field_data = sub {
3675
        my ($field) = @_;
3676
        my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
3677
        return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields());
3678
    };
3679
3680
    my $diff_by_key = sub {
3681
        my ($a, $b) = @_;
3682
        my @removed;
3683
        my @intersecting;
3684
        my @added;
3685
        my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
3686
        foreach my $key (keys %keys_index) {
3687
            if ($a->{$key} && $b->{$key}) {
3688
                push @intersecting, $a->{$key};
3689
            }
3690
            elsif ($a->{$key}) {
3691
                push @removed, $a->{$key};
3692
            }
3693
            else {
3694
                push @added, $b->{$key};
3695
            }
3696
        }
3697
        return (\@removed, \@intersecting, \@added);
3698
    };
3699
3700
    my $get_matching_field_rule = sub {
3701
        my ($tag) = @_;
3702
        my $matched_rule = undef;
3703
        # Exact match takes precedence
3704
        if (exists $merge_rules->{$tag}) {
3705
            $matched_rule = $merge_rules->{$tag};
3706
        }
3707
        else {
3708
            # TODO: sorty by module/weight rule id or something, grab first matching via listutils thingy
3709
            my @matching_rules = map { $merge_rules->{$_} } grep { $is_matching_regex->($tag, $_) } sort keys %{$merge_rules};
3710
            # TODO: fix
3711
            if (@matching_rules) {
3712
                $matched_rule = pop @matching_rules;
3713
            }
3714
            elsif($merge_rules->{'*'}) {
3715
                $matched_rule = $merge_rules->{'*'};
3716
            }
3717
        }
3718
        return $matched_rule;
3719
    };
3720
3721
    my $merged_record = MARC::Record->new();
3722
    my @merged_record_fields;
3723
3724
    # Leader is always overwritten, or kept???
3725
    $merged_record->leader($incoming_record->leader());
3726
3727
    my $current_fields = $fields_by_tag->($old_record);
3728
    my $incoming_fields = $fields_by_tag->($incoming_record);
3729
3730
    # First we get all new incoming control fields
3731
    my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
3732
3733
    foreach my $tag (@new_field_tags) {
3734
        my $rule = $get_matching_field_rule->($tag) // {
3735
            on_new => {'action' => 'skip', 'rule' => 0}
3736
        };
3737
        if (
3738
            $rule->{on_new}->{action} eq 'add' ||
3739
            $rule->{on_new}->{action} eq 'overwrite' # ???
3740
        ) { # Or could just be write/protect?
3741
            # Hmm, only one control field possible??
3742
            push @merged_record_fields, @{$incoming_fields->{$tag}};
3743
        }
3744
    }
3745
3746
    # Then we get all control fields no longer present in incoming fields
3747
    # (removed)
3748
    my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
3749
    foreach my $tag (@deleted_field_tags) {
3750
        my $rule = $get_matching_field_rule->($tag) // {
3751
            on_deleted => {'action' => 'skip', 'rule' => 0}
3752
        };
3753
        if ($rule->{on_deleted}->{action} eq 'skip') {
3754
            push @merged_record_fields, @{$current_fields->{$tag}};
3755
        }
3756
    }
3757
3758
    # Then we get the intersection of control fields, present both in
3759
    # current and incoming record (possibly to be overwritten)
3760
    my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
3761
    foreach my $tag (@common_field_tags) {
3762
        # Is control field
3763
        my $rule = $get_matching_field_rule->($tag) // {
3764
            on_removed => {'action' => 'skip', 'rule' => 0},
3765
            on_appended => {'action' => 'skip', 'rule' => 0}
3766
        };
3767
        if ($tag < 10) {
3768
            # on_existing = on_match
3769
            if ($rule->{on_appended}->{action} eq 'skip') { # TODO: replace with "protect", "keep"
3770
                push @merged_record_fields, @{$current_fields->{$tag}};
3771
            }
3772
            elsif ($rule->{on_appended}->{action} eq 'append') {
3773
                push @merged_record_fields, @{$incoming_fields->{$tag}};
3774
            }
3775
            if (
3776
                $rule->{on_appended}->{action} eq 'append' &&
3777
                $rule->{on_removed}->{action} eq 'skip'
3778
            ) {
3779
                #TODO: This is an invalid combination for control fields, warn!!
3780
                # Or should perform client/server side validation to prevent this choice
3781
            }
3782
        }
3783
        else {
3784
            # Compute intersection and diff using field data
3785
            my %current_fields_by_data = map { $hash_field_data->($_) => $_ } @{$current_fields->{$tag}};
3786
            my %incoming_fields_by_data = map { $hash_field_data->($_) => $_ } @{$incoming_fields->{$tag}};
3787
            my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
3788
3789
            # First add common fields (intersection)
3790
            # Unchanged
3791
            if (@{$common_fields}) {
3792
                push @merged_record_fields, @{$common_fields};
3793
            }
3794
            # Removed
3795
            if (@{$current_fields_only}) {
3796
                if ($rule->{on_removed}->{action} eq 'skip') {
3797
                    push @merged_record_fields, @{$current_fields_only};
3798
                }
3799
            }
3800
            # Appended
3801
            if (@{$incoming_fields_only}) {
3802
                if ($rule->{on_appended}->{action} eq 'append') {
3803
                    push @merged_record_fields, @{$incoming_fields_only};
3804
                }
3805
            }
3806
        }
3807
    }
3808
    if ($#merged_record_fields != 0) {
3809
        $merged_record->insert_fields_ordered(@merged_record_fields);
3810
    }
3811
    return $merged_record;
3812
}
3813
3814
=head2 GetMarcPermissions
3815
3816
    my $marc_permissions = GetMarcPermissions()
3817
3818
Loads MARC field permissions from the marc_permissions table.
3819
3820
Returns:
3821
3822
=over 4
3823
3824
=item C<$marc_permissions>
3825
3826
hashref with permissions structure for use with GetMarcPermissionsAction.
3827
3828
=back
3829
3830
=cut
3831
3832
sub GetMarcPermissions {
3833
    my ($filter) = @_;
3834
    my $dbh = C4::Context->dbh;
3835
    my $rule_count = 0;
3836
    my $modules = GetMarcPermissionsModules();
3837
    # We only care about modules included in the context/filter
3838
    # TODO: Perhaps make sure source => '*' is default?
3839
    my @filter_modules = grep { exists $filter->{$_->{name}} } @{$modules};
3840
3841
    my $cache = Koha::Caches->get_instance();
3842
    my $permissions = $cache->get_from_cache('marc_permissions', { unsafe => 1 });
3843
3844
    if (!$permissions) {
3845
        my $query = '
3846
            SELECT `marc_permissions`.*,
3847
                `marc_permissions_modules`.`name`,
3848
                `marc_permissions_modules`.`description`,
3849
                `marc_permissions_modules`.`specificity`
3850
                FROM `marc_permissions`
3851
                LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id`
3852
                ORDER BY `marc_permissions_modules`.`specificity`, `id`
3853
        ';
3854
        my $sth = $dbh->prepare($query);
3855
        $sth->execute();
3856
        while (my $perm = $sth->fetchrow_hashref) {
3857
            my $target = ($permissions->{$perm->{name}}->{$perm->{filter}}->{$perm->{tagfield}} //= {});
3858
            foreach my $event (GetMarcPermissionEvents()) {
3859
                $target->{$event} = { action => $perm->{$event}, rule => $perm->{'id'} };
3860
            }
3861
        }
3862
        $cache->set_in_cache('marc_permissions', $permissions);
3863
    }
3864
3865
    my $filtered_permissions = undef;
3866
    foreach my $module (@filter_modules) {
3867
        if (
3868
            exists $permissions->{$module->{name}} &&
3869
            exists $permissions->{$module->{name}}->{$filter->{$module->{name}}}
3870
        ) {
3871
            # TODO: Support multiple overlapping filters/context??
3872
            $filtered_permissions = $permissions->{$module->{name}}->{$filter->{$module->{name}}};
3873
            last;
3874
        }
3875
    }
3876
    if (!$filtered_permissions) {
3877
        # No perms matching specific context conditions found, try wildcard value for each active context
3878
        foreach my $module (@filter_modules) {
3879
            if (exists $permissions->{$module->{name}}->{'*'}) {
3880
                $filtered_permissions = $permissions->{$module->{name}}->{'*'};
3881
                last;
3882
            }
3883
        }
3884
    }
3885
    return $filtered_permissions;
3886
}
3887
3888
# TODO: Use this in GetMarcPermissions
3889
=head2 GetMarcPermissionsRules
3890
3891
    my $rules = GetMarcPermissionsRules()
3892
3893
Returns:
3894
3895
=over 4
3896
3897
=item C<$rules>
3898
3899
array (in list context, arrayref otherwise) of hashrefs from marc_permissions
3900
table in order of module specificity and rule id.
3901
3902
=back
3903
3904
=cut
3905
3906
sub GetMarcPermissionsRules {
3907
    my $dbh = C4::Context->dbh;
3908
    my @rules = ();
3909
3910
    my $query = '
3911
    SELECT `marc_permissions`.`id`,
3912
           `marc_permissions`.`tagfield`,
3913
           `marc_permissions`.`filter`,
3914
           `marc_permissions`.`on_new`,
3915
           `marc_permissions`.`on_appended`,
3916
           `marc_permissions`.`on_removed`,
3917
           `marc_permissions`.`on_deleted`,
3918
           `marc_permissions_modules`.`name` as  `module`,
3919
           `marc_permissions_modules`.`description`,
3920
           `marc_permissions_modules`.`specificity`
3921
    FROM `marc_permissions`
3922
    LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id`
3923
    ORDER BY `marc_permissions_modules`.`specificity`, `id`
3924
    ';
3925
    my $sth = $dbh->prepare($query);
3926
    $sth->execute();
3927
    while ( my $row = $sth->fetchrow_hashref ) {
3928
        push(@rules, $row);
3929
    }
3930
3931
    return wantarray ? @rules : \@rules;
3932
}
3933
3934
=head2 GetMarcPermissionsModules
3935
3936
    my $modules = GetMarcPermissionsModules()
3937
3938
Returns:
3939
3940
=over 4
3941
3942
=item C<$modules>
3943
3944
array (in list context, arrayref otherwise) of hashrefs from
3945
marc_permissions_modules table in order of specificity.
3946
3947
=back
3948
3949
=cut
3950
3951
sub GetMarcPermissionsModules {
3952
    my $dbh = C4::Context->dbh;
3953
    my @modules = ();
3954
3955
    my $query = '
3956
    SELECT *
3957
    FROM `marc_permissions_modules`
3958
    ORDER BY `specificity` DESC
3959
    ';
3960
    my $sth = $dbh->prepare($query);
3961
    $sth->execute();
3962
    while ( my $row = $sth->fetchrow_hashref ) {
3963
        push(@modules, $row);
3964
    }
3965
3966
    return wantarray ? @modules : \@modules;
3967
}
3968
3969
sub GetMarcPermissionEvents {
3970
    return ('on_new', 'on_appended', 'on_removed', 'on_deleted');
3971
}
3972
3973
=head2 ModMarcPermissionsRule
3974
3975
    my $success = ModMarcPermissionsRule($id, $fields)
3976
3977
Modifies rule in the marc_permissions table.
3978
3979
=over 4
3980
3981
=item C<$id>
3982
3983
rule id to modify
3984
3985
=item C<$fields>
3986
3987
hashref defining the table fields
3988
3989
      * tagfield - required
3990
      * module - required
3991
      * filter - required
3992
      * on_new - required
3993
      * on_appended - required
3994
      * on_removed - required
3995
      * on_deleted - required
3996
3997
=back
3998
3999
Returns:
4000
4001
=over 4
4002
4003
=item C<$success>
4004
4005
undef if an error occurs, otherwise true.
4006
4007
=back
4008
4009
=cut
4010
4011
sub ModMarcPermissionsRule {
4012
    my ($id, $f) = @_;
4013
    my $dbh = C4::Context->dbh;
4014
4015
    my $query = '
4016
    UPDATE `marc_permissions`
4017
    SET
4018
      tagfield = ?,
4019
      module = ?,
4020
      filter = ?,
4021
      on_new = ?,
4022
      on_appended = ?,
4023
      on_removed = ?,
4024
      on_deleted = ?
4025
    WHERE
4026
      id = ?
4027
    ';
4028
    my $sth = $dbh->prepare($query);
4029
    my $result = $sth->execute (
4030
        $f->{tagfield},
4031
        $f->{module},
4032
        $f->{filter},
4033
        $f->{on_new},
4034
        $f->{on_appended},
4035
        $f->{on_removed},
4036
        $f->{on_deleted},
4037
        $id
4038
    );
4039
    ClearMarcPermissionsRulesCache();
4040
    return $result;
4041
}
4042
4043
sub ClearMarcPermissionsRulesCache {
4044
    my $cache = Koha::Caches->get_instance();
4045
    $cache->clear_from_cache('marc_permissions');
4046
}
4047
4048
=head2 AddMarcPermissionsRule
4049
4050
    my $success = AddMarcPermissionsRule($fields)
4051
4052
Add rule to the marc_permissions table.
4053
4054
=over 4
4055
4056
=item C<$fields>
4057
4058
hashref defining the table fields
4059
4060
      tagfield - required
4061
      module - required
4062
      filter - required
4063
      on_new - required
4064
      on_appended - required
4065
      on_removed - required
4066
      on_deleted - required
4067
4068
=back
4069
4070
Returns:
4071
4072
=over 4
4073
4074
=item C<$success>
4075
4076
undef if an error occurs, otherwise true.
4077
4078
=back
4079
4080
=cut
4081
4082
sub AddMarcPermissionsRule {
4083
    my $f = shift;
4084
    my $dbh = C4::Context->dbh;
4085
    my $query = '
4086
    INSERT INTO `marc_permissions`
4087
    (
4088
      tagfield,
4089
      module,
4090
      filter,
4091
      on_new,
4092
      on_appended,
4093
      on_removed,
4094
      on_deleted
4095
    )
4096
    VALUES (?, ?, ?, ?, ?, ?, ?)
4097
    ';
4098
    my $sth = $dbh->prepare($query);
4099
    my $result = $sth->execute (
4100
        $f->{tagfield},
4101
        $f->{module},
4102
        $f->{filter},
4103
        $f->{on_new},
4104
        $f->{on_appended},
4105
        $f->{on_removed},
4106
        $f->{on_deleted}
4107
    );
4108
    ClearMarcPermissionsRulesCache();
4109
    return $result;
4110
}
4111
4112
=head2 DelMarcPermissionsRule
4113
4114
    my $success = DelMarcPermissionsRule($id)
4115
4116
Deletes rule from the marc_permissions table.
4117
4118
=over 4
4119
4120
=item C<$id>
4121
4122
rule id to delete
4123
4124
=back
4125
4126
Returns:
4127
4128
=over 4
4129
4130
=item C<$success>
4131
4132
undef if an error occurs, otherwise true.
4133
4134
=back
4135
4136
=cut
4137
4138
sub DelMarcPermissionsRule {
4139
    my $id = shift;
4140
    my $dbh = C4::Context->dbh;
4141
    my $query = '
4142
    DELETE FROM `marc_permissions`
4143
    WHERE
4144
      id = ?
4145
    ';
4146
    my $sth = $dbh->prepare($query);
4147
    my $result = $sth->execute($id);
4148
    ClearMarcPermissionsRulesCache();
4149
    return $result;
4150
}
4151
1;
3635
1;
4152
3636
4153
__END__
3637
__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 47-54 my $manual_url = Koha::Manual::get_url($refer, $language); Link Here
47
47
48
print $query->redirect($manual_url);
48
print $query->redirect($manual_url);
49
49
50
my $rules = GetMarcPermissionsRules();
50
my $rules = GetMarcMergeRules();
51
my $modules = GetMarcPermissionsModules();
51
my $modules = GetMarcMergeRulesModules();
52
$template->param( rules => $rules, modules => $modules );
52
$template->param( rules => $rules, modules => $modules );
53
53
54
output_html_with_http_headers $query, "", $template->output;
54
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 282-289 Cataloging: Link Here
282
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
282
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
283
        -
283
        -
284
            - When importing records
284
            - When importing records
285
            - pref: MARCPermissions
285
            - pref: MARCMergeRules
286
              choices:
286
              choices:
287
                  yes: "use"
287
                  yes: "use"
288
                  no: "don't use"
288
                  no: "don't use"
289
            - MARC permissions rules to decide which action to take for each field.
289
            - 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