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

(-)a/C4/Biblio.pm (-9 / +966 lines)
Lines 28-33 use MARC::File::USMARC; Link Here
28
use MARC::File::XML;
28
use MARC::File::XML;
29
use POSIX qw(strftime);
29
use POSIX qw(strftime);
30
use Module::Load::Conditional qw(can_load);
30
use Module::Load::Conditional qw(can_load);
31
use String::Similarity;
31
32
32
use C4::Koha;
33
use C4::Koha;
33
use C4::Log;    # logaction
34
use C4::Log;    # logaction
Lines 106-111 BEGIN { Link Here
106
      &CountItemsIssued
107
      &CountItemsIssued
107
      &CountBiblioInOrders
108
      &CountBiblioInOrders
108
      &GetSubscriptionsId
109
      &GetSubscriptionsId
110
111
      &GetMarcPermissionsRules
112
      &GetMarcPermissionsModules
113
      &ModMarcPermissionsRule
114
      &AddMarcPermissionsRule
115
      &DelMarcPermissionsRule
109
    );
116
    );
110
117
111
    # To modify something
118
    # To modify something
Lines 133-138 BEGIN { Link Here
133
    # they are useful in a few circumstances, so they are exported,
140
    # they are useful in a few circumstances, so they are exported,
134
    # but don't use them unless you are a core developer ;-)
141
    # but don't use them unless you are a core developer ;-)
135
    push @EXPORT, qw(
142
    push @EXPORT, qw(
143
      &ApplyMarcPermissions
136
      &ModBiblioMarc
144
      &ModBiblioMarc
137
    );
145
    );
138
146
Lines 270-276 sub AddBiblio { Link Here
270
278
271
=head2 ModBiblio
279
=head2 ModBiblio
272
280
273
  ModBiblio( $record,$biblionumber,$frameworkcode);
281
  ModBiblio($record, $biblionumber, $frameworkcode, $options);
274
282
275
Replace an existing bib record identified by C<$biblionumber>
283
Replace an existing bib record identified by C<$biblionumber>
276
with one supplied by the MARC::Record object C<$record>.  The embedded
284
with one supplied by the MARC::Record object C<$record>.  The embedded
Lines 291-297 Returns 1 on success 0 on failure Link Here
291
=cut
299
=cut
292
300
293
sub ModBiblio {
301
sub ModBiblio {
294
    my ( $record, $biblionumber, $frameworkcode ) = @_;
302
    my ( $record, $biblionumber, $frameworkcode, $filter ) = @_;
303
295
    if (!$record) {
304
    if (!$record) {
296
        carp 'No record passed to ModBiblio';
305
        carp 'No record passed to ModBiblio';
297
        return 0;
306
        return 0;
Lines 329-341 sub ModBiblio { Link Here
329
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
338
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
330
339
331
    # load the koha-table data object
340
    # load the koha-table data object
332
    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode );
341
    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode, undef, $biblionumber, $filter );
333
342
334
    # update MARC subfield that stores biblioitems.cn_sort
343
    # update MARC subfield that stores biblioitems.cn_sort
335
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
344
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
336
345
337
    # update the MARC record (that now contains biblio and items) with the new record data
346
    # update the MARC record (that now contains biblio and items) with the new record data
338
    &ModBiblioMarc( $record, $biblionumber, $frameworkcode );
347
    ModBiblioMarc( $record, $biblionumber, $frameworkcode, $filter );
339
348
340
    # modify the other koha tables
349
    # modify the other koha tables
341
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
350
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
Lines 2656-2665 sub TransformHtmlToMarc { Link Here
2656
2665
2657
=head2 TransformMarcToKoha
2666
=head2 TransformMarcToKoha
2658
2667
2659
  $result = TransformMarcToKoha( $record, $frameworkcode )
2668
  $result = TransformMarcToKoha( $record, $frameworkcode, $biblionumber, $filter )
2660
2669
2661
Extract data from a MARC bib record into a hashref representing
2670
Extract data from a MARC bib record into a hashref representing
2662
Koha biblio, biblioitems, and items fields. 
2671
Koha biblio, biblioitems, and items fields.
2663
2672
2664
If passed an undefined record will log the error and return an empty
2673
If passed an undefined record will log the error and return an empty
2665
hash_ref
2674
hash_ref
Lines 2667-2673 hash_ref Link Here
2667
=cut
2676
=cut
2668
2677
2669
sub TransformMarcToKoha {
2678
sub TransformMarcToKoha {
2670
    my ( $record, $frameworkcode, $limit_table ) = @_;
2679
    my ( $record, $frameworkcode, $limit_table, $biblionumber, $filter ) = @_;
2671
2680
2672
    my $result = {};
2681
    my $result = {};
2673
    if (!defined $record) {
2682
    if (!defined $record) {
Lines 2688-2693 sub TransformMarcToKoha { Link Here
2688
        $tables{'biblioitems'} = 1;
2697
        $tables{'biblioitems'} = 1;
2689
    }
2698
    }
2690
2699
2700
    # apply permissions
2701
    if ( C4::Context->preference('MARCPermissions') && $biblionumber && $filter) {
2702
        $record = ApplyMarcPermissions({
2703
                biblionumber => $biblionumber,
2704
                record => $record,
2705
                frameworkcode => $frameworkcode,
2706
                filter => $filter,
2707
                nolog => 1
2708
            });
2709
    }
2710
2691
    # traverse through record
2711
    # traverse through record
2692
  MARCFIELD: foreach my $field ( $record->fields() ) {
2712
  MARCFIELD: foreach my $field ( $record->fields() ) {
2693
        my $tag = $field->tag();
2713
        my $tag = $field->tag();
Lines 3442-3448 sub _koha_delete_biblioitems { Link Here
3442
3462
3443
=head2 ModBiblioMarc
3463
=head2 ModBiblioMarc
3444
3464
3445
  &ModBiblioMarc($newrec,$biblionumber,$frameworkcode);
3465
  &ModBiblioMarc($newrec,$biblionumber,$frameworkcode,$filter);
3446
3466
3447
Add MARC XML data for a biblio to koha
3467
Add MARC XML data for a biblio to koha
3448
3468
Lines 3453-3459 Function exported, but should NOT be used, unless you really know what you're do Link Here
3453
sub ModBiblioMarc {
3473
sub ModBiblioMarc {
3454
    # pass the MARC::Record to this function, and it will create the records in
3474
    # pass the MARC::Record to this function, and it will create the records in
3455
    # the marcxml field
3475
    # the marcxml field
3456
    my ( $record, $biblionumber, $frameworkcode ) = @_;
3476
    my ( $record, $biblionumber, $frameworkcode, $options ) = @_;
3477
3457
    if ( !$record ) {
3478
    if ( !$record ) {
3458
        carp 'ModBiblioMarc passed an undefined record';
3479
        carp 'ModBiblioMarc passed an undefined record';
3459
        return;
3480
        return;
Lines 3466-3471 sub ModBiblioMarc { Link Here
3466
    if ( !$frameworkcode ) {
3487
    if ( !$frameworkcode ) {
3467
        $frameworkcode = "";
3488
        $frameworkcode = "";
3468
    }
3489
    }
3490
3491
    # apply permissions
3492
    if (
3493
        C4::Context->preference('MARCPermissions') &&
3494
        defined $options &&
3495
        exists $options->{'filter'}
3496
    ) {
3497
        $record = ApplyMarcPermissions(
3498
            {
3499
                biblionumber => $biblionumber,
3500
                record => $record,
3501
                frameworkcode => $frameworkcode,
3502
                filter => $options->{'filter'}
3503
            }
3504
        );
3505
    }
3506
3469
    my $sth = $dbh->prepare("UPDATE biblio SET frameworkcode=? WHERE biblionumber=?");
3507
    my $sth = $dbh->prepare("UPDATE biblio SET frameworkcode=? WHERE biblionumber=?");
3470
    $sth->execute( $frameworkcode, $biblionumber );
3508
    $sth->execute( $frameworkcode, $biblionumber );
3471
    $sth->finish;
3509
    $sth->finish;
Lines 3786-3791 sub RemoveAllNsb { Link Here
3786
    return $record;
3824
    return $record;
3787
}
3825
}
3788
3826
3827
=head2 ApplyMarcPermissions
3828
3829
    my $record = ApplyMarcPermissions($arguments)
3830
3831
Applies marc permission rules to a record.
3832
3833
C<$arguments> is expected to be a hashref with below keys defined.
3834
3835
=over 4
3836
3837
=item C<biblionumber>
3838
biblionumber of old record
3839
3840
=item C<record>
3841
record that will modify old record
3842
3843
=item C<frameworkcode>
3844
only tags included in framework will be processed
3845
3846
=item C<filter>
3847
hashref containing at least one filter module from the marc_permissions_modules
3848
table in form {module => filter}. Three predefined filter modules exists:
3849
3850
    * source
3851
    * category
3852
    * borrower
3853
3854
=item C<log>
3855
optional reference to array that will be filled with rule evaluation log
3856
entries.
3857
3858
=item C<nolog>
3859
optional boolean which when true disables logging to action log.
3860
3861
=back
3862
3863
Returns:
3864
3865
=over 4
3866
3867
=item C<$record>
3868
3869
new MARC record based on C<record> with C<filter> applied. If no old
3870
record for C<biblionumber> can be found, C<record> is returned unchanged.
3871
Default action when no matching filter found is to leave old record unchanged.
3872
3873
=back
3874
3875
=cut
3876
3877
sub ApplyMarcPermissions {
3878
    my $arguments     = shift;
3879
    my $biblionumber  = $arguments->{biblionumber};
3880
    my $new_record    = $arguments->{record};
3881
    my $frameworkcode = $arguments->{frameworkcode} || '';
3882
3883
    if ( !$biblionumber ) {
3884
        carp 'ApplyMarcPermissions called on undefined biblionumber';
3885
        return;
3886
    }
3887
    if ( !$new_record ) {
3888
        carp 'ApplyMarcPermissions called on undefined record';
3889
        return;
3890
    }
3891
    my $filter = $arguments->{filter}
3892
      or return $new_record;
3893
3894
    my $log   = $arguments->{log} || [];
3895
    my $nolog = $arguments->{nolog};
3896
3897
    my $dbh = C4::Context->dbh;
3898
3899
    my $is_regex = sub {
3900
        my ( $tag, $m ) = @_;
3901
        # tag is not exactly same as possible regex
3902
        $tag ne $m &&
3903
3904
        # wildcard
3905
        $m ne '*' &&
3906
3907
        # valid tagDataType
3908
        $m !~ /^(0[1-9A-z][\dA-Z]) |
3909
        ([1-9A-z][\dA-z]{2})$/x &&
3910
3911
        # nor valid controltagDataType
3912
        $m !~ /00[1-9A-Za-z]{1}/ &&
3913
3914
        # so we try it as a regex
3915
        $tag =~ /^$m$/
3916
    };
3917
3918
    my $old_record = GetMarcBiblio($biblionumber);
3919
    if ( $old_record ) {
3920
        my $ret_record      = MARC::Record->new();
3921
        my $perm            = GetMarcPermissions() or return $new_record;
3922
        my $marc_struct     = GetMarcStructure(1, $frameworkcode);
3923
        my $similarity_pref = C4::Context->preference("MARCPermissionsCorrectionSimilarity");
3924
        $similarity_pref    = defined $similarity_pref &&
3925
                              $similarity_pref >= 0 &&
3926
                              $similarity_pref <= 100 ?
3927
                              $similarity_pref/100.0 : undef;
3928
3929
        # First came the leader ...
3930
        $ret_record->leader($old_record->leader());
3931
3932
        # ... and then came all the tags in the framework
3933
        for my $tag (sort keys %{$marc_struct}) {
3934
            my @old_fields = $old_record->field($tag);
3935
            my @new_fields = $new_record->field($tag);
3936
            next unless @old_fields or @new_fields;
3937
            my @perms = (
3938
                $perm->{'*'},
3939
                (   # regex tags
3940
                    map { $perm->{$_} }
3941
                      grep { $is_regex->( $tag, $_ ) } sort keys %{$perm}
3942
                ),
3943
                $perm->{$tag}
3944
            );
3945
3946
            # There can be more than one field for each tag, so we have to
3947
            # iterate all fields and distinguish which are new, removed or
3948
            # modified
3949
            my $max_fields = ($#old_fields, $#new_fields)
3950
                             [$#old_fields< $#new_fields];
3951
            for ( my $i_field = 0; $i_field <= $max_fields; $i_field++ ) {
3952
                my $old_field = $old_fields[$i_field];
3953
                my $new_field = $new_fields[$i_field];
3954
3955
                # Existing field
3956
                if ( $old_field and $new_field ) {
3957
                    # Control fields
3958
                    if ( $old_field->is_control_field() ) {
3959
                        # Existing control field
3960
                        if ( $old_field and $new_field ) {
3961
                            my $on_existing = GetMarcPermissionsAction('on_existing', $filter, @perms);
3962
                            if ( defined $on_existing->{action} ) {
3963
                                push @{$log},
3964
                                  {
3965
                                    rule         => $on_existing->{rule},
3966
                                    event        => "existing",
3967
                                    action       => $on_existing->{action},
3968
                                    tag          => $tag,
3969
                                    subfieldcode => undef,
3970
                                    filter       => $filter
3971
                                  };
3972
                                if ( $on_existing->{action} eq 'skip' ) {
3973
                                    $ret_record->insert_fields_ordered($old_field);
3974
                                    next;
3975
                                } elsif ( $on_existing->{action} eq 'overwrite' ) {
3976
                                    $ret_record->insert_fields_ordered($new_field);
3977
                                    next;
3978
                                } elsif ( $on_existing->{action} eq 'add' ) {
3979
                                    $ret_record->insert_fields_ordered($old_field);
3980
                                    $ret_record->insert_fields_ordered($new_field);
3981
                                    next;
3982
                                } elsif ( $on_existing->{action} eq 'add_or_correct' ) {
3983
                                    if ( $similarity_pref ) {
3984
                                        my $similarity = similarity $old_field->data(), $new_field->data();
3985
                                        if ( $similarity >= $similarity_pref ) {
3986
                                            $ret_record->insert_fields_ordered($new_field);
3987
                                        } else {
3988
                                            $ret_record->insert_fields_ordered($old_field);
3989
                                            $ret_record->insert_fields_ordered($new_field);
3990
                                        }
3991
                                    } else {
3992
                                        $ret_record->insert_fields_ordered($old_field);
3993
                                    }
3994
                                }
3995
                            } else { # default to skip
3996
                                $ret_record->insert_fields_ordered($old_field);
3997
                                next;
3998
                            }
3999
                        # New control field
4000
                        } elsif ( not $old_field and $new_field ) {
4001
                            my $on_new = GetMarcPermissionsAction('on_new', $filter, @perms);
4002
                            if ( defined $on_new->{action} ) {
4003
                                push @{$log},
4004
                                  {
4005
                                    rule         => $on_new->{rule},
4006
                                    event        => "new",
4007
                                    action       => $on_new->{action},
4008
                                    tag          => $tag,
4009
                                    subfieldcode => undef,
4010
                                    filter       => $filter
4011
                                  };
4012
                                if ( $on_new->{action} eq 'skip' ) { # Redundant
4013
                                    next;
4014
                                } elsif ( $on_new->{action} eq 'add' ) {
4015
                                    $ret_record->insert_fields_ordered($new_field);
4016
                                    next;
4017
                                }
4018
                            }
4019
                        # Removed control field
4020
                        } elsif ( $old_field and not $new_field ) {
4021
                            my $on_removed = GetMarcPermissionsAction('on_removed', $filter, @perms);
4022
                            if ( defined $on_removed->{action} ) {
4023
                                push @{$log},
4024
                                  {
4025
                                    rule         => $on_removed->{rule},
4026
                                    event        => "removed",
4027
                                    action       => $on_removed->{action},
4028
                                    tag          => $tag,
4029
                                    subfieldcode => undef
4030
                                  };
4031
                                if ( $on_removed->{action} eq 'skip' ) {
4032
                                    $ret_record->insert_fields_ordered($old_field);
4033
                                    next;
4034
                                } elsif ( $on_removed->{action} eq 'remove' ) {
4035
                                    next;
4036
                                }
4037
                            } else { # default to skip
4038
                                $ret_record->insert_fields_ordered($old_field);
4039
                                next;
4040
                            }
4041
                        }
4042
                    # Indicators and subfields
4043
                    } else {
4044
                        # Indicators
4045
                        my @old_ind = map { $old_field->indicator($_) } (1,2);
4046
                        my @new_ind = map { $new_field->indicator($_) } (1,2);
4047
                        my @ind = ('','');
4048
4049
                        for my $i ((0,1)) {
4050
                            my @ind_perms = (
4051
                                @perms,
4052
                                $perm->{$tag}->{subfields}->{ 'i' . ( $i + 1 ) }
4053
                            );
4054
4055
                            # Existing indicator
4056
                            if ( defined $old_ind[$i] and defined $new_ind[$i] ) {
4057
                                my $on_existing = GetMarcPermissionsAction('on_existing', $filter, @ind_perms);
4058
                                if ( defined $on_existing->{action} ) {
4059
                                    push @{$log},
4060
                                      {
4061
                                        rule         => $on_existing->{rule},
4062
                                        event        => "existing",
4063
                                        action       => $on_existing->{action},
4064
                                        tag          => $tag,
4065
                                        subfieldcode => 'i' . ( $i + 1 ),
4066
                                        filter       => $filter
4067
                                      };
4068
                                    if ( $on_existing->{action} eq 'skip' ) {
4069
                                        $ind[$i] = $old_ind[$i];
4070
                                    } elsif ( $on_existing->{action} eq 'overwrite' ) {
4071
                                        $ind[$i] = $new_ind[$i];
4072
                                    }
4073
                                } else { # default to skip
4074
                                    $ind[$i] = $old_ind[$i];
4075
                                }
4076
                            # New indicator
4077
                            } elsif ( not defined $old_ind[$i] and defined $new_ind[$i] ) {
4078
                                my $on_new = GetMarcPermissionsAction('on_new', $filter, @ind_perms);
4079
                                if ( defined $on_new->{action} ) {
4080
                                    push @{$log},
4081
                                      {
4082
                                        rule         => $on_new->{rule},
4083
                                        event        => "new",
4084
                                        action       => $on_new->{action},
4085
                                        tag          => $tag,
4086
                                        subfieldcode => 'i' . ( $i + 1 ),
4087
                                        filter       => $filter
4088
                                      };
4089
                                    if ( $on_new->{action} eq 'skip' ) {
4090
                                        $ind[$i] = $old_ind[$i];
4091
                                    } elsif ( $on_new->{action} eq 'add' ) {
4092
                                        $ind[$i] = $new_ind[$i];
4093
                                    }
4094
                                }
4095
                            # Removed indicator
4096
                            } elsif ( defined $old_ind[$i] and not defined $new_ind[$i] ) {
4097
                                my $on_removed = GetMarcPermissionsAction('on_removed', $filter, @ind_perms);
4098
                                if ( defined $on_removed->{action} ) {
4099
                                    push @{$log},
4100
                                      {
4101
                                        rule         => $on_removed->{rule},
4102
                                        event        => "removed",
4103
                                        action       => $on_removed->{action},
4104
                                        tag          => $tag,
4105
                                        subfieldcode => 'i' . ( $i + 1 ),
4106
                                        filter       => $filter
4107
                                      };
4108
                                    if ( $on_removed->{action} eq 'skip' ) {
4109
                                        $ind[$i] = $old_ind[$i];
4110
                                    } elsif ( $on_removed->{action} eq 'remove' ) {
4111
                                        $ind[$i] = '';
4112
                                    }
4113
                                } else { # default to skip
4114
                                    $ind[$i] = $old_ind[$i];
4115
                                }
4116
                            }
4117
                        }
4118
4119
                        # Subfields
4120
                        my @subfields = ();
4121
                        my %proccessed_subcodes = ();
4122
4123
                        # Try to preserve subfield order by first processing
4124
                        # subfields from the old record, and then subfields from
4125
                        # the framework structure unless include in old record.
4126
                        my @sorted_subfields = (
4127
                            map { shift @{$_} } $old_field->subfields(),
4128
                            () = sort keys %{ $marc_struct->{$tag} }
4129
                        );
4130
                        for my $subcode ( @sorted_subfields ) {
4131
                            next if $proccessed_subcodes{$subcode};
4132
                            $proccessed_subcodes{$subcode} = 1;
4133
                            next unless ref $marc_struct->{$tag}{$subcode} eq 'HASH';
4134
                            my @old_subfields = $old_field->subfield($subcode);
4135
                            my @new_subfields = $new_field->subfield($subcode);
4136
                            next unless @old_subfields or @new_subfields;
4137
4138
                            my @subfield_perms = (
4139
                                @perms,
4140
                                $perm->{'*'}->{subfields}->{'*'},
4141
                                $perm->{$tag}->{subfields}->{'*'},
4142
                                (    # tag->regex
4143
                                    map { $perm->{$tag}->{subfields}->{$_} }
4144
                                      grep {
4145
                                        # subcode is not exactly same as
4146
                                        # possible regex
4147
                                        $subcode ne $_ &&
4148
4149
                                        # wildcard, not checking for valid
4150
                                        # subfieldcodeDataType since it
4151
                                        # contains too many regex characters
4152
                                        $_ !~ /^(\*)$/ &&
4153
4154
                                        # so we try it as a regex
4155
                                        $subcode =~ /$_/
4156
                                      } sort
4157
                                      keys %{ $perm->{$tag}->{subfields} }
4158
                                ),
4159
                                (    # regex->*
4160
                                    map { $perm->{$_}->{subfields}->{'*'} }
4161
                                      grep { $is_regex->( $tag, $_ ) }
4162
                                      sort keys %{$perm}
4163
                                ),
4164
                                $perm->{'*'}->{subfields}->{$subcode},
4165
                                (    # regex->subcode
4166
                                    map { $perm->{$_}->{subfields}->{$subcode} }
4167
                                      grep { $is_regex->( $tag, $_ ) }
4168
                                      sort keys %{$perm}
4169
                                ),
4170
                                $perm->{$tag}->{subfields}->{$subcode}
4171
                            );
4172
4173
                            # Existing subfield
4174
                            if ( @old_subfields and @new_subfields ) {
4175
                                my $on_existing = GetMarcPermissionsAction('on_existing', $filter, @subfield_perms);
4176
                                if ( defined $on_existing->{action} ) {
4177
                                    push @{$log},
4178
                                      {
4179
                                        rule         => $on_existing->{rule},
4180
                                        event        => "existing",
4181
                                        action       => $on_existing->{action},
4182
                                        tag          => $tag,
4183
                                        subfieldcode => $subcode,
4184
                                        filter       => $filter
4185
                                      };
4186
                                    if ( $on_existing->{action} eq 'skip' ) {
4187
                                        push(@subfields, map {($subcode,$_)} @old_subfields);
4188
                                        next;
4189
                                    } elsif ( $on_existing->{action} eq 'overwrite' ) {
4190
                                        push(@subfields, map {($subcode,$_)} @new_subfields);
4191
                                        next;
4192
                                    } elsif ( $on_existing->{action} eq 'add' ) {
4193
                                        push(@subfields, map {($subcode,$_)} @old_subfields);
4194
                                        push(@subfields, map {($subcode,$_)} @new_subfields);
4195
                                        next;
4196
                                    } elsif ( $on_existing->{action} eq 'add_or_correct' ) {
4197
                                        if ( $similarity_pref ) {
4198
                                            my @corrected_vals = ();
4199
4200
                                            # correct all old subfields
4201
                                            for ( my $i_new = 0; $i_new <= $#new_subfields; $i_new++ ) {
4202
                                                my $new_val = $new_subfields[$i_new];
4203
                                                for ( my $i_old = 0; $i_old <= $#old_subfields; $i_old++ ) {
4204
                                                    my $old_val = $old_subfields[$i_old];
4205
                                                    next if not defined $old_val or not defined $new_val;
4206
                                                    my $similarity = similarity ($old_val, $new_val);
4207
                                                    if ( $similarity >= $similarity_pref ) {
4208
                                                        $corrected_vals[$i_old] = $new_val;
4209
                                                        $new_subfields[$i_new] = undef;
4210
                                                        $old_subfields[$i_old] = undef;
4211
                                                        last;
4212
                                                    }
4213
                                                }
4214
                                            }
4215
4216
                                            # insert all unchanged old subfields
4217
                                            map {
4218
                                                $corrected_vals[$_] = $old_subfields[$_]
4219
                                                    if defined $old_subfields[$_]
4220
                                                } 0 .. $#old_subfields;
4221
4222
4223
                                            # append all remaning new subfields
4224
                                            map {push @corrected_vals, $_ if defined} @new_subfields;
4225
4226
                                            push(@subfields, map {($subcode,$_)} @corrected_vals);
4227
                                        } else {
4228
                                            push(@subfields, map {($subcode,$_)} @old_subfields);
4229
                                        }
4230
                                    }
4231
                                } else { # default to skip
4232
                                    push(@subfields, map {($subcode,$_)} @old_subfields);
4233
                                    next;
4234
                                }
4235
                            # New subfield
4236
                            } elsif ( not @old_subfields and @new_subfields ) {
4237
                                my $on_new = GetMarcPermissionsAction('on_new', $filter, @subfield_perms);
4238
                                if ( defined $on_new->{action} ) {
4239
                                    push @{$log},
4240
                                      {
4241
                                        rule         => $on_new->{rule},
4242
                                        event        => "new",
4243
                                        action       => $on_new->{action},
4244
                                        tag          => $tag,
4245
                                        subfieldcode => $subcode,
4246
                                        filter       => $filter
4247
                                      };
4248
                                    if ( $on_new->{action} eq 'skip' ) {
4249
                                        next;
4250
                                    } elsif ( $on_new->{action} eq 'add' ) {
4251
                                        push(@subfields, map {($subcode,$_)} @new_subfields);
4252
                                        next;
4253
                                    }
4254
                                }
4255
                            # Removed subfield
4256
                            } elsif ( @old_subfields and not @new_subfields ) {
4257
                                my $on_removed = GetMarcPermissionsAction('on_removed', $filter, @subfield_perms);
4258
                                if ( defined $on_removed->{action} ) {
4259
                                    push @{$log},
4260
                                      {
4261
                                        rule         => $on_removed->{rule},
4262
                                        event        => "removed",
4263
                                        action       => $on_removed->{action},
4264
                                        tag          => $tag,
4265
                                        subfieldcode => $subcode,
4266
                                        filter       => $filter
4267
                                      };
4268
                                    if ( $on_removed->{action} eq 'skip' ) {
4269
                                        push(@subfields, ($subcode => @old_subfields));
4270
                                        next;
4271
                                    } elsif ( $on_removed->{action} eq 'remove' ) {
4272
                                        next;
4273
                                    }
4274
                                } else { # default to skip
4275
                                    push(@subfields, ($subcode => @old_subfields));
4276
                                    next;
4277
                                }
4278
                            }
4279
                        } # / for each subfield
4280
                        $ret_record->insert_grouped_field(MARC::Field->new($tag, $ind[0], $ind[1], @subfields));
4281
                    }
4282
                # New field
4283
                } elsif ( not $old_field and $new_field ) {
4284
                    my $on_new = GetMarcPermissionsAction('on_new', $filter, @perms);
4285
4286
                    if ( defined $on_new->{action} ) {
4287
                        push @{$log},
4288
                          {
4289
                            rule         => $on_new->{rule},
4290
                            event        => "new",
4291
                            action       => $on_new->{action},
4292
                            tag          => $tag,
4293
                            subfieldcode => undef,
4294
                            filter       => $filter
4295
                          };
4296
                        if ( $on_new->{action} eq 'skip' ) {
4297
                            next;
4298
                        } elsif ( $on_new->{action} eq 'add' ) {
4299
                            $ret_record->insert_fields_ordered($new_field);
4300
                            next;
4301
                        }
4302
                    }
4303
                # Removed field
4304
                } elsif ( $old_field and not $new_field ) {
4305
                    my $on_removed = GetMarcPermissionsAction('on_removed', $filter, @perms);
4306
                    if ( defined $on_removed->{action} ) {
4307
                        push @{$log},
4308
                          {
4309
                            rule         => $on_removed->{rule},
4310
                            event        => "removed",
4311
                            action       => $on_removed->{action},
4312
                            tag          => $tag,
4313
                            subfieldcode => undef,
4314
                            filter       => $filter
4315
                          };
4316
                        if ( $on_removed->{action} eq 'skip' ) {
4317
                            $ret_record->insert_grouped_field($old_field);
4318
                            next;
4319
                        } elsif ( $on_removed->{action} eq 'remove' ) {
4320
                            next;
4321
                        }
4322
                    } else { # default to skip
4323
                        $ret_record->insert_grouped_field($old_field);
4324
                        next;
4325
                    }
4326
                }
4327
            } # / for each field
4328
        } # / for each tag
4329
        if ( !$nolog && C4::Context->preference("MARCPermissionsLog") ) {
4330
            my $log_str = '';
4331
            my $n       = $#{$log};
4332
            for my $l ( @{$log} ) {
4333
                $log_str .= "{\n";
4334
                my $kn = keys %{$l};
4335
                for my $k ( sort keys %{$l} ) {
4336
                    if ( $k eq 'filter' ) {
4337
                        $log_str .= "  filter => {\n";
4338
                        my $fkn = keys %{ $l->{$k} };
4339
                        for my $fk ( sort keys %{ $l->{$k} } ) {
4340
                            $log_str .= '    '
4341
                              . $fk . ' => '
4342
                              . $l->{$k}->{$fk}
4343
                              . ( --$fkn ? ',' : '' ) . "\n";
4344
                        }
4345
                        $log_str .= "  }" . ( --$kn ? ',' : '' ) . "\n";
4346
                    }
4347
                    else {
4348
                        $log_str .= '  '
4349
                          . $k . ' => '
4350
                          . $l->{$k}
4351
                          . ( --$kn ? ',' : '' ) . "\n"
4352
                          if defined $l->{$k};
4353
                    }
4354
                }
4355
                $log_str .= '}' . ( $n-- ? ",\n" : '' );
4356
            }
4357
            logaction( "CATALOGUING", "MODIFY", $biblionumber, $log_str );
4358
        }
4359
        return $ret_record;
4360
    }
4361
    return $new_record;
4362
}
4363
4364
4365
=head2 GetMarcPermissions
4366
4367
    my $marc_permissions = GetMarcPermissions()
4368
4369
Loads MARC field permissions from the marc_permissions table.
4370
4371
Returns:
4372
4373
=over 4
4374
4375
=item C<$marc_permissions>
4376
4377
hashref with permissions structure for use with GetMarcPermissionsAction.
4378
4379
=back
4380
4381
=cut
4382
4383
sub GetMarcPermissions {
4384
    my $dbh = C4::Context->dbh;
4385
    my $rule_count = 0;
4386
    my %perms = ();
4387
4388
    my $query = '
4389
    SELECT `marc_permissions`.*,
4390
           `marc_permissions_modules`.`name`,
4391
           `marc_permissions_modules`.`description`,
4392
           `marc_permissions_modules`.`specificity`
4393
    FROM `marc_permissions`
4394
    LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id`
4395
    ORDER BY `marc_permissions_modules`.`specificity`, `id`
4396
    ';
4397
    my $sth = $dbh->prepare($query);
4398
    $sth->execute();
4399
    while ( my $row = $sth->fetchrow_hashref ) {
4400
        $rule_count++;
4401
        if ( defined $row->{tagsubfield} and $row->{tagsubfield} ) {
4402
            $perms{ $row->{tagfield} }->{subfields}->{ $row->{tagsubfield} }
4403
              ->{on_existing}->{ $row->{name} }->{ $row->{filter} } =
4404
              { action => $row->{on_existing}, rule => $row->{'id'} };
4405
            $perms{ $row->{tagfield} }->{subfields}->{ $row->{tagsubfield} }
4406
              ->{on_new}->{ $row->{name} }->{ $row->{filter} } =
4407
              { action => $row->{on_new}, rule => $row->{'id'} };
4408
            $perms{ $row->{tagfield} }->{subfields}->{ $row->{tagsubfield} }
4409
              ->{on_removed}->{ $row->{name} }->{ $row->{filter} } =
4410
              { action => $row->{on_removed}, rule => $row->{'id'} };
4411
        }
4412
        else {
4413
            $perms{ $row->{tagfield} }->{on_existing}->{ $row->{name} }
4414
              ->{ $row->{filter} } =
4415
              { action => $row->{on_existing}, rule => $row->{'id'} };
4416
            $perms{ $row->{tagfield} }->{on_new}->{ $row->{name} }
4417
              ->{ $row->{filter} } =
4418
              { action => $row->{on_new}, rule => $row->{'id'} };
4419
            $perms{ $row->{tagfield} }->{on_removed}->{ $row->{name} }
4420
              ->{ $row->{filter} } =
4421
              { action => $row->{on_removed}, rule => $row->{'id'} };
4422
        }
4423
    }
4424
4425
    return unless $rule_count;
4426
    return \%perms;
4427
}
4428
4429
=head2 GetMarcPermissionsAction
4430
4431
    my $action = GetMarcPermissionsAction($event, $filter, @permissions)
4432
4433
Gets action based on C<$event>, C<$filter> and C<@permissions>.
4434
4435
=over 4
4436
4437
=item C<$event>
4438
4439
which event: 'on_existing', 'on_new' or 'on_removed'
4440
4441
=item C<$filter>
4442
4443
hashref containing at least one filter module from the marc_permissions_modules
4444
table in form {module => filter}. Three predefined filter modules exists:
4445
4446
    * source
4447
    * category
4448
    * borrower
4449
4450
=item C<@permissions>
4451
4452
list of permission structures in order of specificity from least to most
4453
specific.
4454
4455
=back
4456
4457
Returns:
4458
4459
=over 4
4460
4461
=item C<$action>
4462
4463
hashref defining matching action.
4464
4465
=back
4466
4467
=cut
4468
4469
sub GetMarcPermissionsAction {
4470
    my $what = shift or return;
4471
    my $filter = shift or return;
4472
    my @perms = @_;
4473
    my $modules = GetMarcPermissionsModules();
4474
    my $action = undef;
4475
4476
    for my $perm (@perms) {
4477
        next if not defined $perm;
4478
        next if not defined $perm->{$what};
4479
        my $tmp_action = undef;
4480
        for my $module ( @{$modules} ) {
4481
            my $action_candidate = defined $perm->{$what}->{ $module->{'name'} } ? $perm->{$what}->{ $module->{'name'} } : undef;
4482
            if ($action_candidate) {
4483
                if (
4484
                    defined $filter->{ $module->{'name'} } and
4485
                    defined $action_candidate->{ $filter->{ $module->{'name'} } }->{action}
4486
                ) {
4487
                    $tmp_action = $perm->{$what}->{ $module->{'name'} }->{ $filter->{ $module->{'name'} } };
4488
                    last;
4489
                }
4490
                elsif (defined $action_candidate->{'*'}->{action} and !$tmp_action) {
4491
                    $tmp_action = $perm->{$what}->{ $module->{'name'} }->{'*'};
4492
                }
4493
            }
4494
        }
4495
        $action = $tmp_action if defined $tmp_action;
4496
    }
4497
    return $action;
4498
}
4499
4500
=head2 GetMarcPermissionsRules
4501
4502
    my $rules = GetMarcPermissionsRules()
4503
4504
Returns:
4505
4506
=over 4
4507
4508
=item C<$rules>
4509
4510
array (in list context, arrayref otherwise) of hashrefs from marc_permissions
4511
table in order of module specificity and rule id.
4512
4513
=back
4514
4515
=cut
4516
4517
sub GetMarcPermissionsRules {
4518
    my $dbh = C4::Context->dbh;
4519
    my @rules = ();
4520
4521
    my $query = '
4522
    SELECT `marc_permissions`.`id`,
4523
           `marc_permissions`.`tagfield`,
4524
           `marc_permissions`.`tagsubfield`,
4525
           `marc_permissions`.`filter`,
4526
           `marc_permissions`.`on_existing`,
4527
           `marc_permissions`.`on_new`,
4528
           `marc_permissions`.`on_removed`,
4529
           `marc_permissions_modules`.`name` as  `module`,
4530
           `marc_permissions_modules`.`description`,
4531
           `marc_permissions_modules`.`specificity`
4532
    FROM `marc_permissions`
4533
    LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id`
4534
    ORDER BY `marc_permissions_modules`.`specificity`, `id`
4535
    ';
4536
    my $sth = $dbh->prepare($query);
4537
    $sth->execute();
4538
    while ( my $row = $sth->fetchrow_hashref ) {
4539
        push(@rules, $row);
4540
    }
4541
4542
    return wantarray ? @rules : \@rules;
4543
}
4544
4545
=head2 GetMarcPermissionsModules
4546
4547
    my $modules = GetMarcPermissionsModules()
4548
4549
Returns:
4550
4551
=over 4
4552
4553
=item C<$modules>
4554
4555
array (in list context, arrayref otherwise) of hashrefs from
4556
marc_permissions_modules table in order of specificity.
4557
4558
=back
4559
4560
=cut
4561
4562
sub GetMarcPermissionsModules {
4563
    my $dbh = C4::Context->dbh;
4564
    my @modules = ();
4565
4566
    my $query = '
4567
    SELECT *
4568
    FROM `marc_permissions_modules`
4569
    ORDER BY `specificity` DESC
4570
    ';
4571
    my $sth = $dbh->prepare($query);
4572
    $sth->execute();
4573
    while ( my $row = $sth->fetchrow_hashref ) {
4574
        push(@modules, $row);
4575
    }
4576
4577
    return wantarray ? @modules : \@modules;
4578
}
4579
4580
=head2 ModMarcPermissionsRule
4581
4582
    my $success = ModMarcPermissionsRule($id, $fields)
4583
4584
Modifies rule in the marc_permissions table.
4585
4586
=over 4
4587
4588
=item C<$id>
4589
4590
rule id to modify
4591
4592
=item C<$fields>
4593
4594
hashref defining the table fields
4595
4596
      * tagfield - required
4597
      * tagsubfield
4598
      * module - required
4599
      * filter - required
4600
      * on_existing - required
4601
      * on_new - required
4602
      * on_removed - required
4603
4604
=back
4605
4606
Returns:
4607
4608
=over 4
4609
4610
=item C<$success>
4611
4612
undef if an error occurs, otherwise true.
4613
4614
=back
4615
4616
=cut
4617
4618
sub ModMarcPermissionsRule {
4619
    my ($id, $f) = @_;
4620
    my $dbh = C4::Context->dbh;
4621
    my $query = '
4622
    UPDATE `marc_permissions`
4623
    SET
4624
      tagfield = ?,
4625
      tagsubfield = ?,
4626
      module = ?,
4627
      filter = ?,
4628
      on_existing = ?,
4629
      on_new = ?,
4630
      on_removed = ?
4631
    WHERE
4632
      id = ?
4633
    ';
4634
    my $sth = $dbh->prepare($query);
4635
    return $sth->execute (
4636
                      $f->{tagfield},
4637
                      $f->{tagsubfield},
4638
                      $f->{module},
4639
                      $f->{filter},
4640
                      $f->{on_existing},
4641
                      $f->{on_new},
4642
                      $f->{on_removed},
4643
                      $id
4644
                  );
4645
}
4646
4647
=head2 AddMarcPermissionsRule
4648
4649
    my $success = AddMarcPermissionsRule($fields)
4650
4651
Add rule to the marc_permissions table.
4652
4653
=over 4
4654
4655
=item C<$fields>
4656
4657
hashref defining the table fields
4658
4659
      tagfield - required
4660
      tagsubfield
4661
      module - required
4662
      filter - required
4663
      on_existing - required
4664
      on_new - required
4665
      on_removed - required
4666
4667
=back
4668
4669
Returns:
4670
4671
=over 4
4672
4673
=item C<$success>
4674
4675
undef if an error occurs, otherwise true.
4676
4677
=back
4678
4679
=cut
4680
4681
sub AddMarcPermissionsRule {
4682
    my $f = shift;
4683
    my $dbh = C4::Context->dbh;
4684
    my $query = '
4685
    INSERT INTO `marc_permissions`
4686
    (
4687
      tagfield,
4688
      tagsubfield,
4689
      module,
4690
      filter,
4691
      on_existing,
4692
      on_new,
4693
      on_removed
4694
    )
4695
    VALUES (?, ?, ?, ?, ?, ?, ?)
4696
    ';
4697
    my $sth = $dbh->prepare($query);
4698
    return $sth->execute (
4699
                      $f->{tagfield},
4700
                      $f->{tagsubfield},
4701
                      $f->{module},
4702
                      $f->{filter},
4703
                      $f->{on_existing},
4704
                      $f->{on_new},
4705
                      $f->{on_removed}
4706
                  );
4707
}
4708
4709
=head2 DelMarcPermissionsRule
4710
4711
    my $success = DelMarcPermissionsRule($id)
4712
4713
Deletes rule from the marc_permissions table.
4714
4715
=over 4
4716
4717
=item C<$id>
4718
4719
rule id to delete
4720
4721
=back
4722
4723
Returns:
4724
4725
=over 4
4726
4727
=item C<$success>
4728
4729
undef if an error occurs, otherwise true.
4730
4731
=back
4732
4733
=cut
4734
4735
sub DelMarcPermissionsRule {
4736
    my $id = shift;
4737
    my $dbh = C4::Context->dbh;
4738
    my $query = '
4739
    DELETE FROM `marc_permissions`
4740
    WHERE
4741
      id = ?
4742
    ';
4743
    my $sth = $dbh->prepare($query);
4744
    return $sth->execute($id);
4745
}
3789
1;
4746
1;
3790
4747
3791
4748
(-)a/C4/ImportBatch.pm (-1 / +1 lines)
Lines 674-680 sub BatchCommitRecords { Link Here
674
                }
674
                }
675
                $oldxml = $old_marc->as_xml($marc_type);
675
                $oldxml = $old_marc->as_xml($marc_type);
676
676
677
                ModBiblio($marc_record, $recordid, $oldbiblio->{'frameworkcode'});
677
                ModBiblio($marc_record, $recordid, $oldbiblio->{'frameworkcode'}, {filter => {source => 'batchimport'}});
678
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?";
678
                $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?";
679
679
680
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
680
                if ($item_result eq 'create_new' || $item_result eq 'replace') {
(-)a/C4/Installer/PerlDependencies.pm (+5 lines)
Lines 857-862 our $PERL_DEPS = { Link Here
857
        'required' => '0',
857
        'required' => '0',
858
        'min_ver'  => '0.07',
858
        'min_ver'  => '0.07',
859
    },
859
    },
860
    'String::Similarity' => {
861
        usage => 'cataloguing',
862
        required => 1,
863
        min_version => '1.04',
864
    },
860
};
865
};
861
866
862
1;
867
1;
(-)a/C4/Items.pm (+2 lines)
Lines 406-411 sub AddItemBatchFromMarc { Link Here
406
        logaction("CATALOGUING", "ADD", $itemnumber, "item") if C4::Context->preference("CataloguingLog"); 
406
        logaction("CATALOGUING", "ADD", $itemnumber, "item") if C4::Context->preference("CataloguingLog"); 
407
407
408
        my $new_item_marc = _marc_from_item_hash($item, $frameworkcode, $unlinked_item_subfields);
408
        my $new_item_marc = _marc_from_item_hash($item, $frameworkcode, $unlinked_item_subfields);
409
410
        # @FIXME: why modify clone record here since not even saving below?
409
        $item_field->replace_with($new_item_marc->field($itemtag));
411
        $item_field->replace_with($new_item_marc->field($itemtag));
410
    }
412
    }
411
413
(-)a/admin/marc-permissions.pl (+123 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 ( $template, $loggedinuser, $cookie ) = get_template_and_user(
44
    {
45
        template_name   => "admin/marc-permissions.tt",
46
        query           => $input,
47
        type            => "intranet",
48
        authnotrequired => 0,
49
        flagsrequired   => { parameters => 'parameters_remaining_permissions' },
50
        debug           => 1,
51
    }
52
);
53
54
my %cookies = parse CGI::Cookie($cookie);
55
our $sessionID = $cookies{'CGISESSID'}->value;
56
57
my $rules;
58
if ( $op eq "remove" ) {
59
    $template->{VARS}->{removeConfirm} = 1;
60
    my @removeIDs = $input->multi_param('batchremove');
61
    push( @removeIDs, scalar $input->param('id') ) if $input->param('id');
62
63
    $rules = GetMarcPermissionsRules();
64
    for my $removeID (@removeIDs) {
65
        map { $_->{'remove'} = 1 if $_->{'id'} == $removeID } @{$rules};
66
    }
67
68
}
69
elsif ( $op eq "doremove" ) {
70
    my @removeIDs = $input->multi_param('batchremove');
71
    push( @removeIDs, scalar $input->param('id') ) if $input->param('id');
72
    for my $removeID (@removeIDs) {
73
        DelMarcPermissionsRule($removeID);
74
    }
75
76
    $rules = GetMarcPermissionsRules();
77
78
}
79
elsif ( $op eq "edit" ) {
80
    $template->{VARS}->{edit} = 1;
81
    my $id = $input->param('id');
82
    $rules = GetMarcPermissionsRules();
83
    map { $_->{'edit'} = 1 if $_->{'id'} == $id } @{$rules};
84
85
}
86
elsif ( $op eq "doedit" ) {
87
    my $id     = $input->param('id');
88
    my $fields = {
89
        module      => scalar $input->param('module'),
90
        tagfield    => scalar $input->param('tagfield'),
91
        tagsubfield => scalar $input->param('tagsubfield'),
92
        filter      => scalar $input->param('filter'),
93
        on_existing => scalar $input->param('on_existing'),
94
        on_new      => scalar $input->param('on_new'),
95
        on_removed  => scalar $input->param('on_removed')
96
    };
97
98
    ModMarcPermissionsRule( $id, $fields );
99
    $rules = GetMarcPermissionsRules();
100
101
}
102
elsif ( $op eq "add" ) {
103
    my $fields = {
104
        module      => scalar $input->param('module'),
105
        tagfield    => scalar $input->param('tagfield'),
106
        tagsubfield => scalar $input->param('tagsubfield'),
107
        filter      => scalar $input->param('filter'),
108
        on_existing => scalar $input->param('on_existing'),
109
        on_new      => scalar $input->param('on_new'),
110
        on_removed  => scalar $input->param('on_removed')
111
    };
112
113
    AddMarcPermissionsRule($fields);
114
    $rules = GetMarcPermissionsRules();
115
116
}
117
else {
118
    $rules = GetMarcPermissionsRules();
119
}
120
my $modules = GetMarcPermissionsModules();
121
$template->param( rules => $rules, modules => $modules );
122
123
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/cataloguing/addbiblio.pl (-1 / +9 lines)
Lines 837-843 if ( $op eq "addbiblio" ) { Link Here
837
            BiblioAutoLink( $record, $frameworkcode );
837
            BiblioAutoLink( $record, $frameworkcode );
838
        } 
838
        } 
839
        if ( $is_a_modif ) {
839
        if ( $is_a_modif ) {
840
            ModBiblio( $record, $biblionumber, $frameworkcode );
840
            my ($member) = C4::Members::GetMember('borrowernumber' => $loggedinuser);
841
            ModBiblio( $record, $biblionumber, $frameworkcode,
842
                {
843
                    source => $z3950 ? 'z39.50' : 'intranet',
844
                    category => $member->{'category_type'},
845
                    borrower => $loggedinuser
846
                }
847
            );
841
        }
848
        }
842
        else {
849
        else {
843
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
850
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
Lines 930-935 elsif ( $op eq "delete" ) { Link Here
930
    $template->param(
937
    $template->param(
931
        biblionumberdata => $biblionumber,
938
        biblionumberdata => $biblionumber,
932
        op               => $op,
939
        op               => $op,
940
        z3950            => $z3950
933
    );
941
    );
934
    if ( $op eq "duplicate" ) {
942
    if ( $op eq "duplicate" ) {
935
        $biblionumber = "";
943
        $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 `marc_permissions` (
21
    `id` int(11) NOT NULL auto_increment,
22
    `tagfield` varchar(255) NOT NULL, -- can be regexe, so need > 3 chars
23
    `tagsubfield` varchar(255) DEFAULT NULL, -- can be regex, so need > 1 char
24
    `module` int(11) NOT NULL,
25
    `filter` varchar(255) NOT NULL,
26
    `on_existing` ENUM('skip', 'overwrite', 'add', 'add_or_correct') DEFAULT NULL,
27
    `on_new` ENUM('skip', 'add') DEFAULT NULL,
28
    `on_removed` ENUM('skip', 'remove') 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 50-55 Link Here
50
    <li><a href="/cgi-bin/koha/admin/matching-rules.pl">Record matching rules</a></li>
50
    <li><a href="/cgi-bin/koha/admin/matching-rules.pl">Record matching rules</a></li>
51
    <li><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></li>
51
    <li><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></li>
52
    <li><a href="/cgi-bin/koha/admin/items_search_fields.pl">Item search fields</a></li>
52
    <li><a href="/cgi-bin/koha/admin/items_search_fields.pl">Item search fields</a></li>
53
    <li><a href="/cgi-bin/koha/admin/marc-permissions.pl">MARC field permissions</a></li>
53
</ul>
54
</ul>
54
55
55
<h5>Acquisition parameters</h5>
56
<h5>Acquisition parameters</h5>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+2 lines)
Lines 89-94 Link Here
89
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
89
                        <dt><a href="/cgi-bin/koha/admin/searchengine/elasticsearch/mappings.pl">Search engine configuration</a></dt>
90
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
90
                        <dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
91
                    [% END %]
91
                    [% END %]
92
                    <dt><a href="/cgi-bin/koha/admin/marc-permissions.pl">MARC field permissions</a></dt>
93
                    <dd>Managed MARC field permissions</dd>
92
                </dl>
94
                </dl>
93
95
94
                <h3>Acquisition parameters</h3>
96
                <h3>Acquisition parameters</h3>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc-permissions.tt (+314 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 some options if subfield is indicator */
68
    $('input[name="tagsubfield"]').change(function() {
69
        if( /i[0-9]/.test($(this).val()) ) {
70
            $('select[name="on_existing"] option[value="add"]').attr('disabled', 'disabled')
71
            $('select[name="on_existing"] option[value="add_or_correct"]').attr('disabled', 'disabled')
72
        } else {
73
            $('select[name="on_existing"] option[value="add"]').removeAttr('disabled')
74
            $('select[name="on_existing"] option[value="add_or_correct"]').removeAttr('disabled')
75
        }
76
    });
77
78
    /* disable batch remove unless one or more checkboxes are checked */
79
    $('input[name="batchremove"]').change(function() {
80
        if($('input[name="batchremove"]:checked').length > 0) {
81
            $('#btn_batchremove').removeAttr('disabled');
82
        } else {
83
            $('#btn_batchremove').attr('disabled', 'disabled');
84
        }
85
    });
86
87
    /* check validity of regexes in field and subfield inputs */
88
    $('input[name="tagfield"], input[name="tagsubfield"]').change(function() {
89
        $(this).removeClass('required');
90
        $(this).attr('title', '');
91
        if($(this).val() != '*') {
92
            try {
93
                new RegExp($(this).val())
94
            } catch ( e ) {
95
                $(this).addClass('required');
96
                $(this).attr('title', 'Invalid regular expression: ' + e.message);
97
            }
98
        }
99
    });
100
101
    $.fn.dataTable.ext.order['dom-input'] = function (settings, col) {
102
        return this.api().column(col, { order: 'index' }).nodes()
103
            .map(function (td, i) {
104
                if($('input', td).val() != undefined) {
105
                    return $('input', td).val();
106
                } else if($('select', td).val() != undefined) {
107
                    return $('option[selected="selected"]', td).val();
108
                } else {
109
                    return $(td).html();
110
                }
111
            });
112
    }
113
114
    $('#marc-permissions').dataTable($.extend(true, {}, dataTablesDefaults, {
115
        "aoColumns": [
116
            {"bSearchable": false, "bSortable": false},
117
            {"sSortDataType": "dom-input"},
118
            {"sSortDataType": "dom-input"},
119
            {"bSearchable": false, "sSortDataType": "dom-input"},
120
            {"bSearchable": false, "sSortDataType": "dom-input"},
121
            {"bSearchable": false, "sSortDataType": "dom-input"},
122
            {"bSearchable": false, "sSortDataType": "dom-input"},
123
            {"bSearchable": false, "sSortDataType": "dom-input"},
124
            {"bSearchable": false, "bSortable": false},
125
            {"bSearchable": false, "bSortable": false}
126
        ],
127
        "sPaginationType": "four_button"
128
    }));
129
130
});
131
//]]>
132
</script>
133
</head>
134
<body id="admin_marc-permissions" class="admin">
135
[% INCLUDE 'header.inc' %]
136
[% INCLUDE 'cat-search.inc' %]
137
138
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
139
 &rsaquo; MARC field permissions
140
</div>
141
142
<div id="doc3" class="yui-t2">
143
   <div id="bd">
144
    <div id="yui-main">
145
    <div class="yui-b">
146
147
<h1>Manage MARC field permissions</h1>
148
149
[% UNLESS Koha.Preference( 'MARCPermissions' ) %]
150
    <div class="dialog message">
151
        The <b>MARCPermissions</b> preference is not set, don't forget to enable it for rules to take effect.
152
    </div>
153
[% END %]
154
[% IF removeConfirm %]
155
<div class="dialog alert">
156
<h3>Remove rule?</h3>
157
<p>Are you sure you want to remove the selected rule(s)?</p>
158
159
<form action="[% script_name %]" method="GET">
160
    <input type="submit" value="No, do not remove" class="deny"/>
161
</form>
162
<input type="button" value="Yes, remove" class="approve" id="doremove" />
163
</div>
164
[% END %]
165
166
<form action="[% script_name %]" method="POST" id="marc-permissions-form">
167
<table id="marc-permissions">
168
    <thead><tr>
169
        <th>Rule</th>
170
        <th>Tag</th>
171
        <th>Subfield tag</th>
172
        <th>Module</th>
173
        <th>Filter</th>
174
        <th>On Existing</th>
175
        <th>On New</th>
176
        <th>On Removed</th>
177
        <th>Actions</th>
178
        <th>&nbsp;</th>
179
    </tr></thead>
180
    [% UNLESS edit %]
181
    <tfoot>
182
        <tr>
183
            <th>&nbsp;</th>
184
            <th><input type="text" size="5" name="tagfield"/></th>
185
            <th><input type="text" size="5" name="tagsubfield"/></th>
186
            <th>
187
                <select name="module">
188
                    [% FOREACH module IN modules %]
189
                    <option value="[% module.id %]">[% module.name %]</option>
190
                    [% END %]
191
                </select>
192
            </th>
193
            <th><input type="text" size="5" name="filter"/></th>
194
            <th>
195
                <select name="on_existing">
196
                [% FOR on_ex IN ['skip', 'overwrite', 'add', 'add_or_correct'] %]
197
                    <option value="[% on_ex %]">[% on_ex %]</option>
198
                [% END %]
199
                </select>
200
            </th>
201
            <th>
202
                <select name="on_new">
203
                [% FOR on_new IN ['skip', 'add'] %]
204
                    <option value="[% on_new %]">[% on_new %]</option>
205
                [% END %]
206
                </select>
207
            </th>
208
            <th>
209
                <select name="on_removed">
210
                [% FOR on_removed IN ['skip', 'remove'] %]
211
                    <option value="[% on_removed %]">[% on_removed %]</option>
212
                [% END %]
213
                </select>
214
            </th>
215
            <th><button class="btn btn-small" title="Add" id="add"><i class="fa fa-plus"></i> Add rule</button></th>
216
            <th><button id="btn_batchremove" disabled="disabled" class="btn btn-small" title="Batch remove"><i class="fa fa-trash"></i> Delete selected</button></th>
217
        </tr>
218
    </tfoot>
219
    [% END %]
220
    <tbody>
221
        [% FOREACH rule IN rules %]
222
            <tr id="[% rule.id %]">
223
            [% IF rule.edit %]
224
                <td>[% rule.id %]</td>
225
                <td><input type="text" size="3" name="tagfield" value="[% rule.tagfield %]"/></td>
226
                <td><input type="text" size="1" name="tagsubfield" value="[% rule.tagsubfield %]"/></td>
227
                <td>
228
                    <select name="module">
229
                        [% FOREACH module IN modules %]
230
                            [% IF module.name == rule.module %]
231
                                <option value="[% module.id %]" selected="selected">[% module.name %]</option>
232
                            [% ELSE %]
233
                                <option value="[% module.id %]">[% module.name %]</option>
234
                            [% END %]
235
                        [% END %]
236
                    </select>
237
                </td>
238
                <td><input type="text" size="5" name="filter" value="[% rule.filter %]"/></td>
239
                <td>
240
                    <select name="on_existing">
241
                        [% FOR on_ex IN ['skip', 'overwrite', 'add', 'add_or_correct'] %]
242
                            [% IF on_ex == rule.on_existing %]
243
                                <option value="[% on_ex %]" selected="selected">[% on_ex %]</option>
244
                            [% ELSE %]
245
                                <option value="[% on_ex %]">[% on_ex %]</option>
246
                            [% END %]
247
                        [% END %]
248
                    </select>
249
                </td>
250
                <td>
251
                    <select name="on_new">
252
                        [% FOR on_new IN ['skip', 'add'] %]
253
                            [% IF on_new == rule.on_new %]
254
                                <option value="[% on_new %]" selected="selected">[% on_new %]</option>
255
                            [% ELSE %]
256
                                <option value="[% on_new %]">[% on_new %]</option>
257
                            [% END %]
258
                        [% END %]
259
                    </select>
260
                </td>
261
                <td>
262
                    <select name="on_removed">
263
                        [% FOR on_removed IN ['skip', 'remove'] %]
264
                            [% IF on_removed == rule.on_removed %]
265
                                <option value="[% on_removed %]" selected="selected">[% on_removed %]</option>
266
                            [% ELSE %]
267
                                <option value="[% on_removed %]">[% on_removed %]</option>
268
                            [% END %]
269
                        [% END %]
270
                    </select>
271
                </td>
272
                <td class="actions">
273
                    <button class="btn btn-mini" title="Save" id="doedit" value="[% rule.id %]"><i class="fa fa-check"></i> Save</button>
274
                    <a href="?"><button class="btn btn-mini" title="Cancel" ><i class="fa fa-times"></i> Cancel</button></a>
275
                </td>
276
                <td></td>
277
            [% ELSE %]
278
                <td>[% rule.id %]</td>
279
                <td>[% rule.tagfield %]</td>
280
                <td>[% rule.tagsubfield%]</td>
281
                <td>[% rule.module %]</td>
282
                <td>[% rule.filter %]</td>
283
                <td>[% rule.on_existing %]</td>
284
                <td>[% rule.on_new %]</td>
285
                <td>[% rule.on_removed %]</td>
286
                <td class="actions">
287
                    <a href="?op=remove&id=[% rule.id %]" title="Delete" class="btn btn-mini"><i class="fa fa-trash"></i> Delete</a>
288
                    <a href="?op=edit&id=[% rule.id %]" title="Edit" class="btn btn-mini"><i class="fa fa-pencil"></i> Edit</a>
289
                </td>
290
                <td>
291
                    [% IF rule.remove %]
292
                    <input type="checkbox" name="batchremove" value="[% rule.id %]" checked="checked"/>
293
                    [% ELSE %]
294
                    <input type="checkbox" name="batchremove" value="[% rule.id %]"/>
295
                    [% END %]
296
                </td>
297
            [% END %]
298
            </tr>
299
        [% END %]
300
    </tbody>
301
</table>
302
</form>
303
304
<form action="[% script_name %]" method="post">
305
<input type="hidden" name="op" value="redo-matching" />
306
</form>
307
308
</div>
309
</div>
310
<div class="yui-b">
311
[% INCLUDE 'admin-menu.inc' %]
312
</div>
313
</div>
314
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref (-21 / +9 lines)
Lines 234-257 Cataloging: Link Here
234
234
235
    Exporting:
235
    Exporting:
236
        -
236
        -
237
            - Include following fields when exporting BibTeX,
237
            - When importing records
238
            - pref: BibtexExportAdditionalFields
238
            - pref: MARCPermissions
239
              type: textarea
239
              choices:
240
            - "Use one line per tag in the format BT_TAG: TAG$SUBFIELD ( e.g. lccn: 010$a )"
240
                  yes: "use"
241
            - "<br/>"
241
                  no: "don't use"
242
            - "To specificy multiple marc tags/subfields as targets for a repeating BibTex tag, use the following format: BT_TAG: [TAG2$SUBFIELD1, TAG2$SUBFIELD2] ( e.g. notes: [501$a, 505$g] )"
242
            - MARC permissions rules to decide which action to take for each field.
243
            - "<br/>"
243
            - When <b>add_or_correct</b>, consider a field modification to be a correction if the old and new value has a similarity of
244
            - "All values of repeating tags and subfields will be printed with the given BibTeX tag."
244
            - pref: MARCPermissionsCorrectionSimilarity
245
            - "<br/>"
245
            - %. If similarity falls below this value, the new value will be treated as a new field.
246
            - "Use '@' ( with quotes ) as the BT_TAG to replace the bibtex record type with a field value of your choosing."
247
        -
248
            - Include following fields when exporting RIS,
249
            - pref: RisExportAdditionalFields
250
              type: textarea
251
            - "Use one line per tag in the format RIS_TAG: TAG$SUBFIELD ( e.g. LC: 010$a )"
252
            - "<br/>"
253
            - "To specificy multiple marc tags/subfields as targets for a repeating RIS tag, use the following format: RIS_TAG: [TAG2$SUBFIELD1, TAG2$SUBFIELD2] ( e.g. NT: [501$a, 505$g] )"
254
            - "<br/>"
255
            - "All values of repeating tags and subfields will be printed with the given RIS tag."
256
            - "<br/>"
257
            - "Use of TY ( record type ) as a key will <i>replace</i> the default TY with the field value of your choosing."
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/logs.pref (+6 lines)
Lines 66-71 Logging: Link Here
66
                  on: Log
66
                  on: Log
67
                  off: "Don't log"
67
                  off: "Don't log"
68
            - when reports are added, deleted or changed.
68
            - when reports are added, deleted or changed.
69
        -
70
            - pref: MARCPermissionsLog
71
              choices:
72
                  on: Log
73
                  off: "Don't log"
74
            - MARC permissions rule evaluations when applying rules for MARC field modifications.
69
    Debugging:
75
    Debugging:
70
        -
76
        -
71
            - pref: DumpTemplateVarsIntranet
77
            - pref: DumpTemplateVarsIntranet
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt (+1 lines)
Lines 545-550 function Changefwk() { Link Here
545
[% END %]
545
[% END %]
546
        <input type="hidden" name="op" value="addbiblio" />
546
        <input type="hidden" name="op" value="addbiblio" />
547
        <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode %]" />
547
        <input type="hidden" id="frameworkcode" name="frameworkcode" value="[% frameworkcode %]" />
548
        <input type="hidden" name="z3950" value="[% z3950 %]" />
548
        <input type="hidden" name="biblionumber" value="[% biblionumber %]" />
549
        <input type="hidden" name="biblionumber" value="[% biblionumber %]" />
549
        <input type="hidden" name="breedingid" value="[% breedingid %]" />
550
        <input type="hidden" name="breedingid" value="[% breedingid %]" />
550
        <input type="hidden" name="changed_framework" value="" />
551
        <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 417-423 RECORD: while ( ) { Link Here
417
			}
417
			}
418
					# create biblio, unless we already have it ( either match or isbn )
418
					# create biblio, unless we already have it ( either match or isbn )
419
            if ($biblionumber) {
419
            if ($biblionumber) {
420
                eval{$biblioitemnumber=GetBiblioData($biblionumber)->{biblioitemnumber};};
420
                eval { ( $biblionumber, $biblioitemnumber ) = ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber), {filter => {source => 'bulkmarcimport'}}) };
421
                if ($update) {
421
                if ($update) {
422
                    eval { ( $biblionumber, $biblioitemnumber ) = ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber) ) };
422
                    eval { ( $biblionumber, $biblioitemnumber ) = ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber) ) };
423
                    if ($@) {
423
                    if ($@) {
Lines 805-808 from the migration_tools directory. Link Here
805
=back
805
=back
806
806
807
=cut
807
=cut
808
(-)a/misc/migration_tools/import_lexile.pl (-1 / +2 lines)
Lines 151-156 while ( my $row = $csv->getline_hr($fh) ) { Link Here
151
    foreach my $biblionumber (@biblionumbers) {
151
    foreach my $biblionumber (@biblionumbers) {
152
        $counter++;
152
        $counter++;
153
        my $record = GetMarcBiblio($biblionumber);
153
        my $record = GetMarcBiblio($biblionumber);
154
        my $frameworkcode = GetFrameworkCode($biblionumber);
154
155
155
        if ($verbose) {
156
        if ($verbose) {
156
            say "Found matching record! Biblionumber: $biblionumber";
157
            say "Found matching record! Biblionumber: $biblionumber";
Lines 200-206 while ( my $row = $csv->getline_hr($fh) ) { Link Here
200
            $record->append_fields($field);
201
            $record->append_fields($field);
201
        }
202
        }
202
203
203
        ModBiblio( $record, $biblionumber ) unless ( $test );
204
        ModBiblio( $record, $biblionumber, $frameworkcode, {filter => {source => 'import_lexile'}} ) unless ( $test );
204
    }
205
    }
205
206
206
}
207
}
(-)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 156-162 if ( $op eq 'form' ) { Link Here
156
    my ( $job );
156
    my ( $job );
157
    if ( $runinbackground ) {
157
    if ( $runinbackground ) {
158
        my $job_size = scalar( @record_ids );
158
        my $job_size = scalar( @record_ids );
159
        $job = C4::BackgroundJob->new( $sessionID, "FIXME", '/cgi-bin/koha/tools/batch_record_modification.pl', $job_size );
159
        $job = C4::BackgroundJob->new( $sessionID, "FIXME", $ENV{SCRIPT_NAME}, $job_size );
160
        my $job_id = $job->id;
160
        my $job_id = $job->id;
161
        if (my $pid = fork) {
161
        if (my $pid = fork) {
162
            $dbh->{InactiveDestroy}  = 1;
162
            $dbh->{InactiveDestroy}  = 1;
Lines 168-174 if ( $op eq 'form' ) { Link Here
168
        } elsif (defined $pid) {
168
        } elsif (defined $pid) {
169
            close STDOUT;
169
            close STDOUT;
170
        } else {
170
        } else {
171
            warn "fork failed while attempting to run tools/batch_record_modification.pl as a background job";
171
            warn "fork failed while attempting to run $ENV{'SCRIPT_NAME'} as a background job";
172
            exit 0;
172
            exit 0;
173
        }
173
        }
174
    }
174
    }
Lines 192-198 if ( $op eq 'form' ) { Link Here
192
                my $record = GetMarcBiblio( $biblionumber );
192
                my $record = GetMarcBiblio( $biblionumber );
193
                ModifyRecordWithTemplate( $mmtid, $record );
193
                ModifyRecordWithTemplate( $mmtid, $record );
194
                my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
194
                my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
195
                ModBiblio( $record, $biblionumber, $frameworkcode );
195
                my ($member) = C4::Members::GetMember('borrowernumber' => $loggedinuser);
196
                ModBiblio( $record, $biblionumber, $frameworkcode,
197
                    {
198
                        source => 'batchmod',
199
                        category => $member->{'category_type'},
200
                        borrower => $loggedinuser
201
                    }
202
                );
196
            };
203
            };
197
            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
204
            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
198
                push @messages, {
205
                push @messages, {
199
- 

Return to bug 14957