@@ -, +, @@ 1. Apply this patch 2. Run updatedatabase.pl 3. Log in to staff client 4. Enable new syspref MARCPermissions 5. You should see a new link "MARC field permissions" in the "Catalog" section under "Koha administrtion". 6. Go to the "MARC field permissions" page 7. Next to the page heading ("Manage MARC field permissions") there is a button with a question mark, click this for more information about the rule system. 8. Add desired rules to the rule table, press the +-button to add. 9. Clicking the pen-icon (edit) should allow you to edit corresponding rule. 10. Clicking the x-icon should remove corresponding rule after confirmation. 11. Selecting one or more rules followed by clicking the trash-icon should remove all selected rules after confirmation. 12. To test your rules, import or in other way modify a record. 13. Check that the record was modified as defined by your rules. --- C4/Biblio.pm | 615 ++++++++++++++++++++- C4/ImportBatch.pm | 2 +- admin/marc-permissions.pl | 115 ++++ cataloguing/addbiblio.pl | 12 +- help.pl | 5 + .../bug_14957-marc-permissions-syspref.sql | 2 + .../atomicupdate/bug_14957-marc-permissions.sql | 31 ++ .../intranet-tmpl/prog/en/includes/admin-menu.inc | 1 + .../prog/en/modules/admin/admin-home.tt | 2 + .../prog/en/modules/admin/marc-permissions.tt | 320 +++++++++++ .../en/modules/admin/preferences/cataloguing.pref | 7 + .../prog/en/modules/admin/preferences/logs.pref | 6 + .../prog/en/modules/cataloguing/addbiblio.tt | 1 + .../prog/en/modules/help/marc-permissions.tt | 148 +++++ misc/migration_tools/bulkmarcimport.pl | 2 +- misc/migration_tools/import_lexile.pl | 3 +- t/db_dependent/Biblio/MARCPermissions.t | 279 ++++++++++ tools/batch_record_modification.pl | 13 +- 18 files changed, 1549 insertions(+), 15 deletions(-) create mode 100755 admin/marc-permissions.pl create mode 100644 installer/data/mysql/atomicupdate/bug_14957-marc-permissions-syspref.sql create mode 100644 installer/data/mysql/atomicupdate/bug_14957-marc-permissions.sql create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc-permissions.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/help/marc-permissions.tt create mode 100755 t/db_dependent/Biblio/MARCPermissions.t --- a/C4/Biblio.pm +++ a/C4/Biblio.pm @@ -99,6 +99,12 @@ BEGIN { &CountItemsIssued &CountBiblioInOrders + + &GetMarcPermissionsRules + &GetMarcPermissionsModules + &ModMarcPermissionsRule + &AddMarcPermissionsRule + &DelMarcPermissionsRule ); # To modify something @@ -126,6 +132,7 @@ BEGIN { # they are useful in a few circumstances, so they are exported, # but don't use them unless you are a core developer ;-) push @EXPORT, qw( + &ApplyMarcPermissions &ModBiblioMarc ); @@ -263,7 +270,7 @@ sub AddBiblio { =head2 ModBiblio - ModBiblio( $record,$biblionumber,$frameworkcode); + ModBiblio($record, $biblionumber, $frameworkcode, $options); Replace an existing bib record identified by C<$biblionumber> with one supplied by the MARC::Record object C<$record>. The embedded @@ -284,7 +291,9 @@ Returns 1 on success 0 on failure =cut sub ModBiblio { - my ( $record, $biblionumber, $frameworkcode ) = @_; + my ( $record, $biblionumber, $frameworkcode, $options ) = @_; + $options //= {}; + if (!$record) { carp 'No record passed to ModBiblio'; return 0; @@ -312,6 +321,16 @@ sub ModBiblio { _strip_item_fields($record, $frameworkcode); + # apply permissions + if (C4::Context->preference('MARCPermissions') && $biblionumber && defined $options && exists $options->{'context'}) { + $record = ApplyMarcPermissions({ + biblionumber => $biblionumber, + record => $record, + filter => $options->{'context'}, + } + ); + } + # update biblionumber and biblioitemnumber in MARC # FIXME - this is assuming a 1 to 1 relationship between # biblios and biblioitems @@ -322,13 +341,13 @@ sub ModBiblio { _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber ); # load the koha-table data object - my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode ); + my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode, undef, $biblionumber); # update MARC subfield that stores biblioitems.cn_sort _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode ); # update the MARC record (that now contains biblio and items) with the new record data - &ModBiblioMarc( $record, $biblionumber, $frameworkcode ); + ModBiblioMarc( $record, $biblionumber, $frameworkcode ); # modify the other koha tables _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode ); @@ -2508,7 +2527,7 @@ sub TransformHtmlToMarc { =head2 TransformMarcToKoha - $result = TransformMarcToKoha( $record, undef, $limit ) + $result = TransformMarcToKoha($record, undef, $biblionumber, $option) Extract data from a MARC bib record into a hashref representing Koha biblio, biblioitems, and items fields. @@ -2519,7 +2538,7 @@ hash_ref. =cut sub TransformMarcToKoha { - my ( $record, $frameworkcode, $limit_table ) = @_; + my ( $record, $frameworkcode, $limit_table, $biblionumber ) = @_; # FIXME Parameter $frameworkcode is obsolete and will be removed $limit_table //= q{}; @@ -3206,7 +3225,7 @@ sub _koha_delete_biblio_metadata { =head2 ModBiblioMarc - &ModBiblioMarc($newrec,$biblionumber,$frameworkcode); + &ModBiblioMarc($newrec, $biblionumber, $frameworkcode, $options); Add MARC XML data for a biblio to koha @@ -3533,8 +3552,588 @@ sub RemoveAllNsb { return $record; } -1; +=head2 ApplyMarcPermissions + + my $record = ApplyMarcPermissions($arguments) + +Applies marc permission rules to a record. + +C<$arguments> is expected to be a hashref with below keys defined. + +=over 4 + +=item C +biblionumber of old record + +=item C +record that will modify old record + +=item C +only tags included in framework will be processed + +=item C +hashref containing at least one filter module from the marc_permissions_modules +table in form {module => filter}. Three predefined filter modules exists: + + * source + * category + * borrower + +=item C +optional reference to array that will be filled with rule evaluation log +entries. + +=item C +optional boolean which when true disables logging to action log. + +=back + +Returns: + +=over 4 + +=item C<$record> + +new MARC record based on C with C applied. If no old +record for C can be found, C is returned unchanged. +Default action when no matching filter found is to leave old record unchanged. + +=back + +=cut + +sub ApplyMarcPermissions { + my ($arguments) = @_; + my $biblionumber = $arguments->{biblionumber}; + my $incoming_record = $arguments->{record}; + + if ( !$biblionumber ) { + carp 'ApplyMarcPermissions called on undefined biblionumber'; + return; + } + if ( !$incoming_record ) { + carp 'ApplyMarcPermissions called on undefined record'; + return; + } + my $old_record = GetMarcBiblio({ biblionumber => $biblionumber }); + + my $merge_rules = undef; + if ($old_record && $arguments->{filter} && ($merge_rules = GetMarcPermissions($arguments->{filter}))) { + return MergeRecords($old_record, $incoming_record, $merge_rules); + } + return $incoming_record; +} + +sub MergeRecords { + my ($old_record, $incoming_record, $merge_rules) = @_; + my $is_matching_regex = sub { + my ( $tag, $m ) = @_; + + # tag is not exactly same as possible regex + $tag ne $m && + + # wildcard + $m ne '*' && + + # valid tagDataType + $m !~ /^(0[1-9A-z][\dA-Z]) | + ([1-9A-z][\dA-z]{2})$/x && + + # nor valid controltagDataType + $m !~ /00[1-9A-Za-z]{1}/ && + + # so we try it as a regex + $tag =~ /^$m$/ + }; + + my $fields_by_tag = sub { + my ($record) = @_; + my $fields = {}; + foreach my $field ($record->fields()) { + $fields->{$field->tag()} //= []; + push @{$fields->{$field->tag()}}, $field; + } + return $fields; + }; + + my $hash_field_data = sub { + my ($field) = @_; + my $indicators = join("\x1E", map { $field->indicator($_) } (1, 2)); + return $indicators . "\x1E" . join("\x1E", sort map { join "\x1E", @{$_} } $field->subfields()); + }; + + my $diff_by_key = sub { + my ($a, $b) = @_; + my @removed; + my @intersecting; + my @added; + my %keys_index = map { $_ => undef } (keys %{$a}, keys %{$b}); + foreach my $key (keys %keys_index) { + if ($a->{$key} && $b->{$key}) { + push @intersecting, $a->{$key}; + } + elsif ($a->{$key}) { + push @removed, $a->{$key}; + } + else { + push @added, $b->{$key}; + } + } + return (\@removed, \@intersecting, \@added); + }; + + my $get_matching_field_rule = sub { + my ($tag) = @_; + my $matched_rule = undef; + # Exact match takes precedence + if (exists $merge_rules->{$tag}) { + $matched_rule = $merge_rules->{$tag}; + } + else { + # TODO: sorty by module/weight rule id or something, grab first matching via listutils thingy + my @matching_rules = map { $merge_rules->{$_} } grep { $is_matching_regex->($tag, $_) } sort keys %{$merge_rules}; + # TODO: fix + if (@matching_rules) { + $matched_rule = pop @matching_rules; + } + elsif($merge_rules->{'*'}) { + $matched_rule = $merge_rules->{'*'}; + } + } + return $matched_rule; + }; + + my $merged_record = MARC::Record->new(); + my @merged_record_fields; + + # Leader is always overwritten, or kept??? + $merged_record->leader($incoming_record->leader()); + + my $current_fields = $fields_by_tag->($old_record); + my $incoming_fields = $fields_by_tag->($incoming_record); + + # First we get all new incoming control fields + my @new_field_tags = grep { !(exists $current_fields->{$_}) } keys %{$incoming_fields}; + + foreach my $tag (@new_field_tags) { + my $rule = $get_matching_field_rule->($tag) // { + on_new => {'action' => 'skip', 'rule' => 0} + }; + if ( + $rule->{on_new}->{action} eq 'add' || + $rule->{on_new}->{action} eq 'overwrite' # ??? + ) { # Or could just be write/protect? + # Hmm, only one control field possible?? + push @merged_record_fields, @{$incoming_fields->{$tag}}; + } + } + + # Then we get all control fields no longer present in incoming fields + # (removed) + my @deleted_field_tags = grep { !(exists $incoming_fields->{$_}) } keys %{$current_fields}; + foreach my $tag (@deleted_field_tags) { + my $rule = $get_matching_field_rule->($tag) // { + on_deleted => {'action' => 'skip', 'rule' => 0} + }; + if ($rule->{on_deleted}->{action} eq 'skip') { + push @merged_record_fields, @{$current_fields->{$tag}}; + } + } + + # Then we get the intersection of control fields, present both in + # current and incoming record (possibly to be overwritten) + my @common_field_tags = grep { exists $incoming_fields->{$_} } keys %{$current_fields}; + foreach my $tag (@common_field_tags) { + # Is control field + my $rule = $get_matching_field_rule->($tag) // { + on_removed => {'action' => 'skip', 'rule' => 0}, + on_appended => {'action' => 'skip', 'rule' => 0} + }; + if ($tag < 10) { + # on_existing = on_match + if ($rule->{on_appended}->{action} eq 'skip') { # TODO: replace with "protect", "keep" + push @merged_record_fields, @{$current_fields->{$tag}}; + } + elsif ($rule->{on_appended}->{action} eq 'append') { + push @merged_record_fields, @{$incoming_fields->{$tag}}; + } + if ( + $rule->{on_appended}->{action} eq 'append' && + $rule->{on_removed}->{action} eq 'skip' + ) { + #TODO: This is an invalid combination for control fields, warn!! + # Or should perform client/server side validation to prevent this choice + } + } + else { + # Compute intersection and diff using field data + my %current_fields_by_data = map { $hash_field_data->($_) => $_ } @{$current_fields->{$tag}}; + my %incoming_fields_by_data = map { $hash_field_data->($_) => $_ } @{$incoming_fields->{$tag}}; + my ($current_fields_only, $common_fields, $incoming_fields_only) = $diff_by_key->(\%current_fields_by_data, \%incoming_fields_by_data); + + # First add common fields (intersection) + # Unchanged + if (@{$common_fields}) { + push @merged_record_fields, @{$common_fields}; + } + # Removed + if (@{$current_fields_only}) { + if ($rule->{on_removed}->{action} eq 'skip') { + push @merged_record_fields, @{$current_fields_only}; + } + } + # Appended + if (@{$incoming_fields_only}) { + if ($rule->{on_appended}->{action} eq 'append') { + push @merged_record_fields, @{$incoming_fields_only}; + } + } + } + } + if ($#merged_record_fields != 0) { + $merged_record->insert_fields_ordered(@merged_record_fields); + } + return $merged_record; +} + +=head2 GetMarcPermissions + + my $marc_permissions = GetMarcPermissions() + +Loads MARC field permissions from the marc_permissions table. + +Returns: + +=over 4 + +=item C<$marc_permissions> + +hashref with permissions structure for use with GetMarcPermissionsAction. + +=back + +=cut + +sub GetMarcPermissions { + my ($filter) = @_; + my $dbh = C4::Context->dbh; + my $rule_count = 0; + my $modules = GetMarcPermissionsModules(); + # We only care about modules included in the context/filter + # TODO: Perhaps make sure source => '*' is default? + my @filter_modules = grep { exists $filter->{$_->{name}} } @{$modules}; + + my $cache = Koha::Caches->get_instance(); + my $permissions = $cache->get_from_cache('marc_permissions', { unsafe => 1 }); + + if (!$permissions) { + my $query = ' + SELECT `marc_permissions`.*, + `marc_permissions_modules`.`name`, + `marc_permissions_modules`.`description`, + `marc_permissions_modules`.`specificity` + FROM `marc_permissions` + LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id` + ORDER BY `marc_permissions_modules`.`specificity`, `id` + '; + my $sth = $dbh->prepare($query); + $sth->execute(); + while (my $perm = $sth->fetchrow_hashref) { + my $target = ($permissions->{$perm->{name}}->{$perm->{filter}}->{$perm->{tagfield}} //= {}); + foreach my $event (GetMarcPermissionEvents()) { + $target->{$event} = { action => $perm->{$event}, rule => $perm->{'id'} }; + } + } + $cache->set_in_cache('marc_permissions', $permissions); + } + + my $filtered_permissions = undef; + foreach my $module (@filter_modules) { + if ( + exists $permissions->{$module->{name}} && + exists $permissions->{$module->{name}}->{$filter->{$module->{name}}} + ) { + # TODO: Support multiple overlapping filters/context?? + $filtered_permissions = $permissions->{$module->{name}}->{$filter->{$module->{name}}}; + last; + } + } + if (!$filtered_permissions) { + # No perms matching specific context conditions found, try wildcard value for each active context + foreach my $module (@filter_modules) { + if (exists $permissions->{$module->{name}}->{'*'}) { + $filtered_permissions = $permissions->{$module->{name}}->{'*'}; + last; + } + } + } + return $filtered_permissions; +} + +# TODO: Use this in GetMarcPermissions +=head2 GetMarcPermissionsRules + + my $rules = GetMarcPermissionsRules() + +Returns: + +=over 4 + +=item C<$rules> + +array (in list context, arrayref otherwise) of hashrefs from marc_permissions +table in order of module specificity and rule id. + +=back + +=cut + +sub GetMarcPermissionsRules { + my $dbh = C4::Context->dbh; + my @rules = (); + + my $query = ' + SELECT `marc_permissions`.`id`, + `marc_permissions`.`tagfield`, + `marc_permissions`.`filter`, + `marc_permissions`.`on_new`, + `marc_permissions`.`on_appended`, + `marc_permissions`.`on_removed`, + `marc_permissions`.`on_deleted`, + `marc_permissions_modules`.`name` as `module`, + `marc_permissions_modules`.`description`, + `marc_permissions_modules`.`specificity` + FROM `marc_permissions` + LEFT JOIN `marc_permissions_modules` ON `module` = `marc_permissions_modules`.`id` + ORDER BY `marc_permissions_modules`.`specificity`, `id` + '; + my $sth = $dbh->prepare($query); + $sth->execute(); + while ( my $row = $sth->fetchrow_hashref ) { + push(@rules, $row); + } + + return wantarray ? @rules : \@rules; +} + +=head2 GetMarcPermissionsModules + + my $modules = GetMarcPermissionsModules() + +Returns: + +=over 4 + +=item C<$modules> + +array (in list context, arrayref otherwise) of hashrefs from +marc_permissions_modules table in order of specificity. + +=back + +=cut +sub GetMarcPermissionsModules { + my $dbh = C4::Context->dbh; + my @modules = (); + + my $query = ' + SELECT * + FROM `marc_permissions_modules` + ORDER BY `specificity` DESC + '; + my $sth = $dbh->prepare($query); + $sth->execute(); + while ( my $row = $sth->fetchrow_hashref ) { + push(@modules, $row); + } + + return wantarray ? @modules : \@modules; +} + +sub GetMarcPermissionEvents { + return ('on_new', 'on_appended', 'on_removed', 'on_deleted'); +} + +=head2 ModMarcPermissionsRule + + my $success = ModMarcPermissionsRule($id, $fields) + +Modifies rule in the marc_permissions table. + +=over 4 + +=item C<$id> + +rule id to modify + +=item C<$fields> + +hashref defining the table fields + + * tagfield - required + * module - required + * filter - required + * on_new - required + * on_appended - required + * on_removed - required + * on_deleted - required + +=back + +Returns: + +=over 4 + +=item C<$success> + +undef if an error occurs, otherwise true. + +=back + +=cut + +sub ModMarcPermissionsRule { + my ($id, $f) = @_; + my $dbh = C4::Context->dbh; + + my $query = ' + UPDATE `marc_permissions` + SET + tagfield = ?, + module = ?, + filter = ?, + on_new = ?, + on_appended = ?, + on_removed = ?, + on_deleted = ? + WHERE + id = ? + '; + my $sth = $dbh->prepare($query); + my $result = $sth->execute ( + $f->{tagfield}, + $f->{module}, + $f->{filter}, + $f->{on_new}, + $f->{on_appended}, + $f->{on_removed}, + $f->{on_deleted}, + $id + ); + ClearMarcPermissionsRulesCache(); + return $result; +} + +sub ClearMarcPermissionsRulesCache { + my $cache = Koha::Caches->get_instance(); + $cache->clear_from_cache('marc_permissions'); +} + +=head2 AddMarcPermissionsRule + + my $success = AddMarcPermissionsRule($fields) + +Add rule to the marc_permissions table. + +=over 4 + +=item C<$fields> + +hashref defining the table fields + + tagfield - required + module - required + filter - required + on_new - required + on_appended - required + on_removed - required + on_deleted - required + +=back + +Returns: + +=over 4 + +=item C<$success> + +undef if an error occurs, otherwise true. + +=back + +=cut + +sub AddMarcPermissionsRule { + my $f = shift; + my $dbh = C4::Context->dbh; + my $query = ' + INSERT INTO `marc_permissions` + ( + tagfield, + module, + filter, + on_new, + on_appended, + on_removed, + on_deleted + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + '; + my $sth = $dbh->prepare($query); + my $result = $sth->execute ( + $f->{tagfield}, + $f->{module}, + $f->{filter}, + $f->{on_new}, + $f->{on_appended}, + $f->{on_removed}, + $f->{on_deleted} + ); + ClearMarcPermissionsRulesCache(); + return $result; +} + +=head2 DelMarcPermissionsRule + + my $success = DelMarcPermissionsRule($id) + +Deletes rule from the marc_permissions table. + +=over 4 + +=item C<$id> + +rule id to delete + +=back + +Returns: + +=over 4 + +=item C<$success> + +undef if an error occurs, otherwise true. + +=back + +=cut + +sub DelMarcPermissionsRule { + my $id = shift; + my $dbh = C4::Context->dbh; + my $query = ' + DELETE FROM `marc_permissions` + WHERE + id = ? + '; + my $sth = $dbh->prepare($query); + my $result = $sth->execute($id); + ClearMarcPermissionsRulesCache(); + return $result; +} +1; __END__ --- a/C4/ImportBatch.pm +++ a/C4/ImportBatch.pm @@ -666,7 +666,7 @@ sub BatchCommitRecords { } $oldxml = $old_marc->as_xml($marc_type); - ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode); + ModBiblio($marc_record, $recordid, $oldbiblio->frameworkcode, {context => {source => 'batchimport'}}); $query = "UPDATE import_biblios SET matched_biblionumber = ? WHERE import_record_id = ?"; if ($item_result eq 'create_new' || $item_result eq 'replace') { --- a/admin/marc-permissions.pl +++ a/admin/marc-permissions.pl @@ -0,0 +1,115 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use strict; +use warnings; + +# standard or CPAN modules used +use CGI qw ( -utf8 ); +use CGI::Cookie; +use MARC::File::USMARC; + +# Koha modules used +use C4::Context; +use C4::Koha; +use C4::Auth; +use C4::AuthoritiesMarc; +use C4::Output; +use C4::Biblio; +use C4::ImportBatch; +use C4::Matcher; +use C4::BackgroundJob; +use C4::Labels::Batch; + +my $script_name = "/cgi-bin/koha/admin/marc-permissions.pl"; + +my $input = new CGI; +my $op = $input->param('op') || ''; + +my $rule_from_cgi = sub { + my ($cgi) = @_; + my %rule = map { $_ => scalar $cgi->param($_) } ( + 'tagfield', + 'module', + 'filter', + 'on_new', + 'on_appended', + 'on_removed', + 'on_deleted' + ); + return \%rule; +}; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "admin/marc-permissions.tt", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { parameters => 'parameters_remaining_permissions' }, + debug => 1, + } +); + +my %cookies = parse CGI::Cookie($cookie); +our $sessionID = $cookies{'CGISESSID'}->value; + +my $rules; +if ( $op eq "remove" ) { + $template->{VARS}->{removeConfirm} = 1; + my @removeIDs = $input->multi_param('batchremove'); + push( @removeIDs, scalar $input->param('id') ) if $input->param('id'); + + $rules = GetMarcPermissionsRules(); + for my $removeID (@removeIDs) { + map { $_->{'remove'} = 1 if $_->{'id'} == $removeID } @{$rules}; + } +} +elsif ( $op eq "doremove" ) { + my @removeIDs = $input->multi_param('batchremove'); + push( @removeIDs, scalar $input->param('id') ) if $input->param('id'); + for my $removeID (@removeIDs) { + DelMarcPermissionsRule($removeID); + } + $rules = GetMarcPermissionsRules(); +} +elsif ( $op eq "edit" ) { + $template->{VARS}->{edit} = 1; + my $id = $input->param('id'); + $rules = GetMarcPermissionsRules(); + map { $_->{'edit'} = 1 if $_->{'id'} == $id } @{$rules}; + +} +elsif ( $op eq "doedit" ) { + my $id = $input->param('id'); + my $rule = $rule_from_cgi->($input); + ModMarcPermissionsRule($id, $rule); + $rules = GetMarcPermissionsRules(); +} +elsif ( $op eq "add" ) { + my $rule = $rule_from_cgi->($input); + AddMarcPermissionsRule($rule); + $rules = GetMarcPermissionsRules(); + +} +else { + $rules = GetMarcPermissionsRules(); +} +my $modules = GetMarcPermissionsModules(); +$template->param( rules => $rules, modules => $modules ); + +output_html_with_http_headers $input, $cookie, $template->output; --- a/cataloguing/addbiblio.pl +++ a/cataloguing/addbiblio.pl @@ -41,6 +41,7 @@ use Koha::ItemTypes; use Koha::Libraries; use Koha::BiblioFrameworks; +use Koha::Patrons; use Date::Calc qw(Today); use MARC::File::USMARC; @@ -840,7 +841,15 @@ if ( $op eq "addbiblio" ) { BiblioAutoLink( $record, $frameworkcode ); } if ( $is_a_modif ) { - ModBiblio( $record, $biblionumber, $frameworkcode ); + my $member = Koha::Patrons->find($loggedinuser); + ModBiblio( $record, $biblionumber, $frameworkcode, { + context => { + source => $z3950 ? 'z39.50' : 'intranet', + category => $member->{'category_type'}, + borrower => $loggedinuser + } + } + ); } else { ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode ); @@ -933,6 +942,7 @@ elsif ( $op eq "delete" ) { $template->param( biblionumberdata => $biblionumber, op => $op, + z3950 => $z3950 ); if ( $op eq "duplicate" ) { $biblionumber = ""; --- a/help.pl +++ a/help.pl @@ -23,6 +23,7 @@ use C4::Output; # use C4::Auth; use C4::Context; use CGI qw ( -utf8 ); +use C4::Biblio; sub _help_template_file_of_url { my $url = shift; @@ -74,4 +75,8 @@ if ( $help_version =~ m|^(\d+)\.(\d{2}).*$| ) { } $template->param( helpVersion => $help_version ); +my $rules = GetMarcPermissionsRules(); +my $modules = GetMarcPermissionsModules(); +$template->param( rules => $rules, modules => $modules ); + output_html_with_http_headers $query, "", $template->output; --- a/installer/data/mysql/atomicupdate/bug_14957-marc-permissions-syspref.sql +++ a/installer/data/mysql/atomicupdate/bug_14957-marc-permissions-syspref.sql @@ -0,0 +1,2 @@ +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'); +INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('MARCPermissionsLog','0','','Write MARC permissions rule evaluations to the system log when applying rules for MARC field modifications.','YesNo'); --- a/installer/data/mysql/atomicupdate/bug_14957-marc-permissions.sql +++ a/installer/data/mysql/atomicupdate/bug_14957-marc-permissions.sql @@ -0,0 +1,31 @@ +-- DROP TABLE IF EXISTS `marc_permissions`; +DROP TABLE IF EXISTS `marc_permissions_modules`; + +CREATE TABLE `marc_permissions_modules` ( + `id` int(11) NOT NULL auto_increment, + `name` varchar(24) NOT NULL, + `description` varchar(255), + `specificity` int(11) NOT NULL DEFAULT 0, -- higher specificity will override rules with lower specificity + PRIMARY KEY(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +-- a couple of useful default filter modules +-- these are used in various scripts, so don't remove them if you don't know +-- what you're doing. +-- New filter modules can be added here when needed +INSERT INTO `marc_permissions_modules` VALUES(NULL, 'source', 'source from where modification request was sent', 0); +INSERT INTO `marc_permissions_modules` VALUES(NULL, 'category', 'categorycode of user who requested modification', 1); +INSERT INTO `marc_permissions_modules` VALUES(NULL, 'borrower', 'borrowernumber of user who requested modification', 2); + +CREATE TABLE IF NOT EXISTS `marc_permissions` ( + `id` int(11) NOT NULL auto_increment, + `tagfield` varchar(255) NOT NULL, -- can be regexe, so need > 3 chars + `module` int(11) NOT NULL, + `filter` varchar(255) NOT NULL, + `on_new` ENUM('skip', 'add') DEFAULT NULL, + `on_appended` ENUM('skip', 'append') DEFAULT NULL, + `on_removed` ENUM('skip', 'remove') DEFAULT NULL, + `on_deleted` ENUM('skip', 'delete') DEFAULT NULL, + PRIMARY KEY(`id`), + CONSTRAINT `marc_permissions_ibfk1` FOREIGN KEY (`module`) REFERENCES `marc_permissions_modules` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; --- a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc @@ -40,6 +40,7 @@ [% IF Koha.Preference('SearchEngine') == 'Elasticsearch' %]
  • Search engine configuration
  • [% END %] +
  • MARC field permissions
  • Acquisition parameters
    --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt @@ -93,6 +93,8 @@
    Search engine configuration
    Manage indexes, facets, and their mappings to MARC fields and subfields.
    [% END %] +
    MARC field permissions
    +
    Managed MARC field permissions

    Acquisition parameters

    --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc-permissions.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc-permissions.tt @@ -0,0 +1,320 @@ +[% USE Koha %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Administration › MARC field permissions +[% INCLUDE 'doc-head-close.inc' %] + +[% INCLUDE 'datatables.inc' %] + + + + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'cat-search.inc' %] + + + +
    +
    +
    +
    + +

    Manage MARC field permissions

    + +[% UNLESS Koha.Preference( 'MARCPermissions' ) %] +
    + The MARCPermissions preference is not set, don't forget to enable it for rules to take effect. +
    +[% END %] +[% IF removeConfirm %] +
    +

    Remove rule?

    +

    Are you sure you want to remove the selected rule(s)?

    + +
    + +
    + +
    +[% END %] + +
    + + + + + + + + + + + + + + [% UNLESS edit %] + + + + + + + + + + + + + + + [% END %] + + [% FOREACH rule IN rules %] + + [% IF rule.edit %] + + + + + + + + + + + [% ELSE %] + + + + + + + + + + + [% END %] + + [% END %] + +
    RuleTagModuleFilterOn NewOn AppendedOn RemovedOn DeletedActions 
      + + + + + + + + + +
    [% rule.id %] + + + + + + + + + + + + + [% rule.id %][% rule.tagfield %][% rule.module %][% rule.filter %][% rule.on_new %][% rule.on_appended %][% rule.on_removed %][% rule.on_deleted %] + Delete + Edit + + [% IF rule.remove %] + + [% ELSE %] + + [% END %] +
    +
    + +
    + +
    + +
    +
    +
    +[% INCLUDE 'admin-menu.inc' %] +
    +
    +[% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref @@ -276,3 +276,10 @@ Cataloging: - "All values of repeating tags and subfields will be printed with the given RIS tag." - "
    " - "Use of TY ( record type ) as a key will replace the default TY with the field value of your choosing." + - + - When importing records + - pref: MARCPermissions + choices: + yes: "use" + no: "don't use" + - MARC permissions rules to decide which action to take for each field. --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/logs.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/logs.pref @@ -72,6 +72,12 @@ Logging: on: Log off: "Don't log" - when reports are added, deleted or changed. + - + - pref: MARCPermissionsLog + choices: + on: Log + off: "Don't log" + - MARC permissions rule evaluations when applying rules for MARC field modifications. Debugging: - - pref: DumpTemplateVarsIntranet --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt @@ -562,6 +562,7 @@ function Changefwk() { [% END %] + --- a/koha-tmpl/intranet-tmpl/prog/en/modules/help/marc-permissions.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/help/marc-permissions.tt @@ -0,0 +1,148 @@ +[% INCLUDE 'help-top.inc' %] + +

    Manage MARC field permissions

    + +

    Rule evaluation

    +

    Rules are evaluated from least specific to most specific. This means that a more specific rule will override a less specific rule. * is less specific than a regular expression. A regular expression is less specific than a normal field name (e.g. 245). A Subfield tag is more specific than a Tag.

    +

    To add a field-specific rule (i.e. not a subfield), leave the Subfield tag field blank.

    + +

    Defaults

    +

    Default action when no rules exist is to replace the old record with the new record. Same as if MARCPermissions is disabled.

    +

    Default action when no matching rule is found is to leave a field unchanged (skip). If you wish to changed the default actions for fields and subfields, please add wildcard rules.

    + +

    Wildcards

    +

    * can be used as a wildcard for Tag, Subfield tag and Filter. * is considered less specific than a non wildcard value, and thus will be overridden by a non wildcard value (e.g. "100", "a", etc).

    + +

    Regular expressions

    +

    Regular expressions can be used in both the Tag and Subfield tag fields. Beware though, using regular expressions may create overlapping matches, in which case they will be applied in a sorted order.

    + +

    Example rules

    +

    Following is an example rule set in order of specificity.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TagSubfield tagModuleFilterDescription
    *[% modules.0.name %]*Match any field regardless of [% modules.0.name %]
    **[% modules.0.name %]*Match any subfield regardless of [% modules.0.name %]
    245*[% modules.0.name %]z39.50Match any subfield under 245 when [% modules.0.name %] is z39.50
    *b[% modules.0.name %]z39.50Match subfield b regardless of field when [% modules.0.name %] is z39.50
    500[a-d][% modules.0.name %]z39.50Match subfields a, b, c, d when field is 500 and [% modules.0.name %] is z39.50
    5..a[% modules.0.name %]z39.50Match subfield a when field matches the regular expression 5.. (500-599) and [% modules.0.name %] is z39.50
    245a[% modules.0.name %]z39.50Match subfield a when field is 500 and [% modules.0.name %] is z39.50
    + +
    + +

    Available filter modules

    +

    Filters cannot be regular expressions. Please use a single * as a wildcard if need.

    + + + + + + + + + [% FOREACH module IN modules %] + + + + + + [% END %] + +
    SpecificityModuleDescription
    [% module.specificity %][% module.name %][% module.description %]
    + +
    + +

    Available sources

    +

    The following sources currently implement MARCPermissions.

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescription
    bulkmarcimportbin/migration_tools/bulkmarcimport.pl
    import_lexilebin/migration_tools/import_lexile.pl
    z39.50Import from Z39.50 search in browser
    intranetModifications from intranet in browser
    batchmodBatch record modification in browser
    + +
    + +

    Indicators

    +

    Indicators can be addressed as i1 and i2 in the Subfield tag field. Some actions might not be available for indicators, in which case they will be disabled.

    + +
    + +

    Logging

    +

    If MARCPermissionsLog is enabled, log entries for each record modification will be available in the Modification log in the Catalog module under the Modify action. This can be very helpful when debugging rule sets.

    +

    The administration area is where you set all of your preferences for the system. Preference are broken down into several categories, detailed below.

    + +

    See the full documentation for Koha in the manual (online).

    + +[% INCLUDE 'help-bottom.inc' %] --- a/misc/migration_tools/bulkmarcimport.pl +++ a/misc/migration_tools/bulkmarcimport.pl @@ -451,7 +451,7 @@ RECORD: while ( ) { $biblioitemnumber = Koha::Biblios->find( $biblionumber )->biblioitem->biblioitemnumber; }; if ($update) { - eval { ( $biblionumber, $biblioitemnumber ) = ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber) ) }; + eval { ( $biblionumber, $biblioitemnumber ) = ModBiblio( $record, $biblionumber, GetFrameworkCode($biblionumber), {context => {source => 'bulkmarcimport'}}) }; if ($@) { warn "ERROR: Edit biblio $biblionumber failed: $@\n"; printlog( { id => $id || $originalid || $biblionumber, op => "update", status => "ERROR" } ) if ($logfile); --- a/misc/migration_tools/import_lexile.pl +++ a/misc/migration_tools/import_lexile.pl @@ -153,6 +153,7 @@ while ( my $row = $csv->getline_hr($fh) ) { foreach my $biblionumber (@biblionumbers) { $counter++; my $record = GetMarcBiblio({ biblionumber => $biblionumber }); + my $frameworkcode = GetFrameworkCode($biblionumber); if ($verbose) { say "Found matching record! Biblionumber: $biblionumber"; @@ -202,7 +203,7 @@ while ( my $row = $csv->getline_hr($fh) ) { $record->append_fields($field); } - ModBiblio( $record, $biblionumber ) unless ( $test ); + ModBiblio( $record, $biblionumber, $frameworkcode, {context => {source => 'import_lexile'}} ) unless ( $test ); } } --- a/t/db_dependent/Biblio/MARCPermissions.t +++ a/t/db_dependent/Biblio/MARCPermissions.t @@ -0,0 +1,279 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 16; +use Test::MockModule; + +use MARC::Record; +use Data::Dumper; + +BEGIN { + use_ok('C4::Biblio'); +} + +# Start transaction +my $dbh = C4::Context->dbh; +$dbh->{AutoCommit} = 0; +$dbh->{RaiseError} = 1; + +C4::Context->set_preference( 'MARCPermissions', '1'); +C4::Context->set_preference( 'MARCPermissionsLog', '1'); + +# Create a record +my $record = MARC::Record->new(); +$record->append_fields ( + MARC::Field->new('008', '12345'), + MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'), + MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'), + MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'), + MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'), + MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'), +); + +# Add record to DB +my ($biblionumber, $biblioitemnumber) = AddBiblio($record, ''); + +my $modules = GetMarcPermissionsModules(); + +############################################################################## +# Test overwrite rule +my $mod_record = MARC::Record->new(); +$mod_record->append_fields ( + MARC::Field->new('008', '12345'), + MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'), + MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'), + MARC::Field->new('500', '1','1', 'a' => 'this field has now been changed'), + MARC::Field->new('500', '2','2', 'a' => 'and so have this field'), +); + +# Clear MARC permission rules from DB +DelMarcPermissionsRule($_->{id}) for GetMarcPermissionsRules(); + +# Add MARC permission rules to DB +AddMarcPermissionsRule({ + module => $modules->[0]->{'id'}, + tagfield => '*', + tagsubfield => '', + filter => '*', + on_existing => 'overwrite', + on_new => 'add', + on_removed => 'remove' +}); + +my @log = (); +my $new_record = ApplyMarcPermissions({ + biblionumber => $biblionumber, + record => $mod_record, + frameworkcode => '', + filter => {$modules->[0]->{'name'} => 'foo'}, + log => \@log + }); + +my @a500 = $new_record->field('500'); +is ($a500[0]->subfield('a'), 'this field has now been changed', 'old field is replaced when overwrite'); +is ($a500[1]->subfield('a'), 'and so have this field', 'old field is replaced when overwrite'); + +############################################################################## +# Test remove rule +is ($new_record->field('250'), undef, 'removed field is removed'); + +############################################################################## +# Test skip rule +$mod_record = MARC::Record->new(); +$mod_record->append_fields ( + MARC::Field->new('008', '12345'), + MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'), + MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'), + MARC::Field->new('500', '1','1', 'a' => 'this should not show'), + MARC::Field->new('500', '2','2', 'a' => 'and neither should this'), +); + +AddMarcPermissionsRule({ + module => $modules->[0]->{'id'}, + tagfield => '500', + tagsubfield => '*', + filter => '*', + on_existing => 'skip', + on_new => 'skip', + on_removed => 'skip' +}); + +@log = (); +$new_record = ApplyMarcPermissions({ + biblionumber => $biblionumber, + record => $mod_record, + frameworkcode => '', + filter => {$modules->[0]->{'name'} => 'foo'}, + log => \@log + }); + +@a500 = $new_record->field('500'); +is ($a500[0]->subfield('a'), 'the lazy programmer jumps over the quick brown tests', 'old field is kept when skip'); +is ($a500[1]->subfield('a'), 'the quick brown test jumps over the lazy programmers', 'old field is kept when skip'); + +############################################################################## +# Test add rule +$mod_record = MARC::Record->new(); +$mod_record->append_fields ( + MARC::Field->new('008', '12345'), + MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'), + MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'), + #MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'), + MARC::Field->new('245', '1','2', 'a' => 'some new fun value'), + MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'), + MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'), +); + +AddMarcPermissionsRule({ + module => $modules->[0]->{'id'}, + tagfield => '245', + tagsubfield => '*', + filter => '*', + on_existing => 'add', + on_new => 'add', + on_removed => 'skip' +}); + + +@log = (); +$new_record = ApplyMarcPermissions({ + biblionumber => $biblionumber, + record => $mod_record, + frameworkcode => '', + filter => {$modules->[0]->{'name'} => 'foo'}, + log => \@log + }); + +my @a245 = $new_record->field('245')->subfield('a'); +is ($a245[0], 'field data for 245 a with indicators 12', 'old field is kept when adding new'); +is ($a245[1], 'some new fun value', 'new field is added'); + +############################################################################## +# Test add_or_correct rule +$mod_record = MARC::Record->new(); +$mod_record->append_fields ( + MARC::Field->new('008', '12345'), + #MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'), + MARC::Field->new('100', '','', 'a' => 'a very different value'), + MARC::Field->new('250', '','', 'a' => '250 bottles of beer on the wall'), + #MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'), + MARC::Field->new('245', '1','2', 'a' => 'Field data for 245 a with indicators 12', 'a' => 'some very different value'), + MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'), + MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'), +); + +AddMarcPermissionsRule({ + module => $modules->[0]->{'id'}, + tagfield => '(100|245)', + tagsubfield => '*', + filter => '*', + on_existing => 'add_or_correct', + on_new => 'add', + on_removed => 'skip' +}); + +@log = (); +$new_record = ApplyMarcPermissions({ + biblionumber => $biblionumber, + record => $mod_record, + frameworkcode => '', + filter => {$modules->[0]->{'name'} => 'foo'}, + log => \@log + }); + +@a245 = $new_record->field('245')->subfield('a'); +is ($a245[0], 'Field data for 245 a with indicators 12', 'add_or_correct modifies field when a correction'); +is ($a245[1], 'some very different value', 'add_or_correct adds field when not a correction'); + +my @a100 = $new_record->field('100')->subfield('a'); +is ($a100[0], 'field data for 100 a without indicators', 'add_or_correct keeps old field when not a correction'); +is ($a100[1], 'a very different value', 'add_or_correct adds field when not a correction'); + +############################################################################## +# Test rule evaluation order +$mod_record = MARC::Record->new(); +$mod_record->append_fields ( + MARC::Field->new('008', '12345'), + MARC::Field->new('100', '','', 'a' => 'field data for 100 a without indicators'), + MARC::Field->new('250', '','', 'a' => 'take one down, pass it around'), + MARC::Field->new('245', '1','2', 'a' => 'field data for 245 a with indicators 12'), + MARC::Field->new('500', '1','1', 'a' => 'the lazy programmer jumps over the quick brown tests'), + MARC::Field->new('500', '2','2', 'a' => 'the quick brown test jumps over the lazy programmers'), +); + + +DelMarcPermissionsRule($_->{id}) for GetMarcPermissionsRules(); + +AddMarcPermissionsRule({ + module => $modules->[0]->{'id'}, + tagfield => '*', + tagsubfield => '*', + filter => '*', + on_existing => 'skip', + on_new => 'skip', + on_removed => 'skip' +}); +AddMarcPermissionsRule({ + module => $modules->[0]->{'id'}, + tagfield => '250', + tagsubfield => '*', + filter => '*', + on_existing => 'overwrite', + on_new => 'skip', + on_removed => 'skip' +}); +AddMarcPermissionsRule({ + module => $modules->[0]->{'id'}, + tagfield => '*', + tagsubfield => 'a', + filter => '*', + on_existing => 'add_or_correct', + on_new => 'skip', + on_removed => 'skip' +}); +AddMarcPermissionsRule({ + module => $modules->[0]->{'id'}, + tagfield => '250', + tagsubfield => 'a', + filter => '*', + on_existing => 'add', + on_new => 'skip', + on_removed => 'skip' +}); + +@log = (); +$new_record = ApplyMarcPermissions({ + biblionumber => $biblionumber, + record => $mod_record, + frameworkcode => '', + filter => {$modules->[0]->{'name'} => 'foo'}, + log => \@log + }); + +my @rule = grep { $_->{tag} eq '250' and $_->{subfieldcode} eq 'a' } @log; +is(scalar @rule, 1, 'only one rule applied'); +is($rule[0]->{event}.':'.$rule[0]->{action}, 'existing:add', 'most specific rule used'); + +my @a250 = $new_record->field('250')->subfield('a'); +is ($a250[0], '250 bottles of beer on the wall', 'most specific rule is applied, original field kept'); +is ($a250[1], 'take one down, pass it around', 'most specific rule is applied, new field added'); + +$dbh->rollback; + +1; --- a/tools/batch_record_modification.pl +++ a/tools/batch_record_modification.pl @@ -158,7 +158,7 @@ if ( $op eq 'form' ) { my ( $job ); if ( $runinbackground ) { my $job_size = scalar( @record_ids ); - $job = C4::BackgroundJob->new( $sessionID, "FIXME", '/cgi-bin/koha/tools/batch_record_modification.pl', $job_size ); + $job = C4::BackgroundJob->new( $sessionID, "FIXME", $ENV{SCRIPT_NAME}, $job_size ); my $job_id = $job->id; if (my $pid = fork) { $dbh->{InactiveDestroy} = 1; @@ -170,7 +170,7 @@ if ( $op eq 'form' ) { } elsif (defined $pid) { close STDOUT; } else { - warn "fork failed while attempting to run tools/batch_record_modification.pl as a background job"; + warn "fork failed while attempting to run $ENV{'SCRIPT_NAME'} as a background job"; exit 0; } } @@ -194,7 +194,14 @@ if ( $op eq 'form' ) { my $record = GetMarcBiblio({ biblionumber => $biblionumber }); ModifyRecordWithTemplate( $mmtid, $record ); my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber ); - ModBiblio( $record, $biblionumber, $frameworkcode ); + my ($member) = Koha::Patrons->find($loggedinuser); + ModBiblio( $record, $biblionumber, $frameworkcode, + { + source => 'batchmod', + category => $member->{'category_type'}, + borrower => $loggedinuser + } + ); }; if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well push @messages, { --