@@ -, +, @@ --- C4/ImportBatch.pm | 6 +- C4/Koha.pm | 17 + Koha/MarcModificationTemplates.pm | 579 ++++++++++++++++++++ Koha/SimpleMARC.pm | 331 +++++++++++ .../data/mysql/en/mandatory/userpermissions.sql | 1 + installer/data/mysql/kohastructure.sql | 36 ++ installer/data/mysql/updatedatabase.pl | 39 ++ .../intranet-tmpl/prog/en/includes/tools-menu.inc | 3 + .../prog/en/modules/admin/admin-home.tt | 4 +- .../modules/tools/marc_modification_templates.tt | 492 +++++++++++++++++ .../prog/en/modules/tools/stage-marc-import.tt | 17 + .../prog/en/modules/tools/tools-home.tt | 7 +- tools/marc_modification_templates.pl | 151 +++++ tools/stage-marc-import.pl | 11 +- 14 files changed, 1689 insertions(+), 5 deletions(-) create mode 100644 Koha/MarcModificationTemplates.pm create mode 100644 Koha/SimpleMARC.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/marc_modification_templates.tt create mode 100755 tools/marc_modification_templates.pl --- a/C4/ImportBatch.pm +++ a/C4/ImportBatch.pm @@ -26,6 +26,7 @@ use C4::Biblio; use C4::Items; use C4::Charset; use C4::AuthoritiesMarc; +use Koha::MarcModificationTemplates; use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); @@ -316,7 +317,7 @@ sub ModAuthInBatch { =head2 BatchStageMarcRecords ($batch_id, $num_records, $num_items, @invalid_records) = - BatchStageMarcRecords($record_type, $encoding, $marc_records, $file_name, + BatchStageMarcRecords($encoding, $marc_records, $file_name, $marc_modification_template, $comments, $branch_code, $parse_items, $leave_as_staging, $progress_interval, $progress_callback); @@ -328,6 +329,7 @@ sub BatchStageMarcRecords { my $encoding = shift; my $marc_records = shift; my $file_name = shift; + my $marc_modification_template = shift; my $comments = shift; my $branch_code = shift; my $parse_items = shift; @@ -378,6 +380,8 @@ sub BatchStageMarcRecords { $encoding = $charset_guessed unless $encoding; + ModifyRecordWithTemplate( $marc_modification_template, $marc_record ) if ( $marc_modification_template ); + my $import_record_id; if (scalar($marc_record->fields()) == 0) { push @invalid_records, $marc_blob; --- a/C4/Koha.pm +++ a/C4/Koha.pm @@ -1488,6 +1488,23 @@ sub _isbn_cleanup { return; } +=head2 + + Log( $message ); + + Writes data to /tmp/koha.log. + + This is useful for debugging forked processes + that do not write to the apache error log + +=cut + +sub Log { + my ($data) = @_; + open (MYFILE, '>>/tmp/koha.log'); + print MYFILE "$data\n"; + close (MYFILE); +} 1; __END__ --- a/Koha/MarcModificationTemplates.pm +++ a/Koha/MarcModificationTemplates.pm @@ -0,0 +1,579 @@ +package Koha::MarcModificationTemplates; + +# Copyright 2010 Kyle M Hall +# +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +## NOTE: +## Parts of this module are used from cgi scripts that are detached from apache before +## execution. For this reason, the C4::Koha::Log function has been used to capture +## output for debugging purposes. + +use strict; +use warnings; + +use DateTime; + +use C4::Context; +use Koha::SimpleMARC; + +use vars qw($VERSION @ISA @EXPORT); + +use constant DEBUG => 0; + +BEGIN { + $VERSION = 1.00; # set the version for version checking + @ISA = qw(Exporter); + @EXPORT = qw( + &GetModificationTemplates + &AddModificationTemplate + &DelModificationTemplate + + &GetModificationTemplateAction + &GetModificationTemplateActions + + &AddModificationTemplateAction + &ModModificationTemplateAction + &DelModificationTemplateAction + &MoveModificationTemplateAction + + &ModifyRecordsWithTemplate + &ModifyRecordWithTemplate + ); +} + + +=head1 NAME + +Koha::MarcModificationTemplates - Module to manage MARC Modification Templates + +=head1 DESCRIPTION + +MARC Modification Templates are a tool for marc batch imports, +so that librarians can set up templates for various vendors' +files telling Koha what fields to insert data into. + +=head1 FUNCTIONS + +=cut + +=head2 GetModificationTemplates + + my @templates = GetModificationTemplates( [ $template_id ] ); + + Passing a $template_id will mark the given id as the selected template. +=cut + +sub GetModificationTemplates { + my ( $template_id ) = @_; + C4::Koha::Log("Koha::MarcModificationTemplates::GetModificationTemplates( $template_id )") if DEBUG; + warn("Koha::MarcModificationTemplates::GetModificationTemplates( $template_id )") if DEBUG; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("SELECT * FROM marc_modification_templates"); + $sth->execute(); + + my @templates; + while ( my $template = $sth->fetchrow_hashref() ) { + $template->{'selected'} = 1 if ( $template->{'template_id'} eq $template_id ); + push( @templates, $template ); + } + + return @templates; +} + +=head2 + AddModificationTemplate + + $template_id = AddModificationTemplate( $template_name[, $template_id ] ); + + If $template_id is supplied, the actions from that template will be copied + into the newly created template. +=cut + +sub AddModificationTemplate { + my ( $template_name, $template_id_copy ) = @_; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("INSERT INTO marc_modification_templates ( name ) VALUES ( ? )"); + $sth->execute( $template_name ); + + $sth = $dbh->prepare("SELECT * FROM marc_modification_templates WHERE name = ?"); + $sth->execute( $template_name ); + my $row = $sth->fetchrow_hashref(); + my $template_id = $row->{'template_id'}; + + if ( $template_id_copy ) { + my @actions = GetModificationTemplateActions( $template_id_copy ); + foreach my $action ( @actions ) { + AddModificationTemplateAction( + $template_id, + $action->{'action'}, + $action->{'field_number'}, + $action->{'from_field'}, + $action->{'from_subfield'}, + $action->{'field_value'}, + $action->{'to_field'}, + $action->{'to_subfield'}, + $action->{'to_regex'}, + $action->{'conditional'}, + $action->{'conditional_field'}, + $action->{'conditional_subfield'}, + $action->{'conditional_comparison'}, + $action->{'conditional_value'}, + $action->{'conditional_regex'}, + $action->{'description'}, + ); + + } + } + + return $template_id; +} + +=head2 + DelModificationTemplate + + DelModificationTemplate( $template_id ); +=cut + +sub DelModificationTemplate { + my ( $template_id ) = @_; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("DELETE FROM marc_modification_templates WHERE template_id = ?"); + $sth->execute( $template_id ); + + $sth = $dbh->prepare("DELETE FROM marc_modification_template_actions WHERE template_id = ?"); + $sth->execute( $template_id ); +} + +=head2 + GetModificationTemplateAction + + my $action = GetModificationTemplateAction( $mmta_id ); +=cut + +sub GetModificationTemplateAction { + my ( $mmta_id ) = @_; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("SELECT * FROM marc_modification_template_actions WHERE mmta_id = ?"); + $sth->execute( $mmta_id ); + my $action = $sth->fetchrow_hashref(); + + return $action; +} + +=head2 + GetModificationTemplateActions + + my @actions = GetModificationTemplateActions( $template_id ); +=cut + +sub GetModificationTemplateActions { + my ( $template_id ) = @_; + + C4::Koha::Log( "Koha::MarcModificationTemplates::GetModificationTemplateActions( $template_id )" ) if DEBUG; + warn( "Koha::MarcModificationTemplates::GetModificationTemplateActions( $template_id )" ) if DEBUG; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("SELECT * FROM marc_modification_template_actions WHERE template_id = ? ORDER BY ordering"); + $sth->execute( $template_id ); + + my @actions; + while ( my $action = $sth->fetchrow_hashref() ) { + push( @actions, $action ); + } + + C4::Koha::Log( Data::Dumper::Dumper( @actions ) ) if DEBUG > 4; + warn( Data::Dumper::Dumper( @actions ) ) if DEBUG > 4; + + return @actions; +} + +=head2 + AddModificationTemplateAction + + AddModificationTemplateAction( + $template_id, $action, $field_number, + $from_field, $from_subfield, $field_value, + $to_field, $to_subfield, $to_regex, + $conditional, $conditional_field, $conditional_subfield, + $conditional_comparison, $conditional_value, + $conditional_regex, $description + ); + + Adds a new action to the given modification template. + +=cut + +sub AddModificationTemplateAction { + my ( + $template_id, + $action, + $field_number, + $from_field, + $from_subfield, + $field_value, + $to_field, + $to_subfield, + $to_regex, + $conditional, + $conditional_field, + $conditional_subfield, + $conditional_comparison, + $conditional_value, + $conditional_regex, + $description + ) = @_; + + C4::Koha::Log( "Koha::MarcModificationTemplates::AddModificationTemplateAction( $template_id, $action, + $field_number, $from_field, $from_subfield, $field_value, $to_field, $to_subfield, + $to_regex, $conditional, $conditional_field, $conditional_subfield, $conditional_comparison, + $conditional_value, $conditional_regex, $description )" ) if DEBUG; + warn( "Koha::MarcModificationTemplates::AddModificationTemplateAction( $template_id, $action, + $field_number, $from_field, $from_subfield, $field_value, $to_field, $to_subfield, + $to_regex, $conditional, $conditional_field, $conditional_subfield, $conditional_comparison, + $conditional_value, $conditional_regex, $description )" ) if DEBUG; + + $conditional_regex ||= '0'; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare( 'SELECT MAX(ordering) + 1 AS next_ordering FROM marc_modification_template_actions WHERE template_id = ?' ); + $sth->execute( $template_id ); + my $row = $sth->fetchrow_hashref; + my $ordering = $row->{'next_ordering'} || 1; + + my $query = " + INSERT INTO marc_modification_template_actions ( + mmta_id, + template_id, + ordering, + action, + field_number, + from_field, + from_subfield, + field_value, + to_field, + to_subfield, + to_regex, + conditional, + conditional_field, + conditional_subfield, + conditional_comparison, + conditional_value, + conditional_regex, + description + ) + VALUES ( NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )"; + + $sth = $dbh->prepare( $query ); + + $sth->execute( + $template_id, + $ordering, + $action, + $field_number, + $from_field, + $from_subfield, + $field_value, + $to_field, + $to_subfield, + $to_regex, + $conditional, + $conditional_field, + $conditional_subfield, + $conditional_comparison, + $conditional_value, + $conditional_regex, + $description + ); +} + +=head2 + ModModificationTemplateAction + + ModModificationTemplateAction( + $mmta_id, $action, $field_number, $from_field, + $from_subfield, $field_value, $to_field, + $to_subfield, $to_regex, $conditional, + $conditional_field, $conditional_subfield, + $conditional_comparison, $conditional_value, + $conditional_regex, $description + ); + + Modifies an existing action. + +=cut + +sub ModModificationTemplateAction { + my ( + $mmta_id, + $action, + $field_number, + $from_field, + $from_subfield, + $field_value, + $to_field, + $to_subfield, + $to_regex, + $conditional, + $conditional_field, + $conditional_subfield, + $conditional_comparison, + $conditional_value, + $conditional_regex, + $description + ) = @_; + + my $dbh = C4::Context->dbh; + + my $query = " + UPDATE marc_modification_template_actions SET + action = ?, + field_number = ?, + from_field = ?, + from_subfield = ?, + field_value = ?, + to_field = ?, + to_subfield = ?, + to_regex = ?, + conditional = ?, + conditional_field = ?, + conditional_subfield = ?, + conditional_comparison = ?, + conditional_value = ?, + conditional_regex = ?, + description = ? + WHERE mmta_id = ?"; + + my $sth = $dbh->prepare( $query ); + + $sth->execute( + $action, + $field_number, + $from_field, + $from_subfield, + $field_value, + $to_field, + $to_subfield, + $to_regex, + $conditional, + $conditional_field, + $conditional_subfield, + $conditional_comparison, + $conditional_value, + $conditional_regex, + $description, + $mmta_id + ); +} + + +=head2 + DelModificationTemplateAction + + DelModificationTemplateAction( $mmta_id ); + + Deletes the given template action. +=cut + +sub DelModificationTemplateAction { + my ( $mmta_id ) = @_; + + my $action = GetModificationTemplateAction( $mmta_id ); + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("DELETE FROM marc_modification_template_actions WHERE mmta_id = ?"); + $sth->execute( $mmta_id ); + + $sth = $dbh->prepare("UPDATE marc_modification_template_actions SET ordering = ordering - 1 WHERE template_id = ? AND ordering > ?"); + $sth->execute( $action->{'template_id'}, $action->{'ordering'} ); +} + +=head2 + MoveModificationTemplateAction + + MoveModificationTemplateAction( $mmta_id, $where ); + + Changes the order for the given action. + Options for $where are 'up', 'down', 'top' and 'bottom' +=cut +sub MoveModificationTemplateAction { + my ( $mmta_id, $where ) = @_; + + my $action = GetModificationTemplateAction( $mmta_id ); + + return if ( $action->{'ordering'} eq '1' && ( $where eq 'up' || $where eq 'top' ) ); + return if ( $action->{'ordering'} eq GetModificationTemplateActions( $action->{'template_id'} ) && ( $where eq 'down' || $where eq 'bottom' ) ); + + my $dbh = C4::Context->dbh; + my ( $sth, $query ); + + if ( $where eq 'up' || $where eq 'down' ) { + + ## For up and down, we just swap the ordering number with the one above or below it. + + ## Change the ordering for the other action + $query = "UPDATE marc_modification_template_actions SET ordering = ? WHERE template_id = ? AND ordering = ?"; + + my $ordering = $action->{'ordering'}; + $ordering-- if ( $where eq 'up' ); + $ordering++ if ( $where eq 'down' ); + + $sth = $dbh->prepare( $query ); + $sth->execute( $action->{'ordering'}, $action->{'template_id'}, $ordering ); + + ## Change the ordering for this action + $query = "UPDATE marc_modification_template_actions SET ordering = ? WHERE mmta_id = ?"; + $sth = $dbh->prepare( $query ); + $sth->execute( $ordering, $action->{'mmta_id'} ); + + } elsif ( $where eq 'top' ) { + + $sth = $dbh->prepare('UPDATE marc_modification_template_actions SET ordering = ordering + 1 WHERE template_id = ? AND ordering < ?'); + $sth->execute( $action->{'template_id'}, $action->{'ordering'} ); + + $sth = $dbh->prepare('UPDATE marc_modification_template_actions SET ordering = 1 WHERE mmta_id = ?'); + $sth->execute( $mmta_id ); + + } elsif ( $where eq 'bottom' ) { + + my $ordering = GetModificationTemplateActions( $action->{'template_id'} ); + + $sth = $dbh->prepare('UPDATE marc_modification_template_actions SET ordering = ordering - 1 WHERE template_id = ? AND ordering > ?'); + $sth->execute( $action->{'template_id'}, $action->{'ordering'} ); + + $sth = $dbh->prepare('UPDATE marc_modification_template_actions SET ordering = ? WHERE mmta_id = ?'); + $sth->execute( $ordering, $mmta_id ); + + } + +} + +=head2 + ModifyRecordsWithTemplate + + ModifyRecordsWithTemplate( $template_id, $batch ); + + Accepts a template id and a MARC::Batch object. +=cut + +sub ModifyRecordsWithTemplate { + my ( $template_id, $batch ) = @_; + C4::Koha::Log( "Koha::MarcModificationTemplates::ModifyRecordsWithTemplate( $template_id, $batch )" ) if DEBUG; + warn( "Koha::MarcModificationTemplates::ModifyRecordsWithTemplate( $template_id, $batch )" ) if DEBUG; + + while ( my $record = $batch->next() ) { + ModifyRecordWithTemplate( $template_id, $record ); + } +} + +=head2 + ModifyRecordWithTemplate + + ModifyRecordWithTemplate( $template_id, $record ) + + Accepts a MARC::Record object ( $record ) and modifies + it based on the actions for the given $template_id +=cut + +sub ModifyRecordWithTemplate { + my ( $template_id, $record ) = @_; + C4::Koha::Log( "Koha::MarcModificationTemplates::ModifyRecordWithTemplate( $template_id, $record )" ) if DEBUG; + warn( "Koha::MarcModificationTemplates::ModifyRecordWithTemplate( $template_id, $record )" ) if DEBUG; + C4::Koha::Log( "Unmodified Record:\n" . $record->as_formatted() ) if DEBUG >= 10; + warn( "Unmodified Record:\n" . $record->as_formatted() ) if DEBUG >= 10; + + my $current_date = DateTime->now()->ymd(); + my $branchcode = C4::Context->userenv->{branch}; + + my @actions = GetModificationTemplateActions( $template_id ); + + foreach my $a ( @actions ) { + my $action = $a->{'action'}; + my $field_number = $a->{'field_number'}; + my $from_field = $a->{'from_field'}; + my $from_subfield = $a->{'from_subfield'}; + my $field_value = $a->{'field_value'}; + my $to_field = $a->{'to_field'}; + my $to_subfield = $a->{'to_subfield'}; + my $to_regex = $a->{'to_regex'}; + my $conditional = $a->{'conditional'}; + my $conditional_field = $a->{'conditional_field'}; + my $conditional_subfield = $a->{'conditional_subfield'}; + my $conditional_comparison = $a->{'conditional_comparison'}; + my $conditional_value = $a->{'conditional_value'}; + my $conditional_regex = $a->{'conditional_regex'}; + + my $eval = "$action( \$record, '$from_field', '$from_subfield', "; + + if ( $field_value ) { + C4::Koha::Log( "Field value before replacements: $field_value" ) if ( DEBUG >= 3 ); + warn( "Field value before replacements: $field_value" ) if ( DEBUG >= 3 ); + + $field_value =~ s/__CURRENTDATE__/$current_date/g; + $field_value =~ s/__BRANCHCODE__/$branchcode/g; + + $eval .= " '$field_value' "; + + C4::Koha::Log( "Field value after replacements: $field_value" ) if ( DEBUG >= 3 ); + warn( "Field value after replacements: $field_value" ) if ( DEBUG >= 3 ); + } elsif ( $to_field ) { + $eval .= " '$to_field', '$to_subfield', '$to_regex' "; + } + + $eval .= ", '$field_number' " if ( $field_number ); + $eval .= ') '; + + if ( $conditional ) { + $eval .= " $conditional ( "; + + if ( $conditional_comparison eq 'exists' ) { + $eval .= "field_exists( \$record, '$conditional_field', '$conditional_subfield' )"; + + } elsif ( $conditional_comparison eq 'not_exists' ) { + $eval .= "!field_exists( \$record, '$conditional_field', '$conditional_subfield' )"; + + } elsif ( $conditional_comparison eq 'equals' ) { + $eval .= "field_equals( \$record, '$conditional_value', '$conditional_field', '$conditional_subfield', '$conditional_regex' ) "; + + } elsif ( $conditional_comparison eq 'not_equals' ) { + $eval .= "!field_equals( \$record, '$conditional_value', '$conditional_field', '$conditional_subfield', '$conditional_regex' ) "; + } + + $eval .= " )"; + } + + $eval .= ";"; + + C4::Koha::Log("eval $eval") if DEBUG >= 2; + warn("eval $eval") if DEBUG >= 2; + eval $eval; + C4::Koha::Log( $record->as_formatted() ) if DEBUG >= 10; + warn( $record->as_formatted() ) if DEBUG >= 10; + + } +} +1; +__END__ + +=head1 AUTHOR + +Kyle M Hall + +=cut --- a/Koha/SimpleMARC.pm +++ a/Koha/SimpleMARC.pm @@ -0,0 +1,331 @@ +package Koha::SimpleMARC; + +# Copyright 2009 Kyle M. Hall + +use strict; +use warnings; + +#use MARC::Record; + +require Exporter; + +our @ISA = qw(Exporter); +our %EXPORT_TAGS = ( 'all' => [ qw( + +) ] ); + +our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); + +our @EXPORT = qw( + read_field + update_field + copy_field + move_field + delete_field + field_exists + field_equals +); + +our $VERSION = '0.01'; + +our $debug = 0; + +=head1 NAME + +SimpleMARC - Perl modle for making simple MARC record alterations. + +=head1 SYNOPSIS + + use SimpleMARC; + +=head1 DESCRIPTION + +SimpleMARC is designed to make writing scripts +to modify MARC records simple and easy. + +Every function in the modules requires a +MARC::Record object as its first parameter. + +=head1 AUTHOR + +Kyle Hall kyle.m.hall@gmail.com + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2009 by Kyle Hall + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.8.7 or, +at your option, any later version of Perl 5 you may have available. + +=head1 FUNCTIONS + +=head2 + + copy_field( $record, $fromFieldName, $fromSubfieldName, $toFieldName, $toSubfieldName[, $regex[, $n ] ] ); + + Copies a value from one field to another. If a regular expression ( $regex ) is supplied, + the value will be transformed by the given regex before being copied into the new field. + Example: $regex = 's/Old Text/Replacement Text/' + + If $n is passed, copy_field will only copy the Nth field of the list of fields. + E.g. $n = 1 will only use the first field's value, $n = 2 will use only the 2nd field's value. + +=cut + +sub copy_field { + my ( $record, $fromFieldName, $fromSubfieldName, $toFieldName, $toSubfieldName, $regex, $n ) = @_; + C4::Koha::Log( "C4::SimpleMARC::copy_field( '$record', '$fromFieldName', '$fromSubfieldName', '$toFieldName', '$toSubfieldName', '$regex', '$n' )" ) if $debug; + + if ( ! ( $record && $fromFieldName && $toFieldName ) ) { return; } + + my @values = read_field( $record, $fromFieldName, $fromSubfieldName ); + @values = ( $values[$n-1] ) if ( $n ); + C4::Koha::Log( "@values = read_field( $record, $fromFieldName, $fromSubfieldName )" ) if $debug >= 3; + + if ( $regex ) { + foreach my $value ( @values ) { + C4::Koha::Log( "\$value =~ s$regex" ) if ( $debug >= 3 ); + eval "\$value =~ s$regex"; + } + } + + update_field( $record, $toFieldName, $toSubfieldName, @values ); + +} + +=head2 + + update_field( $record, $fieldName, $subfieldName, $value[, $value,[ $value ... ] ] ); + + Updates a field with the given value, creating it if neccessary. + + If multiple values are supplied, they will be used to update a list of repeatable fields + until either the fields or the values are all used. + + If a single value is supplied for a repeated field, that value will be used to update + each of the repeated fields. + +=cut + +sub update_field { + my ( $record, $fieldName, $subfieldName, @values ) = @_; + C4::Koha::Log( "C4::SimpleMARC::update_field( $record, $fieldName, $subfieldName, @values )" ) if $debug; + + if ( ! ( $record && $fieldName ) ) { return; } + + if ( @values eq 1 ) { + _update_repeatable_field_with_single_value( $record, $fieldName, $subfieldName, @values ); + return; + } + + my $i = 0; + my $field; + if ( $subfieldName ) { + if ( my @fields = $record->field( $fieldName ) ) { + foreach my $field ( @fields ) { + $field->update( "$subfieldName" => $values[$i++] ); + } + } else { + ## Field does not exist, create it. + foreach my $value ( @values ) { + $field = MARC::Field->new( $fieldName, '', '', "$subfieldName" => $values[$i++] ); + $record->append_fields( $field ); + } + } + } else { ## No subfield + if ( my @fields = $record->field( $fieldName ) ) { + foreach my $field ( @fields ) { + $field->update( $values[$i++] ); + } + } else { + ## Field does not exists, create it + foreach my $value ( @values ) { + $field = MARC::Field->new( $fieldName, $value ); + $record->append_fields( $field ); + } + } + } +} + +=head2 + + my @values = read_field( $record, $fieldName[, $subfieldName, [, $n ] ] ); + + Returns an array of field values for the given field and subfield + + If $n is given, it will return only the $nth value of the array. + E.g. If $n = 1, it return the 1st value, if $n = 3, it will return the 3rd value. + +=cut + +sub read_field { + my ( $record, $fieldName, $subfieldName, $n ) = @_; + C4::Koha::Log( "C4::SimpleMARC::read_field( '$record', '$fieldName', '$subfieldName', '$n' )" ) if $debug; + + my @fields = $record->field( $fieldName ); + + return @fields unless $subfieldName; + + my @subfields; + foreach my $field ( @fields ) { + my @sf = $field->subfield( $subfieldName ); + push( @subfields, @sf ); + } + + if ( $n ) { + return $subfields[$n-1]; + } else { + return @subfields; + } +} + +=head2 + + $bool = field_exists( $record, $fieldName[, $subfieldName ]); + + Returns true if the field exits, false otherwise. + +=cut + +sub field_exists { + my ( $record, $fieldName, $subfieldName ) = @_; + C4::Koha::Log( "C4::SimpleMARC::field_exists( $record, $fieldName, $subfieldName )" ) if $debug; + + if ( ! $record ) { return; } + + my $return = 0; + if ( $fieldName && $subfieldName ) { + $return = $record->field( $fieldName ) && $record->subfield( $fieldName, $subfieldName ); + } elsif ( $fieldName ) { + $return = $record->field( $fieldName ) && 1; + } + + C4::Koha::Log( "C4:SimpleMARC::field_exists: Returning '$return'" ) if $debug >= 2; + return $return; +} + +=head2 + + $bool = field_equals( $record, $value, $fieldName[, $subfieldName[, $regex [, $n ] ] ]); + + Returns true if the field equals the given value, false otherwise. + + If a regular expression ( $regex ) is supplied, the value will be compared using + the given regex. Example: $regex = 'm/sought_text/' + + If $n is passed, the Nth field of a repeatable series will be used for comparison. + Set $n to 1 or leave empty for a non-repeatable field. + +=cut + +sub field_equals { + my ( $record, $value, $fieldName, $subfieldName, $regex, $n ) = @_; + $n = 1 unless ( $n ); ## $n defaults to first field of a repeatable field series + C4::Koha::Log( "C4::SimpleMARC::field_equals( '$record', '$value', '$fieldName', '$subfieldName', '$regex', '$n')" ) if $debug; + + if ( ! $record ) { return; } + + my @field_values = read_field( $record, $fieldName, $subfieldName, $n ); + my $field_value = $field_values[$n-1]; + + if ( $regex ) { + C4::Koha::Log( "Testing '$field_value' =~ m$value" ) if $debug >= 3; + return eval "\$field_value =~ m$value"; + } else { + return $field_value eq $value; + } +} + +=head2 + + move_field( $record, $fromFieldName, $fromSubfieldName, $toFieldName, $toSubfieldName[, $regex [, $n ] ] ); + + Moves a value from one field to another. If a regular expression ( $regex ) is supplied, + the value will be transformed by the given regex before being moved into the new field. + Example: $regex = 's/Old Text/Replacement Text/' + + If $n is passed, only the Nth field will be moved. $n = 1 + will move the first repeatable field, $n = 3 will move the third. + +=cut + +sub move_field { + my ( $record, $fromFieldName, $fromSubfieldName, $toFieldName, $toSubfieldName, $regex, $n ) = @_; + C4::Koha::Log( "C4::SimpleMARC::move_field( '$record', '$fromFieldName', '$fromSubfieldName', '$toFieldName', '$toSubfieldName', '$regex', '$n' )" ) if $debug; + copy_field( $record, $fromFieldName, $fromSubfieldName, $toFieldName, $toSubfieldName, $regex, $n ); + delete_field( $record, $fromFieldName, $fromSubfieldName, $n ); +} + +=head2 + + delete_field( $record, $fieldName[, $subfieldName [, $n ] ] ); + + Deletes the given field. + + If $n is passed, only the Nth field will be deleted. $n = 1 + will delete the first repeatable field, $n = 3 will delete the third. + +=cut + +sub delete_field { + my ( $record, $fieldName, $subfieldName, $n ) = @_; + C4::Koha::Log( "C4::SimpleMARC::delete_field( '$record', '$fieldName', '$subfieldName', '$n' )" ) if $debug; + + my @fields = $record->field( $fieldName ); + + @fields = ( $fields[$n-1] ) if ( $n ); + + if ( @fields && !$subfieldName ) { + foreach my $field ( @fields ) { + $record->delete_field( $field ); + } + } elsif ( @fields && $subfieldName ) { + foreach my $field ( @fields ) { + $field->delete_subfield( code => $subfieldName ); + } + } +} + +=head2 + + _update_repeatable_field_with_single_value( $record, $fieldName, $subfieldName, $value ); + + Updates a repeatable field, giving all existing copies of that field the given value. + + This is an internal function, and thus is not exported. + +=cut + +sub _update_repeatable_field_with_single_value { + my ( $record, $fieldName, $subfieldName, $value ) = @_; + C4::Koha::Log( "C4::SimpleMARC::_update_repeatable_field_with_single_value( $record, $fieldName, $subfieldName, $value )" ) if $debug; + + if ( ! ( $record && $fieldName ) ) { return; } + + my $field; + if ( $subfieldName ) { + if ( my @fields = $record->field( $fieldName ) ) { + foreach my $field ( @fields ) { + $field->update( "$subfieldName" => $value ); + } + } else { + ## Field does not exist, create it. + $field = MARC::Field->new( $fieldName, '', '', "$subfieldName" => $value ); + $record->append_fields( $field ); + } + } else { ## No subfield + if ( my @fields = $record->field( $fieldName ) ) { + foreach my $field ( @fields ) { + $field->update( $value ); + } + } else { + ## Field does not exists, create it + $field = MARC::Field->new( $fieldName, $value ); + $record->append_fields( $field ); + } + } +} + +1; +__END__ --- a/installer/data/mysql/en/mandatory/userpermissions.sql +++ a/installer/data/mysql/en/mandatory/userpermissions.sql @@ -43,6 +43,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES (13, 'moderate_tags', 'Moderate patron tags'), (13, 'rotating_collections', 'Manage rotating collections'), (13, 'upload_local_cover_images', 'Upload local cover images'), + (13, 'marc_modification_templates', 'Manage marc modification templates'), (15, 'check_expiration', 'Check the expiration of a serial'), (15, 'claim_serials', 'Claim missing serials'), (15, 'create_subscription', 'Create a new subscription'), --- a/installer/data/mysql/kohastructure.sql +++ a/installer/data/mysql/kohastructure.sql @@ -3232,6 +3232,42 @@ CREATE TABLE IF NOT EXISTS plugin_data ( PRIMARY KEY (plugin_class,plugin_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- +-- Table structure for table 'marc_modification_templates' +-- + +CREATE TABLE IF NOT EXISTS marc_modification_templates ( + template_id int(11) NOT NULL AUTO_INCREMENT, + name text NOT NULL, + PRIMARY KEY (template_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Table structure for table 'marc_modification_template_actions' +-- + +CREATE TABLE IF NOT EXISTS marc_modification_template_actions ( + mmta_id int(11) NOT NULL AUTO_INCREMENT, + template_id int(11) NOT NULL, + ordering int(3) NOT NULL, + action enum('delete_field','update_field','move_field','copy_field') NOT NULL, + field_number smallint(6) NOT NULL DEFAULT '0', + from_field varchar(3) NOT NULL, + from_subfield varchar(1) DEFAULT NULL, + field_value varchar(100) DEFAULT NULL, + to_field varchar(3) DEFAULT NULL, + to_subfield varchar(1) DEFAULT NULL, + to_regex text, + conditional enum('if','unless') DEFAULT NULL, + conditional_field varchar(3) DEFAULT NULL, + conditional_subfield varchar(1) DEFAULT NULL, + conditional_comparison enum('exists','not_exists','equals','not_equals') DEFAULT NULL, + conditional_value text, + conditional_regex tinyint(1) NOT NULL DEFAULT '0', + description text, + PRIMARY KEY (mmta_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; --- a/installer/data/mysql/updatedatabase.pl +++ a/installer/data/mysql/updatedatabase.pl @@ -7169,6 +7169,45 @@ if ( CheckVersion($DBversion) ) { SetVersion ($DBversion); } +$DBversion = "3.13.00.XXX"; +if (C4::Context->preference("Version") < TransformToNum($DBversion)) { + $dbh->do("CREATE TABLE IF NOT EXISTS marc_modification_templates ( + template_id int(11) NOT NULL auto_increment, + name text NOT NULL, + PRIMARY KEY (template_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;" + ); + + $dbh->do(" + CREATE TABLE IF NOT EXISTS marc_modification_template_actions ( + mmta_id int(11) NOT NULL auto_increment, + template_id int(11) NOT NULL, + ordering int(3) NOT NULL, + action enum('delete_field','update_field','move_field','copy_field') NOT NULL, + field_number smallint(6) NOT NULL default '0', + from_field varchar(3) NOT NULL, + from_subfield varchar(1) NULL, + field_value varchar(100) default NULL, + to_field varchar(3) default NULL, + to_subfield varchar(1) default NULL, + to_regex text, + conditional enum('if','unless') default NULL, + conditional_field varchar(3) default NULL, + conditional_subfield varchar(1) default NULL, + conditional_comparison enum('exists','not_exists','equals','not_equals') default NULL, + conditional_value text, + conditional_regex tinyint(1) NOT NULL default '0', + description text, + PRIMARY KEY (mmta_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + "); + + $dbh->do("INSERT INTO permissions (module_bit, code, description) VALUES ('13', 'marc_modification_templates', 'Manage marc modification templates')"); + + print "Upgrade to $DBversion done ( Bug 8015: Added tables for MARC Modification Framework )\n"; + SetVersion($DBversion); +} + =head1 FUNCTIONS =head2 TableExists($table) --- a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc @@ -68,6 +68,9 @@
  • Rotating collections
  • [% END %] --> + [% IF ( CAN_user_tools_marc_modification_templates ) %] +
  • Manage MARC modification templates
  • + [% END %] [% IF ( CAN_user_tools_stage_marc_import ) %]
  • Stage MARC for import
  • [% END %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt @@ -68,8 +68,8 @@
    Create and manage Bibliographic frameworks that define the characteristics of your MARC Records (field and subfield definitions) as well as templates for the MARC editor.
    Koha to MARC mapping
    Define the mapping between the Koha transactional database (SQL) and the MARC Bibliographic records. Note that the mapping can be defined through MARC Bibliographic Framework. This tool is just a shortcut to speed up linkage.
    -
    Keywords to MARC mapping
    -
    Define the mapping between keywords and MARC fields, those keywords are used to find some datas independently of the framework.
    +
    Keywords to MARC mapping
    +
    Define the mapping between keywords and MARC fields, those keywords are used to find some datas independently of the framework.
    MARC Bibliographic framework test
    Checks the MARC structure. If you change your MARC Bibliographic framework it's recommended that you run this tool to test for errors in your definition.
    Authority types
    --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/marc_modification_templates.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/marc_modification_templates.tt @@ -0,0 +1,492 @@ +[% INCLUDE 'doc-head-open.inc' %] +Koha › Tools › MARC Modification Templates +[% INCLUDE 'doc-head-close.inc' %] + + + + + + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'cat-search.inc' %] + + + +
    +
    +
    +

    MARC Modification Templates

    + + [% IF ( TemplatesLoop ) %] + +
    + + + + +
    + +
    + + + +
    + + + [% IF ( ActionsLoop ) %] + + + + + + + + + + + + + [% FOREACH ActionsLoo IN ActionsLoop %] + + + + + + + + + + [% END %] +
    Actions for this template
    Change OrderOrderActionDescription  
    + + Go up + + + + Go top + + + + Go bottom + + + + Go down + + [% ActionsLoo.ordering %] + [% IF ( ActionsLoo.action_delete_field ) %] Delete [% END %] + [% IF ( ActionsLoo.action_update_field ) %] Update [% END %] + [% IF ( ActionsLoo.action_move_field ) %] Move [% END %] + [% IF ( ActionsLoo.action_copy_field ) %] Copy [% END %] + + [% UNLESS ( ActionsLoo.action_update_field ) %] + [% IF ( ActionsLoo.field_number ) %] + 1st + [% END %] + [% END %] + + field + + [% ActionsLoo.from_field %][% IF ( ActionsLoo.from_subfield ) %]$[% ActionsLoo.from_subfield %][% END %] + + [% IF ( ActionsLoo.field_value ) %] + with value [% ActionsLoo.field_value %] + [% END %] + + [% IF ( ActionsLoo.to_field ) %] + to [% ActionsLoo.to_field %][% IF ( ActionsLoo.to_subfield ) %]$[% ActionsLoo.to_subfield %][% END %] + + [% IF ( ActionsLoo.to_regex ) %] + using RegEx s[% ActionsLoo.to_regex %] + [% END %] + [% END %] + + [% IF ( ActionsLoo.conditional ) %] + [% IF ( ActionsLoo.conditional_if ) %] if [% END %] + [% IF ( ActionsLoo.conditional_unless ) %] unless [% END %] + + [% ActionsLoo.conditional_field %][% IF ( ActionsLoo.conditional_subfield ) %]$[% ActionsLoo.conditional_subfield %][% END %] + + [% IF ( ActionsLoo.conditional_comparison_exists ) %] exists [% END %] + [% IF ( ActionsLoo.conditional_comparison_not_exists ) %] does not exist [% END %] + [% IF ( ActionsLoo.conditional_comparison_equals ) %] matches [% END %] + [% IF ( ActionsLoo.conditional_comparison_not_equals ) %] does not match [% END %] + + [% IF ( ActionsLoo.conditional_regex ) %] RegEx m[% END %][% ActionsLoo.conditional_value %] + [% END %] + [% ActionsLoo.description %]EditDelete
    + [% ELSE %] +

    There are no defined actions for this template.

    + [% END %] + +
    + +
    + Add A New Action + + + + + + + + field(s) + + + + + +

    + + + + + + + + + +

    + + + +

    + + +

    +
    + + [% ELSE %] +

    There are no defined templates. Please create a template first.

    + [% END %] + +
    +
    + Create A New Template + + + + + + + + [% IF ( template_id ) %] + + + + [% END %] +
    +
    + + + +
    +
    + +
    + [% INCLUDE 'tools-menu.inc' %] +
    +
    +[% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stage-marc-import.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stage-marc-import.tt @@ -117,6 +117,23 @@ function CheckForm(f) { + [% IF MarcModificationTemplatesLoop %] +
    + Use MARC Modification Template: +
      +
    1. + + +
    2. +
    +
    + [% END %] +
    Look for existing records in catalog?
    1. --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt @@ -140,10 +140,15 @@ + [% IF ( CAN_user_tools_marc_modification_templates ) %] +
      MARC Modification Templates
      +
      Manage templates for modifying MARC records during import.
      + [% END %] + [% IF ( CAN_user_tools_stage_marc_import ) %]
      Stage MARC records for import
      Stage MARC records into the reservoir.
      --- a/tools/marc_modification_templates.pl +++ a/tools/marc_modification_templates.pl @@ -0,0 +1,151 @@ +#!/usr/bin/perl +# Copyright 2010 Kyle M Hall +# +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use strict; +use warnings; + +use CGI; + +use C4::Auth; +use C4::Koha; +use C4::Output; +use Koha::MarcModificationTemplates; + +my $cgi = new CGI; + +my $op = $cgi->param('op'); +my $template_id = $cgi->param('template_id'); + +my ($template, $loggedinuser, $cookie) + = get_template_and_user({template_name => "tools/marc_modification_templates.tmpl", + query => $cgi, + type => "intranet", + authnotrequired => 0, + flagsrequired => { tools => 'marc_modfication_templates' }, + debug => 1, + }); + +if ( $op eq "create_template" ) { + $template_id = '' unless $cgi->param('duplicate_current_template'); + $template_id = AddModificationTemplate( $cgi->param('template_name'), $template_id ); + +} elsif ( $op eq "delete_template" ) { + + DelModificationTemplate( $template_id ); + $template_id = ''; + +} elsif ( $op eq "add_action" ) { + + my $mmta_id = $cgi->param('mmta_id'); + my $action = $cgi->param('action'); + my $field_number = $cgi->param('field_number'); + my $from_field = $cgi->param('from_field'); + my $from_subfield = $cgi->param('from_subfield'); + my $field_value = $cgi->param('field_value'); + my $to_field = $cgi->param('to_field'); + my $to_subfield = $cgi->param('to_subfield'); + my $to_regex = $cgi->param('to_regex'); + my $conditional = $cgi->param('conditional'); + my $conditional_field = $cgi->param('conditional_field'); + my $conditional_subfield = $cgi->param('conditional_subfield'); + my $conditional_comparison = $cgi->param('conditional_comparison'); + my $conditional_value = $cgi->param('conditional_value'); + my $conditional_regex = $cgi->param('conditional_regex') eq 'on'; + my $description = $cgi->param('description'); + + unless ( $mmta_id ) { + AddModificationTemplateAction( + $template_id, + $action, + $field_number, + $from_field, + $from_subfield, + $field_value, + $to_field, + $to_subfield, + $to_regex, + $conditional, + $conditional_field, + $conditional_subfield, + $conditional_comparison, + $conditional_value, + $conditional_regex, + $description + ); + } else { + ModModificationTemplateAction( + $mmta_id, + $action, + $field_number, + $from_field, + $from_subfield, + $field_value, + $to_field, + $to_subfield, + $to_regex, + $conditional, + $conditional_field, + $conditional_subfield, + $conditional_comparison, + $conditional_value, + $conditional_regex, + $description + ); + + } + +} elsif ( $op eq "delete_action" ) { + DelModificationTemplateAction( $cgi->param('mmta_id') ); + +} elsif ( $op eq "move_action" ) { + + MoveModificationTemplateAction( $cgi->param('mmta_id'), $cgi->param('where') ); + +} + +my @templates = GetModificationTemplates( $template_id ); + +unless ( $template_id ) { + $template_id = $templates[0]->{'template_id'}; + @templates = GetModificationTemplates( $template_id ); +} + +my @actions = GetModificationTemplateActions( $template_id ); +foreach my $action ( @actions ) { + $action->{'action_delete_field'} = ( $action->{'action'} eq 'delete_field' ); + $action->{'action_update_field'} = ( $action->{'action'} eq 'update_field' ); + $action->{'action_move_field'} = ( $action->{'action'} eq 'move_field' ); + $action->{'action_copy_field'} = ( $action->{'action'} eq 'copy_field' ); + + $action->{'conditional_if'} = ( $action->{'conditional'} eq 'if' ); + $action->{'conditional_unless'} = ( $action->{'conditional'} eq 'unless' ); + + $action->{'conditional_comparison_exists'} = ( $action->{'conditional_comparison'} eq 'exists' ); + $action->{'conditional_comparison_not_exists'} = ( $action->{'conditional_comparison'} eq 'not_exists' ); + $action->{'conditional_comparison_equals'} = ( $action->{'conditional_comparison'} eq 'equals' ); + $action->{'conditional_comparison_not_equals'} = ( $action->{'conditional_comparison'} eq 'not_equals' ); +} + +$template->param( + TemplatesLoop => \@templates, + ActionsLoop => \@actions, + + template_id => $template_id, +); + +output_html_with_http_headers $cgi, $cookie, $template->output; --- a/tools/stage-marc-import.pl +++ a/tools/stage-marc-import.pl @@ -41,6 +41,7 @@ use C4::ImportBatch; use C4::Matcher; use C4::UploadedFile; use C4::BackgroundJob; +use Koha::MarcModificationTemplates; my $input = new CGI; my $dbh = C4::Context->dbh; @@ -57,6 +58,8 @@ my $item_action = $input->param('item_action'); my $comments = $input->param('comments'); my $record_type = $input->param('record_type'); my $encoding = $input->param('encoding'); +my $marc_modification_template = $input->param('marc_modification_template_id'); + my ($template, $loggedinuser, $cookie) = get_template_and_user({template_name => "tools/stage-marc-import.tmpl", query => $input, @@ -131,7 +134,7 @@ if ($completedJobID) { } # FIXME branch code - my ($batch_id, $num_valid, $num_items, @import_errors) = BatchStageMarcRecords($record_type, $encoding, $marcrecord, $filename, $comments, '', $parse_items, 0, 50, staging_progress_callback($job, $dbh)); + my ($batch_id, $num_valid, $num_items, @import_errors) = BatchStageMarcRecords($record_type, $encoding, $marcrecord, $filename, $marc_modification_template, $comments, '', $parse_items, 0, 50, staging_progress_callback($job, $dbh)); $dbh->commit(); @@ -180,6 +183,8 @@ if ($completedJobID) { matcher_code => $matcher_code, import_batch_id => $batch_id ); + + } } else { @@ -189,6 +194,10 @@ if ($completedJobID) { } my @matchers = C4::Matcher::GetMatcherList(); $template->param(available_matchers => \@matchers); + + my @templates = GetModificationTemplates(); + $template->param( MarcModificationTemplatesLoop => \@templates ); + } output_html_with_http_headers $input, $cookie, $template->output; --