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

(-)a/C4/Biblio.pm (-8 / +611 lines)
Lines 29-34 use MARC::File::USMARC; Link Here
29
use MARC::File::XML;
29
use MARC::File::XML;
30
use POSIX qw(strftime);
30
use POSIX qw(strftime);
31
use Module::Load::Conditional qw(can_load);
31
use Module::Load::Conditional qw(can_load);
32
use String::Similarity;
33
use Digest::MD5 qw(md5_base64);
32
34
33
use C4::Koha;
35
use C4::Koha;
34
use C4::Log;    # logaction
36
use C4::Log;    # logaction
Lines 102-107 BEGIN { Link Here
102
104
103
      &CountItemsIssued
105
      &CountItemsIssued
104
      &CountBiblioInOrders
106
      &CountBiblioInOrders
107
108
      &GetMarcPermissionsRules
109
      &GetMarcPermissionsModules
110
      &ModMarcPermissionsRule
111
      &AddMarcPermissionsRule
112
      &DelMarcPermissionsRule
105
    );
113
    );
106
114
107
    # To modify something
115
    # To modify something
Lines 129-134 BEGIN { Link Here
129
    # they are useful in a few circumstances, so they are exported,
137
    # they are useful in a few circumstances, so they are exported,
130
    # but don't use them unless you are a core developer ;-)
138
    # but don't use them unless you are a core developer ;-)
131
    push @EXPORT, qw(
139
    push @EXPORT, qw(
140
      &ApplyMarcPermissions
132
      &ModBiblioMarc
141
      &ModBiblioMarc
133
    );
142
    );
134
143
Lines 266-272 sub AddBiblio { Link Here
266
275
267
=head2 ModBiblio
276
=head2 ModBiblio
268
277
269
  ModBiblio( $record,$biblionumber,$frameworkcode);
278
  ModBiblio($record, $biblionumber, $frameworkcode, $options);
270
279
271
Replace an existing bib record identified by C<$biblionumber>
280
Replace an existing bib record identified by C<$biblionumber>
272
with one supplied by the MARC::Record object C<$record>.  The embedded
281
with one supplied by the MARC::Record object C<$record>.  The embedded
Lines 287-293 Returns 1 on success 0 on failure Link Here
287
=cut
296
=cut
288
297
289
sub ModBiblio {
298
sub ModBiblio {
290
    my ( $record, $biblionumber, $frameworkcode ) = @_;
299
    my ( $record, $biblionumber, $frameworkcode, $options ) = @_;
300
    $options //= {};
301
291
    if (!$record) {
302
    if (!$record) {
292
        carp 'No record passed to ModBiblio';
303
        carp 'No record passed to ModBiblio';
293
        return 0;
304
        return 0;
Lines 315-320 sub ModBiblio { Link Here
315
326
316
    _strip_item_fields($record, $frameworkcode);
327
    _strip_item_fields($record, $frameworkcode);
317
328
329
    # apply permissions
330
    if (C4::Context->preference('MARCPermissions') && $biblionumber && defined $options && exists $options->{'context'}) {
331
        $record = ApplyMarcPermissions({
332
                biblionumber => $biblionumber,
333
                record => $record,
334
                filter => $options->{'context'},
335
            }
336
        );
337
    }
338
318
    # update biblionumber and biblioitemnumber in MARC
339
    # update biblionumber and biblioitemnumber in MARC
319
    # FIXME - this is assuming a 1 to 1 relationship between
340
    # FIXME - this is assuming a 1 to 1 relationship between
320
    # biblios and biblioitems
341
    # biblios and biblioitems
Lines 325-337 sub ModBiblio { Link Here
325
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
346
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
326
347
327
    # load the koha-table data object
348
    # load the koha-table data object
328
    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode );
349
    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode, undef, $biblionumber);
329
350
330
    # update MARC subfield that stores biblioitems.cn_sort
351
    # update MARC subfield that stores biblioitems.cn_sort
331
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
352
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
332
353
333
    # update the MARC record (that now contains biblio and items) with the new record data
354
    # update the MARC record (that now contains biblio and items) with the new record data
334
    &ModBiblioMarc( $record, $biblionumber, $frameworkcode );
355
    ModBiblioMarc( $record, $biblionumber, $frameworkcode );
335
356
336
    # modify the other koha tables
357
    # modify the other koha tables
337
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
358
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
Lines 2573-2579 sub TransformHtmlToMarc { Link Here
2573
2594
2574
=head2 TransformMarcToKoha
2595
=head2 TransformMarcToKoha
2575
2596
2576
    $result = TransformMarcToKoha( $record, undef, $limit )
2597
    $result = TransformMarcToKoha($record, undef, $biblionumber, $option)
2577
2598
2578
Extract data from a MARC bib record into a hashref representing
2599
Extract data from a MARC bib record into a hashref representing
2579
Koha biblio, biblioitems, and items fields.
2600
Koha biblio, biblioitems, and items fields.
Lines 2584-2590 hash_ref. Link Here
2584
=cut
2605
=cut
2585
2606
2586
sub TransformMarcToKoha {
2607
sub TransformMarcToKoha {
2587
    my ( $record, $frameworkcode, $limit_table ) = @_;
2608
    my ( $record, $frameworkcode, $limit_table, $biblionumber ) = @_;
2588
    # FIXME  Parameter $frameworkcode is obsolete and will be removed
2609
    # FIXME  Parameter $frameworkcode is obsolete and will be removed
2589
    $limit_table //= q{};
2610
    $limit_table //= q{};
2590
2611
Lines 3271-3277 sub _koha_delete_biblio_metadata { Link Here
3271
3292
3272
=head2 ModBiblioMarc
3293
=head2 ModBiblioMarc
3273
3294
3274
  &ModBiblioMarc($newrec,$biblionumber,$frameworkcode);
3295
  &ModBiblioMarc($newrec, $biblionumber, $frameworkcode, $options);
3275
3296
3276
Add MARC XML data for a biblio to koha
3297
Add MARC XML data for a biblio to koha
3277
3298
Lines 3283-3288 sub ModBiblioMarc { Link Here
3283
    # pass the MARC::Record to this function, and it will create the records in
3304
    # pass the MARC::Record to this function, and it will create the records in
3284
    # the marcxml field
3305
    # the marcxml field
3285
    my ( $record, $biblionumber, $frameworkcode ) = @_;
3306
    my ( $record, $biblionumber, $frameworkcode ) = @_;
3307
3286
    if ( !$record ) {
3308
    if ( !$record ) {
3287
        carp 'ModBiblioMarc passed an undefined record';
3309
        carp 'ModBiblioMarc passed an undefined record';
3288
        return;
3310
        return;
Lines 3295-3300 sub ModBiblioMarc { Link Here
3295
    if ( !$frameworkcode ) {
3317
    if ( !$frameworkcode ) {
3296
        $frameworkcode = "";
3318
        $frameworkcode = "";
3297
    }
3319
    }
3320
3298
    my $sth = $dbh->prepare("UPDATE biblio SET frameworkcode=? WHERE biblionumber=?");
3321
    my $sth = $dbh->prepare("UPDATE biblio SET frameworkcode=? WHERE biblionumber=?");
3299
    $sth->execute( $frameworkcode, $biblionumber );
3322
    $sth->execute( $frameworkcode, $biblionumber );
3300
    $sth->finish;
3323
    $sth->finish;
Lines 3596-3603 sub RemoveAllNsb { Link Here
3596
    return $record;
3619
    return $record;
3597
}
3620
}
3598
3621
3599
1;
3622
=head2 ApplyMarcPermissions
3623
3624
    my $record = ApplyMarcPermissions($arguments)
3625
3626
Applies marc permission rules to a record.
3627
3628
C<$arguments> is expected to be a hashref with below keys defined.
3629
3630
=over 4
3631
3632
=item C<biblionumber>
3633
biblionumber of old record
3634
3635
=item C<record>
3636
record that will modify old record
3637
3638
=item C<frameworkcode>
3639
only tags included in framework will be processed
3640
3641
=item C<filter>
3642
hashref containing at least one filter module from the marc_permissions_modules
3643
table in form {module => filter}. Three predefined filter modules exists:
3644
3645
    * source
3646
    * category
3647
    * borrower
3648
3649
=item C<log>
3650
optional reference to array that will be filled with rule evaluation log
3651
entries.
3652
3653
=item C<nolog>
3654
optional boolean which when true disables logging to action log.
3655
3656
=back
3657
3658
Returns:
3659
3660
=over 4
3661
3662
=item C<$record>
3663
3664
new MARC record based on C<record> with C<filter> applied. If no old
3665
record for C<biblionumber> can be found, C<record> is returned unchanged.
3666
Default action when no matching filter found is to leave old record unchanged.
3667
3668
=back
3669
3670
=cut
3671
3672
sub ApplyMarcPermissions {
3673
    my ($arguments) = @_;
3674
    my $biblionumber = $arguments->{biblionumber};
3675
    my $incoming_record = $arguments->{record};
3676
3677
    if ( !$biblionumber ) {
3678
        carp 'ApplyMarcPermissions called on undefined biblionumber';
3679
        return;
3680
    }
3681
    if ( !$incoming_record ) {
3682
        carp 'ApplyMarcPermissions called on undefined record';
3683
        return;
3684
    }
3685
    my $old_record = GetMarcBiblio({ biblionumber => $biblionumber });
3686
3687
    my $merge_rules = undef;
3688
    if ($old_record && $arguments->{filter} && ($merge_rules = GetMarcPermissions($arguments->{filter}))) {
3689
        return MergeRecords($old_record, $incoming_record, $merge_rules);
3690
    }
3691
    return $incoming_record;
3692
}
3693
3694
sub MergeRecords {
3695
    my ($old_record, $incoming_record, $merge_rules) = @_;
3696
    my $is_matching_regex = sub {
3697
        my ( $tag, $m ) = @_;
3698
3699
        # tag is not exactly same as possible regex
3700
        $tag ne $m &&
3701
3702
        # wildcard
3703
        $m ne '*' &&
3704
3705
        # valid tagDataType
3706
        $m !~ /^(0[1-9A-z][\dA-Z]) |
3707
        ([1-9A-z][\dA-z]{2})$/x &&
3708
3709
        # nor valid controltagDataType
3710
        $m !~ /00[1-9A-Za-z]{1}/ &&
3711
3712
        # so we try it as a regex
3713
        $tag =~ /^$m$/
3714
    };
3715
3716
    my $fields_by_tag = sub {
3717
        my ($record) = @_;
3718
        my $fields = {};
3719
        foreach my $field ($record->fields()) {
3720
            $fields->{$field->tag()} //= [];
3721
            push @{$fields->{$field->tag()}}, $field;
3722
        }
3723
        return $fields;
3724
    };
3725
3726
    my $hash_field_data = sub {
3727
        my ($field) = @_;
3728
        my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2));
3729
        return md5_base64($indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields()));
3730
    };
3731
3732
    my $diff_by_key = sub {
3733
        my ($a, $b) = @_;
3734
        my @removed;
3735
        my @intersecting;
3736
        my @added;
3737
        my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b});
3738
        foreach my $key (keys %keys_index) {
3739
            if ($a->{$key} && $b->{$key}) {
3740
                push @intersecting, $a->{$key};
3741
            }
3742
            elsif ($a->{$key}) {
3743
                push @removed, $a->{$key};
3744
            }
3745
            else {
3746
                push @added, $b->{$key};
3747
            }
3748
        }
3749
        return (\@removed, \@intersecting, \@added);
3750
    };
3751
3752
    my $get_matching_field_rule = sub {
3753
        my ($tag) = @_;
3754
        my $matched_rule = undef;
3755
        # Exact match takes precedence
3756
        if (exists $merge_rules->{$tag}) {
3757
            $matched_rule = $merge_rules->{$tag};
3758
        }
3759
        else {
3760
            # TODO: sorty by module/weight rule id or something, grab first matching via listutils thingy
3761
            my @matching_rules = map { $merge_rules->{$_} } grep { $is_matching_regex->($tag, $_) } sort keys %{$merge_rules};
3762
            # TODO: fix
3763
            if (@matching_rules) {
3764
                $matched_rule = pop @matching_rules;
3765
            }
3766
            elsif($merge_rules->{'*'}) {
3767
                $matched_rule = $merge_rules->{'*'};
3768
            }
3769
        }
3770
        return $matched_rule;
3771
    };
3772
3773
    my $merged_record = MARC::Record->new();
3774
    my @merged_record_fields;
3775
3776
    # Leader is always overwritten, or kept???
3777
    $merged_record->leader($incoming_record->leader());
3778
3779
    my $current_fields = $fields_by_tag->($old_record);
3780
    my $incoming_fields = $fields_by_tag->($incoming_record);
3781
3782
    # First we get all new incoming control fields
3783
    my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields};
3784
3785
    foreach my $tag (@new_field_tags) {
3786
        my $rule = $get_matching_field_rule->($tag) // {
3787
            on_new => {'action' => 'skip', 'rule' => 0}
3788
        };
3789
        if (
3790
            $rule->{on_new}->{action} eq 'add' ||
3791
            $rule->{on_new}->{action} eq 'overwrite' # ???
3792
        ) { # Or could just be write/protect?
3793
            # Hmm, only one control field possible??
3794
            push @merged_record_fields, @{$incoming_fields->{$tag}};
3795
        }
3796
    }
3797
3798
    # Then we get all control fields no longer present in incoming fields
3799
    # (removed)
3800
    my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields};
3801
    foreach my $tag (@deleted_field_tags) {
3802
        my $rule = $get_matching_field_rule->($tag) // {
3803
            on_deleted => {'action' => 'skip', 'rule' => 0}
3804
        };
3805
        if ($rule->{on_deleted}->{action} eq 'skip') {
3806
            push @merged_record_fields, @{$current_fields->{$tag}};
3807
        }
3808
    }
3809
3810
    # Then we get the intersection of control fields, present both in
3811
    # current and incoming record (possibly to be overwritten)
3812
    my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields};
3813
    foreach my $tag (@common_field_tags) {
3814
        # Is control field
3815
        my $rule = $get_matching_field_rule->($tag) // {
3816
            on_removed => {'action' => 'skip', 'rule' => 0},
3817
            on_appended => {'action' => 'skip', 'rule' => 0}
3818
        };
3819
        if ($tag < 10) {
3820
            # on_existing = on_match
3821
            if ($rule->{on_appended}->{action} eq 'skip') { # TODO: replace with "protect", "keep"
3822
                push @merged_record_fields, @{$current_fields->{$tag}};
3823
            }
3824
            elsif ($rule->{on_appended}->{action} eq 'append') {
3825
                push @merged_record_fields, @{$incoming_fields->{$tag}};
3826
            }
3827
            if (
3828
                $rule->{on_appended}->{action} eq 'append' &&
3829
                $rule->{on_removed}->{action} eq 'skip'
3830
            ) {
3831
                #TODO: This is an invalid combination for control fields, warn!!
3832
                # Or should perform client/server side validation to prevent this choice
3833
            }
3834
        }
3835
        else {
3836
            # Compute intersection and diff using field data
3837
            my %current_fields_by_data = map { $hash_field_data->($_) => $_ } @{$current_fields->{$tag}};
3838
            my %incoming_fields_by_data = map { $hash_field_data->($_) => $_ } @{$incoming_fields->{$tag}};
3839
            my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data);
3840
3841
            # First add common fields (intersection)
3842
            # Unchanged
3843
            if (@{$common_fields}) {
3844
                push @merged_record_fields, @{$common_fields};
3845
            }
3846
            # Removed
3847
            if (@{$current_fields_only}) {
3848
                if ($rule->{on_removed}->{action} eq 'skip') {
3849
                    push @merged_record_fields, @{$current_fields_only};
3850
                }
3851
            }
3852
            # Appended
3853
            if (@{$incoming_fields_only}) {
3854
                if ($rule->{on_appended}->{action} eq 'append') {
3855
                    push @merged_record_fields, @{$incoming_fields_only};
3856
                }
3857
            }
3858
        }
3859
    }
3860
    if ($#merged_record_fields != 0) {
3861
        $merged_record->insert_fields_ordered(@merged_record_fields);
3862
    }
3863
    return $merged_record;
3864
}
3865
3866
=head2 GetMarcPermissions
3867
3868
    my $marc_permissions = GetMarcPermissions()
3869
3870
Loads MARC field permissions from the marc_permissions table.
3871
3872
Returns:
3873
3874
=over 4
3875
3876
=item C<$marc_permissions>
3877
3878
hashref with permissions structure for use with GetMarcPermissionsAction.
3879
3880
=back
3881
3882
=cut
3883
3884
sub GetMarcPermissions {
3885
    my ($filter) = @_;
3886
    my $dbh = C4::Context->dbh;
3887
    my $rule_count = 0;
3888
    my $modules = GetMarcPermissionsModules();
3889
    # We only care about modules included in the context/filter
3890
    # TODO: Perhaps make sure source => '*' is default?
3891
    my @filter_modules = grep { exists $filter->{$_->{name}} } @{$modules};
3892
3893
    my $cache = Koha::Caches->get_instance();
3894
    my $permissions = $cache->get_from_cache('marc_permissions', { unsafe => 1 });
3895
3896
    if (!$permissions) {
3897
        my $query = '
3898
            SELECT `marc_permissions`.*,
3899
                `marc_permissions_modules`.`name`,
3900
                `marc_permissions_modules`.`description`,
3901
                `marc_permissions_modules`.`specificity`
3902
                FROM `marc_permissions`
3903
                LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id`
3904
                ORDER BY `marc_permissions_modules`.`specificity`, `id`
3905
        ';
3906
        my $sth = $dbh->prepare($query);
3907
        $sth->execute();
3908
        while (my $perm = $sth->fetchrow_hashref) {
3909
            my $target = ($permissions->{$perm->{name}}->{$perm->{filter}}->{$perm->{tagfield}} //= {});
3910
            foreach my $event (GetMarcPermissionEvents()) {
3911
                $target->{$event} = { action => $perm->{$event}, rule => $perm->{'id'} };
3912
            }
3913
        }
3914
        $cache->set_in_cache('marc_permissions', $permissions);
3915
    }
3916
3917
    my $filtered_permissions = undef;
3918
    foreach my $module (@filter_modules) {
3919
        if (
3920
            exists $permissions->{$module->{name}} &&
3921
            exists $permissions->{$module->{name}}->{$filter->{$module->{name}}}
3922
        ) {
3923
            # TODO: Support multiple overlapping filters/context??
3924
            $filtered_permissions = $permissions->{$module->{name}}->{$filter->{$module->{name}}};
3925
            last;
3926
        }
3927
    }
3928
    if (!$filtered_permissions) {
3929
        # No perms matching specific context conditions found, try wildcard value for each active context
3930
        foreach my $module (@filter_modules) {
3931
            if (exists $permissions->{$module->{name}}->{'*'}) {
3932
                $filtered_permissions = $permissions->{$module->{name}}->{'*'};
3933
                last;
3934
            }
3935
        }
3936
    }
3937
    return $filtered_permissions;
3938
}
3939
3940
# TODO: Use this in GetMarcPermissions
3941
=head2 GetMarcPermissionsRules
3942
3943
    my $rules = GetMarcPermissionsRules()
3944
3945
Returns:
3946
3947
=over 4
3948
3949
=item C<$rules>
3950
3951
array (in list context, arrayref otherwise) of hashrefs from marc_permissions
3952
table in order of module specificity and rule id.
3953
3954
=back
3955
3956
=cut
3957
3958
sub GetMarcPermissionsRules {
3959
    my $dbh = C4::Context->dbh;
3960
    my @rules = ();
3961
3962
    my $query = '
3963
    SELECT `marc_permissions`.`id`,
3964
           `marc_permissions`.`tagfield`,
3965
           `marc_permissions`.`filter`,
3966
           `marc_permissions`.`on_new`,
3967
           `marc_permissions`.`on_appended`,
3968
           `marc_permissions`.`on_removed`,
3969
           `marc_permissions`.`on_deleted`,
3970
           `marc_permissions_modules`.`name` as  `module`,
3971
           `marc_permissions_modules`.`description`,
3972
           `marc_permissions_modules`.`specificity`
3973
    FROM `marc_permissions`
3974
    LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id`
3975
    ORDER BY `marc_permissions_modules`.`specificity`, `id`
3976
    ';
3977
    my $sth = $dbh->prepare($query);
3978
    $sth->execute();
3979
    while ( my $row = $sth->fetchrow_hashref ) {
3980
        push(@rules, $row);
3981
    }
3982
3983
    return wantarray ? @rules : \@rules;
3984
}
3985
3986
=head2 GetMarcPermissionsModules
3987
3988
    my $modules = GetMarcPermissionsModules()
3989
3990
Returns:
3991
3992
=over 4
3993
3994
=item C<$modules>
3995
3996
array (in list context, arrayref otherwise) of hashrefs from
3997
marc_permissions_modules table in order of specificity.
3998
3999
=back
4000
4001
=cut
4002
4003
sub GetMarcPermissionsModules {
4004
    my $dbh = C4::Context->dbh;
4005
    my @modules = ();
4006
4007
    my $query = '
4008
    SELECT *
4009
    FROM `marc_permissions_modules`
4010
    ORDER BY `specificity` DESC
4011
    ';
4012
    my $sth = $dbh->prepare($query);
4013
    $sth->execute();
4014
    while ( my $row = $sth->fetchrow_hashref ) {
4015
        push(@modules, $row);
4016
    }
4017
4018
    return wantarray ? @modules : \@modules;
4019
}
4020
4021
sub GetMarcPermissionEvents {
4022
    return ('on_new', 'on_appended', 'on_removed', 'on_deleted');
4023
}
4024
4025
=head2 ModMarcPermissionsRule
4026
4027
    my $success = ModMarcPermissionsRule($id, $fields)
4028
4029
Modifies rule in the marc_permissions table.
4030
4031
=over 4
4032
4033
=item C<$id>
4034
4035
rule id to modify
4036
4037
=item C<$fields>
4038
4039
hashref defining the table fields
4040
4041
      * tagfield - required
4042
      * module - required
4043
      * filter - required
4044
      * on_new - required
4045
      * on_appended - required
4046
      * on_removed - required
4047
      * on_deleted - required
4048
4049
=back
4050
4051
Returns:
4052
4053
=over 4
4054
4055
=item C<$success>
4056
4057
undef if an error occurs, otherwise true.
4058
4059
=back
3600
4060
4061
=cut
4062
4063
sub ModMarcPermissionsRule {
4064
    my ($id, $f) = @_;
4065
    my $dbh = C4::Context->dbh;
4066
4067
    my $query = '
4068
    UPDATE `marc_permissions`
4069
    SET
4070
      tagfield = ?,
4071
      module = ?,
4072
      filter = ?,
4073
      on_new = ?,
4074
      on_appended = ?,
4075
      on_removed = ?,
4076
      on_deleted = ?
4077
    WHERE
4078
      id = ?
4079
    ';
4080
    my $sth = $dbh->prepare($query);
4081
    my $result = $sth->execute (
4082
        $f->{tagfield},
4083
        $f->{module},
4084
        $f->{filter},
4085
        $f->{on_new},
4086
        $f->{on_appended},
4087
        $f->{on_removed},
4088
        $f->{on_deleted},
4089
        $id
4090
    );
4091
    ClearMarcPermissionsRulesCache();
4092
    return $result;
4093
}
4094
4095
sub ClearMarcPermissionsRulesCache {
4096
    my $cache = Koha::Caches->get_instance();
4097
    $cache->clear_from_cache('marc_permissions');
4098
}
4099
4100
=head2 AddMarcPermissionsRule
4101
4102
    my $success = AddMarcPermissionsRule($fields)
4103
4104
Add rule to the marc_permissions table.
4105
4106
=over 4
4107
4108
=item C<$fields>
4109
4110
hashref defining the table fields
4111
4112
      tagfield - required
4113
      module - required
4114
      filter - required
4115
      on_new - required
4116
      on_appended - required
4117
      on_removed - required
4118
      on_deleted - required
4119
4120
=back
4121
4122
Returns:
4123
4124
=over 4
4125
4126
=item C<$success>
4127
4128
undef if an error occurs, otherwise true.
4129
4130
=back
4131
4132
=cut
4133
4134
sub AddMarcPermissionsRule {
4135
    my $f = shift;
4136
    my $dbh = C4::Context->dbh;
4137
    my $query = '
4138
    INSERT INTO `marc_permissions`
4139
    (
4140
      tagfield,
4141
      module,
4142
      filter,
4143
      on_new,
4144
      on_appended,
4145
      on_removed,
4146
      on_deleted
4147
    )
4148
    VALUES (?, ?, ?, ?, ?, ?, ?)
4149
    ';
4150
    my $sth = $dbh->prepare($query);
4151
    my $result = $sth->execute (
4152
        $f->{tagfield},
4153
        $f->{module},
4154
        $f->{filter},
4155
        $f->{on_new},
4156
        $f->{on_appended},
4157
        $f->{on_removed},
4158
        $f->{on_deleted}
4159
    );
4160
    ClearMarcPermissionsRulesCache();
4161
    return $result;
4162
}
4163
4164
=head2 DelMarcPermissionsRule
4165
4166
    my $success = DelMarcPermissionsRule($id)
4167
4168
Deletes rule from the marc_permissions table.
4169
4170
=over 4
4171
4172
=item C<$id>
4173
4174
rule id to delete
4175
4176
=back
4177
4178
Returns:
4179
4180
=over 4
4181
4182
=item C<$success>
4183
4184
undef if an error occurs, otherwise true.
4185
4186
=back
4187
4188
=cut
4189
4190
sub DelMarcPermissionsRule {
4191
    my $id = shift;
4192
    my $dbh = C4::Context->dbh;
4193
    my $query = '
4194
    DELETE FROM `marc_permissions`
4195
    WHERE
4196
      id = ?
4197
    ';
4198
    my $sth = $dbh->prepare($query);
4199
    my $result = $sth->execute($id);
4200
    ClearMarcPermissionsRulesCache();
4201
    return $result;
4202
}
4203
1;
3601
4204
3602
__END__
4205
__END__
3603
4206
(-)a/C4/ImportBatch.pm (-1 / +1 lines)
Lines 666-672 sub BatchCommitRecords { Link Here
666
                }
666
                }
667
                $oldxml = $old_marc->as_xml($marc_type);
667
                $oldxml = $old_marc->as_xml($marc_type);
668
668
669
                ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode);
