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

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

Return to bug 14957