669
                ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode, {context => {source => 'batchimport'}});
670
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?";
670
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?";
671
671
672
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
672
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
(-)a/C4/Installer/PerlDependencies.pm (+5 lines)
Lines 867-872 our $PERL_DEPS = { Link Here
867
        'required' => '0',
867
        'required' => '0',
868
        'min_ver'  => '0.17',
868
        'min_ver'  => '0.17',
869
    },
869
    },
870
    'String::Similarity' => {
871
        usage => 'cataloguing',
872
        required => 1,
873
        min_version => '1.04',
874
    },
870
};
875
};
871
876
872
1;
877
1;
(-)a/admin/marc-permissions.pl (+115 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
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/cataloguing/addbiblio.pl (-1 / +11 lines)
Lines 41-46 use Koha::ItemTypes; Link Here
41
use Koha::Libraries;
41
use Koha::Libraries;
42
42
43
use Koha::BiblioFrameworks;
43
use Koha::BiblioFrameworks;
44
use Koha::Patrons;
44
45
45
use Date::Calc qw(Today);
46
use Date::Calc qw(Today);
46
use MARC::File::USMARC;
47
use MARC::File::USMARC;
Lines 846-852 if ( $op eq "addbiblio" ) { Link Here
846
            BiblioAutoLink( $record, $frameworkcode );
847
            BiblioAutoLink( $record, $frameworkcode );
847
        } 
848
        } 
848
        if ( $is_a_modif ) {
849
        if ( $is_a_modif ) {
849
            ModBiblio( $record, $biblionumber, $frameworkcode );
850
            my $member = Koha::Patrons->find($loggedinuser);
851
            ModBiblio( $record, $biblionumber, $frameworkcode, {
852
                    context => {
853
                        source => $z3950 ? 'z39.50' : 'intranet',
854
                        category => $member->{'category_type'},
855
                        borrower => $loggedinuser
856
                    }
857
                }
858
            );
850
        }
859
        }
851
        else {
860
        else {
852
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
861
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
Lines 939-944 elsif ( $op eq "delete" ) { Link Here
939
    $template->param(
948
    $template->param(
940
        biblionumberdata => $biblionumber,
949
        biblionumberdata => $biblionumber,
941
        op               => $op,
950
        op               => $op,
951
        z3950            => $z3950
942
    );
952
    );
943
    if ( $op eq "duplicate" ) {
953
    if ( $op eq "duplicate" ) {
944
        $biblionumber = "";
954
        $biblionumber = "";
(-)a/help.pl (+5 lines)
Lines 24-29 use C4::Output; Link Here
24
# use C4::Auth;
24
# use C4::Auth;
25
use C4::Context;
25
use C4::Context;
26
use CGI qw ( -utf8 );
26
use CGI qw ( -utf8 );
27
use C4::Biblio;
27
28
28
sub _help_template_file_of_url {
29
sub _help_template_file_of_url {
29
    my $url = shift;
30
    my $url = shift;
Lines 75-78 if ( $help_version =~ m|^(\d+)\.(\d{2}).*$| ) { Link Here
75
}
76
}
76
$template->param( helpVersion => $help_version );
77
$template->param( helpVersion => $help_version );
77
78
79
my $rules = GetMarcPermissionsRules();
80
my $modules = GetMarcPermissionsModules();
81
$template->param( rules => $rules, modules => $modules );
82
78
output_html_with_http_headers $query, "", $template->output;
83
output_html_with_http_headers $query, "", $template->output;
(-)a/installer/data/mysql/atomicupdate/bug_14957-marc-permissions-syspref.sql (+3 lines)
Line 0 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');
3
INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('MARCPermissionsCorrectionSimilarity','90','','New field value is considered a correction if it is similar to the old to the specified percentage.','Integer');
(-)a/installer/data/mysql/atomicupdate/bug_14957-marc-permissions.sql (+31 lines)
Line 0 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/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc (+1 lines)
Lines 36-41 Link Here
36
    <li><a href="/cgi-bin/koha/admin/matching-rules.pl">Record matching rules</a></li>
36
    <li><a href="/cgi-bin/koha/admin/matching-rules.pl">Record matching rules</a></li>
37
    <li><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></li>
37
    <li><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></li>
38
    <li><a href="/cgi-bin/koha/admin/items_search_fields.pl">Item search fields</a></li>
38
    <li><a href="/cgi-bin/koha/admin/items_search_fields.pl">Item search fields</a></li>
39
    <li><a href="/cgi-bin/koha/admin/marc-permissions.pl">MARC field permissions</a></li>
39
</ul>
40
</ul>
40
41
41
<h5>Acquisition parameters</h5>
42
<h5>Acquisition parameters</h5>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+2 lines)
Lines 90-95 Link Here
90
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
90
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
91
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
91
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
92
                    [% END %]
92
                    [% END %]
93
                    <dt><a href="/cgi-bin/koha/admin/marc-permissions.pl">MARC field permissions</a></dt>
94
                    <dd>Managed MARC field permissions</dd>
93
                </dl>
95
                </dl>
94
96
95
                <h3>Acquisition parameters</h3>
97
                <h3>Acquisition parameters</h3>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc-permissions.tt (+320 lines)
Line 0 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 (+10 lines)
Lines 259-261 Cataloging: Link Here
259
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
259
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
260
            - "<br/>"
260
            - "<br/>"
261
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
261
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
262
        -
263
            - When importing records
264
            - pref: MARCPermissions
265
              choices:
266
                  yes: "use"
267
                  no: "don't use"
268
            - MARC permissions rules to decide which action to take for each field.
269
            - When <b>add_or_correct</b>, consider a field modification to be a correction if the old and new value has a similarity of
270
            - pref: MARCPermissionsCorrectionSimilarity
271
            - %. If similarity falls below this value, the new value will be treated as a new field.
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/logs.pref (+6 lines)
Lines 72-77 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.
75
    Debugging:
81
    Debugging:
76
        -
82
        -
77
            - pref: DumpTemplateVarsIntranet
83
            - pref: DumpTemplateVarsIntranet
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt (+1 lines)
Lines 558-563 function Changefwk() { Link Here
558
[% END %]
558
[% END %]
559
        <input type="hidden" name="op" value="addbiblio" />
559
        <input type="hidden" name="op" value="addbiblio" />
560
        <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode %]" />
560
        <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode %]" />
561
        <input type="hidden" name="z3950" value="[% z3950 %]" />
561
        <input type="hidden" name="biblionumber" value="[% biblionumber %]" />
562
        <input type="hidden" name="biblionumber" value="[% biblionumber %]" />
562
        <input type="hidden" name="breedingid" value="[% breedingid %]" />
563
        <input type="hidden" name="breedingid" value="[% breedingid %]" />
563
        <input type="hidden" name="changed_framework" value="" />
564
        <input type="hidden" name="changed_framework" value="" />
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/help/marc-permissions.tt (+148 lines)
Line 0 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/misc/migration_tools/bulkmarcimport.pl (-2 / +1 lines)
Lines 451-457 RECORD: while ( ) { Link Here
451
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
451
                    $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber;
452
                };
452
                };
453
                if ($update) {
453
                if ($update) {
454
                    eval { ( $biblionumber, $biblioitemnumber ) = ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber) ) };
454
                    eval { ( $biblionumber, $biblioitemnumber ) = ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber), {context => {source => 'bulkmarcimport'}}) };
455
                    if ($@) {
455
                    if ($@) {
456
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
456
                        warn "ERROR: Edit biblio $biblionumber failed: $@\n";
457
                        printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);
457
                        printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile);
Lines 844-847 If not specified, no MARC modification templates are used (default). Link Here
844
=back
844
=back
845
845
846
=cut
846
=cut
847
(-)a/misc/migration_tools/import_lexile.pl (-1 / +2 lines)
Lines 153-158 while ( my $row = $csv->getline_hr($fh) ) { Link Here
153
    foreach my $biblionumber (@biblionumbers) {
153
    foreach my $biblionumber (@biblionumbers) {
154
        $counter++;
154
        $counter++;
155
        my $record = GetMarcBiblio({ biblionumber => $biblionumber });
155
        my $record = GetMarcBiblio({ biblionumber => $biblionumber });
156
        my $frameworkcode = GetFrameworkCode($biblionumber);
156
157
157
        if ($verbose) {
158
        if ($verbose) {
158
            say "Found matching record! Biblionumber: $biblionumber";
159
            say "Found matching record! Biblionumber: $biblionumber";
Lines 202-208 while ( my $row = $csv->getline_hr($fh) ) { Link Here
202
            $record->append_fields($field);
203
            $record->append_fields($field);
203
        }
204
        }
204
205
205
        ModBiblio( $record, $biblionumber ) unless ( $test );
206
        ModBiblio( $record, $biblionumber, $frameworkcode, {context => {source => 'import_lexile'}} ) unless ( $test );
206
    }
207
    }
207
208
208
}
209
}
(-)a/t/db_dependent/Biblio/MARCPermissions.t (+280 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
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
C4::Context->set_preference( 'MARCPermissionsCorrectionSimilarity', '90');
38
39
# Create a record
40
my $record = MARC::Record->new();
41
$record->append_fields (
42
    MARC::Field->new('008', '12345'),
43
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
44
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
45
    MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
46
    MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'),
47
    MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'),
48
);
49
50
# Add record to DB
51
my ($biblionumber, $biblioitemnumber) = AddBiblio($record, '');
52
53
my $modules = GetMarcPermissionsModules();
54
55
##############################################################################
56
# Test overwrite rule
57
my $mod_record = MARC::Record->new();
58
$mod_record->append_fields (
59
    MARC::Field->new('008', '12345'),
60
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
61
    MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
62
    MARC::Field->new('500', '1','1', 'a' => 'this field has now been changed'),
63
    MARC::Field->new('500', '2','2', 'a' => 'and so have this field'),
64
);
65
66
# Clear MARC permission rules from DB
67
DelMarcPermissionsRule($_->{id}) for GetMarcPermissionsRules();
68
69
# Add MARC permission rules to DB
70
AddMarcPermissionsRule({
71
    module => $modules->[0]->{'id'},
72
    tagfield => '*',
73
    tagsubfield => '',
74
    filter => '*',
75
    on_existing => 'overwrite',
76
    on_new => 'add',
77
    on_removed => 'remove'
78
});
79
80
my @log = ();
81
my $new_record = ApplyMarcPermissions({
82
        biblionumber => $biblionumber,
83
        record => $mod_record,
84
        frameworkcode => '',
85
        filter => {$modules->[0]->{'name'} => 'foo'},
86
        log => \@log
87
    });
88
89
my @a500 = $new_record->field('500');
90
is ($a500[0]->subfield('a'), 'this field has now been changed', 'old field is replaced when overwrite');
91
is ($a500[1]->subfield('a'), 'and so have this field', 'old field is replaced when overwrite');
92
93
##############################################################################
94
# Test remove rule
95
is ($new_record->field('250'), undef, 'removed field is removed');
96
97
##############################################################################
98
# Test skip rule
99
$mod_record = MARC::Record->new();
100
$mod_record->append_fields (
101
    MARC::Field->new('008', '12345'),
102
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
103
    MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
104
    MARC::Field->new('500', '1','1', 'a' => 'this should not show'),
105
    MARC::Field->new('500', '2','2', 'a' => 'and neither should this'),
106
);
107
108
AddMarcPermissionsRule({
109
    module => $modules->[0]->{'id'},
110
    tagfield => '500',
111
    tagsubfield => '*',
112
    filter => '*',
113
    on_existing => 'skip',
114
    on_new => 'skip',
115
    on_removed => 'skip'
116
});
117
118
@log = ();
119
$new_record = ApplyMarcPermissions({
120
        biblionumber => $biblionumber,
121
        record => $mod_record,
122
        frameworkcode => '',
123
        filter => {$modules->[0]->{'name'} => 'foo'},
124
        log => \@log
125
    });
126
127
@a500 = $new_record->field('500');
128
is ($a500[0]->subfield('a'), 'the lazy programmer jumps over the quick brown tests', 'old field is kept when skip');
129
is ($a500[1]->subfield('a'), 'the quick brown test jumps over the lazy programmers', 'old field is kept when skip');
130
131
##############################################################################
132
# Test add rule
133
$mod_record = MARC::Record->new();
134
$mod_record->append_fields (
135
    MARC::Field->new('008', '12345'),
136
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
137
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
138
    #MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
139
    MARC::Field->new('245', '1','2', 'a' => 'some new fun value'),
140
    MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'),
141
    MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'),
142
);
143
144
AddMarcPermissionsRule({
145
    module => $modules->[0]->{'id'},
146
    tagfield => '245',
147
    tagsubfield => '*',
148
    filter => '*',
149
    on_existing => 'add',
150
    on_new => 'add',
151
    on_removed => 'skip'
152
});
153
154
155
@log = ();
156
$new_record = ApplyMarcPermissions({
157
        biblionumber => $biblionumber,
158
        record => $mod_record,
159
        frameworkcode => '',
160
        filter => {$modules->[0]->{'name'} => 'foo'},
161
        log => \@log
162
    });
163
164
my @a245 = $new_record->field('245')->subfield('a');
165
is ($a245[0], 'field data for 245 a with indicators 12', 'old field is kept when adding new');
166
is ($a245[1], 'some new fun value', 'new field is added');
167
168
##############################################################################
169
# Test add_or_correct rule
170
$mod_record = MARC::Record->new();
171
$mod_record->append_fields (
172
    MARC::Field->new('008', '12345'),
173
    #MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
174
    MARC::Field->new('100', '','', 'a' => 'a very different value'),
175
    MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'),
176
    #MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
177
    MARC::Field->new('245', '1','2', 'a' => 'Field data for 245 a with indicators 12', 'a' => 'some very different value'),
178
    MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'),
179
    MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'),
180
);
181
182
AddMarcPermissionsRule({
183
    module => $modules->[0]->{'id'},
184
    tagfield => '(100|245)',
185
    tagsubfield => '*',
186
    filter => '*',
187
    on_existing => 'add_or_correct',
188
    on_new => 'add',
189
    on_removed => 'skip'
190
});
191
192
@log = ();
193
$new_record = ApplyMarcPermissions({
194
        biblionumber => $biblionumber,
195
        record => $mod_record,
196
        frameworkcode => '',
197
        filter => {$modules->[0]->{'name'} => 'foo'},
198
        log => \@log
199
    });
200
201
@a245 = $new_record->field('245')->subfield('a');
202
is ($a245[0], 'Field data for 245 a with indicators 12', 'add_or_correct modifies field when a correction');
203
is ($a245[1], 'some very different value', 'add_or_correct adds field when not a correction');
204
205
my @a100 = $new_record->field('100')->subfield('a');
206
is ($a100[0], 'field data for 100 a without indicators', 'add_or_correct keeps old field when not a correction');
207
is ($a100[1], 'a very different value', 'add_or_correct adds field when not a correction');
208
209
##############################################################################
210
# Test rule evaluation order
211
$mod_record = MARC::Record->new();
212
$mod_record->append_fields (
213
    MARC::Field->new('008', '12345'),
214
    MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'),
215
    MARC::Field->new('250', '','', 'a' => 'take one down, pass it around'),
216
    MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'),
217
    MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'),
218
    MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'),
219
);
220
221
222
DelMarcPermissionsRule($_->{id}) for GetMarcPermissionsRules();
223
224
AddMarcPermissionsRule({
225
    module => $modules->[0]->{'id'},
226
    tagfield => '*',
227
    tagsubfield => '*',
228
    filter => '*',
229
    on_existing => 'skip',
230
    on_new => 'skip',
231
    on_removed => 'skip'
232
});
233
AddMarcPermissionsRule({
234
    module => $modules->[0]->{'id'},
235
    tagfield => '250',
236
    tagsubfield => '*',
237
    filter => '*',
238
    on_existing => 'overwrite',
239
    on_new => 'skip',
240
    on_removed => 'skip'
241
});
242
AddMarcPermissionsRule({
243
    module => $modules->[0]->{'id'},
244
    tagfield => '*',
245
    tagsubfield => 'a',
246
    filter => '*',
247
    on_existing => 'add_or_correct',
248
    on_new => 'skip',
249
    on_removed => 'skip'
250
});
251
AddMarcPermissionsRule({
252
    module => $modules->[0]->{'id'},
253
    tagfield => '250',
254
    tagsubfield => 'a',
255
    filter => '*',
256
    on_existing => 'add',
257
    on_new => 'skip',
258
    on_removed => 'skip'
259
});
260
261
@log = ();
262
$new_record = ApplyMarcPermissions({
263
        biblionumber => $biblionumber,
264
        record => $mod_record,
265
        frameworkcode => '',
266
        filter => {$modules->[0]->{'name'} => 'foo'},
267
        log => \@log
268
    });
269
270
my @rule = grep { $_->{tag} eq '250' and $_->{subfieldcode} eq 'a' } @log;
271
is(scalar @rule, 1, 'only one rule applied');
272
is($rule[0]->{event}.':'.$rule[0]->{action}, 'existing:add', 'most specific rule used');
273
274
my @a250 = $new_record->field('250')->subfield('a');
275
is ($a250[0], '250 bottles of beer on the wall', 'most specific rule is applied, original field kept');
276
is ($a250[1], 'take one down, pass it around', 'most specific rule is applied, new field added');
277
278
$dbh->rollback;
279
280
1;
(-)a/tools/batch_record_modification.pl (-4 / +10 lines)
Lines 158-164 if ( $op eq 'form' ) { Link Here
158
    my ( $job );
158
    my ( $job );
159
    if ( $runinbackground ) {
159
    if ( $runinbackground ) {
160
        my $job_size = scalar( @record_ids );
160
        my $job_size = scalar( @record_ids );
161
        $job = C4::BackgroundJob->new( $sessionID, "FIXME", '/cgi-bin/koha/tools/batch_record_modification.pl', $job_size );
161
        $job = C4::BackgroundJob->new( $sessionID, "FIXME", $ENV{SCRIPT_NAME}, $job_size );
162
        my $job_id = $job->id;
162
        my $job_id = $job->id;
163
        if (my $pid = fork) {
163
        if (my $pid = fork) {
164
            $dbh->{InactiveDestroy}  = 1;
164
            $dbh->{InactiveDestroy}  = 1;
Lines 170-176 if ( $op eq 'form' ) { Link Here
170
        } elsif (defined $pid) {
170
        } elsif (defined $pid) {
171
            close STDOUT;
171
            close STDOUT;
172
        } else {
172
        } else {
173
            warn "fork failed while attempting to run tools/batch_record_modification.pl as a background job";
173
            warn "fork failed while attempting to run $ENV{'SCRIPT_NAME'} as a background job";
174
            exit 0;
174
            exit 0;
175
        }
175
        }
176
    }
176
    }
Lines 194-200 if ( $op eq 'form' ) { Link Here
194
                my $record = GetMarcBiblio({ biblionumber => $biblionumber });
194
                my $record = GetMarcBiblio({ biblionumber => $biblionumber });
195
                ModifyRecordWithTemplate( $mmtid, $record );
195
                ModifyRecordWithTemplate( $mmtid, $record );
196
                my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
196
                my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
197
                ModBiblio( $record, $biblionumber, $frameworkcode );
197
                my ($member) = Koha::Patrons->find($loggedinuser);
198
                ModBiblio( $record, $biblionumber, $frameworkcode,
199
                    {
200
                        source => 'batchmod',
201
                        category => $member->{'category_type'},
202
                        borrower => $loggedinuser
203
                    }
204
                );
198
            };
205
            };
199
            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
206
            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
200
                push @messages, {
207
                push @messages, {
201
- 

Return to bug 14957