Bug 16994: Import and export MARC modification templates

This patch adds export and import functions to the MarcModificationTemplates tools
The different templates can be exported either individually or all of them.
On import you can choose to either skip or overwrite existing templates with the same name.

Test plan:
- create a Marc Modification templates with one field creation action.
- click on "Export all" in the menu and save the file on your computer.
- add a new action in the template (ie, update field).
- click on "Import" in the menu and choose the downloaded file.
- by default the import tool doesn't overwrite
- there should be a message saying "skipped 1 template"
- the template should still have 2 actions.
- import the same file again but uncheck the "skip existing template".
- this time the template should be back to only one action.
- delete the template
- import the same file again, the template should be back with a single action.

Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Arthur Suzuki 2025-12-16 12:59:20 +00:00 committed by Lucas Gass
parent 04ca348222
commit 33dc9ac24a
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
3 changed files with 327 additions and 0 deletions

View File

@ -34,6 +34,7 @@ use Koha::SimpleMARC qw(
);
use Koha::MoreUtils;
use Koha::DateUtils qw( dt_from_string );
use JSON qw( encode_json decode_json );
use vars qw(@ISA @EXPORT);
@ -54,6 +55,9 @@ BEGIN {
ModifyRecordsWithTemplate
ModifyRecordWithTemplate
ExportModificationTemplates
ImportModificationTemplates
);
}
@ -735,6 +739,191 @@ sub ModifyRecordWithTemplate {
return;
}
=head2
ExportModificationTemplates
my $json = ExportModificationTemplates( $template_ids );
Exports MARC modification templates to JSON format.
If $template_ids is provided, only export those templates.
If not provided, export all templates.
Returns a JSON string containing the exported templates.
=cut
sub ExportModificationTemplates {
my ($template_ids) = @_;
my $dbh = C4::Context->dbh;
my @templates;
if ( $template_ids && @$template_ids ) {
my $placeholders = join( ',', ('?') x @$template_ids );
my $sth = $dbh->prepare(
"SELECT * FROM marc_modification_templates WHERE template_id IN ($placeholders) ORDER BY name");
$sth->execute(@$template_ids);
while ( my $template = $sth->fetchrow_hashref() ) {
push( @templates, $template );
}
} else {
my $sth = $dbh->prepare("SELECT * FROM marc_modification_templates ORDER BY name");
$sth->execute();
while ( my $template = $sth->fetchrow_hashref() ) {
push( @templates, $template );
}
}
my $export_data = {
version => 1,
templates => [],
};
foreach my $template (@templates) {
my $template_id = $template->{'template_id'};
my @actions = GetModificationTemplateActions($template_id);
my $template_export = {
template_id => $template_id,
name => $template->{'name'},
actions => [],
};
foreach my $action (@actions) {
my $action_export = {
mmta_id => $action->{'mmta_id'},
ordering => $action->{'ordering'},
action => $action->{'action'},
field_number => $action->{'field_number'},
from_field => $action->{'from_field'},
from_subfield => $action->{'from_subfield'},
field_value => $action->{'field_value'},
to_field => $action->{'to_field'},
to_subfield => $action->{'to_subfield'},
to_regex_search => $action->{'to_regex_search'},
to_regex_replace => $action->{'to_regex_replace'},
to_regex_modifiers => $action->{'to_regex_modifiers'},
conditional => $action->{'conditional'},
conditional_field => $action->{'conditional_field'},
conditional_subfield => $action->{'conditional_subfield'},
conditional_comparison => $action->{'conditional_comparison'},
conditional_value => $action->{'conditional_value'},
conditional_regex => $action->{'conditional_regex'},
description => $action->{'description'},
};
push( @{ $template_export->{actions} }, $action_export );
}
push( @{ $export_data->{templates} }, $template_export );
}
return encode_json($export_data);
}
=head2
ImportModificationTemplates
my $result = ImportModificationTemplates( $json, $skip_existing );
Imports MARC modification templates from JSON format.
$json is the JSON string to import.
$skip_existing: if true, skip templates that already exist (default: false)
Returns a hashref with:
- success: number of successfully imported templates
- skipped: number of skipped templates
- errors: arrayref of error messages
=cut
sub ImportModificationTemplates {
my ( $json, $skip_existing ) = @_;
my $result = {
success => 0,
skipped => 0,
errors => [],
};
my $data;
eval { $data = decode_json($json); };
if ($@) {
push( @{ $result->{errors} }, "Failed to parse JSON: $@" );
return $result;
}
my $version = $data->{version} || 0;
if ( $version != 1 ) {
push( @{ $result->{errors} }, "Unsupported export version: $version" );
return $result;
}
my $templates = $data->{templates} || [];
my $dbh = C4::Context->dbh;
foreach my $template_data (@$templates) {
my $template_name = $template_data->{name};
my $actions = $template_data->{actions} || [];
# Check if template already exists
my $sth = $dbh->prepare("SELECT template_id FROM marc_modification_templates WHERE name = ?");
$sth->execute($template_name);
my $existing = $sth->fetchrow_hashref();
if ( $existing && $skip_existing ) {
$result->{skipped}++;
next;
}
my $template_id;
if ($existing) {
$template_id = $existing->{template_id};
# Delete existing actions for this template
$sth = $dbh->prepare("DELETE FROM marc_modification_template_actions WHERE template_id = ?");
$sth->execute($template_id);
$result->{overwrote}++;
} else {
# Create new template
$sth = $dbh->prepare("INSERT INTO marc_modification_templates (name) VALUES (?)");
$sth->execute($template_name);
$sth = $dbh->prepare("SELECT template_id FROM marc_modification_templates WHERE name = ?");
$sth->execute($template_name);
my $row = $sth->fetchrow_hashref();
$template_id = $row->{template_id};
}
# Add new actions
foreach my $action_data (@$actions) {
AddModificationTemplateAction(
$template_id,
$action_data->{action},
$action_data->{field_number},
$action_data->{from_field},
$action_data->{from_subfield},
$action_data->{field_value},
$action_data->{to_field},
$action_data->{to_subfield},
$action_data->{to_regex_search},
$action_data->{to_regex_replace},
$action_data->{to_regex_modifiers},
$action_data->{conditional},
$action_data->{conditional_field},
$action_data->{conditional_subfield},
$action_data->{conditional_comparison},
$action_data->{conditional_value},
$action_data->{conditional_regex},
$action_data->{description},
);
}
$result->{success}++;
}
return $result;
}
1;
__END__

View File

@ -74,6 +74,17 @@
<input type="hidden" name="op" value="cud-delete_template" />
<button type="submit" class="btn btn-default confirm-delete-template"><i class="fa fa-fw fa-trash-can"></i> Delete template</button>
</form>
[% ELSE %]
<div class="btn-group">
<a href="#" class="btn btn-default" data-bs-toggle="dropdown"><i class="fa fa-download"></i> Export</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="/cgi-bin/koha/tools/marc_modification_templates.pl?op=export&export_all=1">Export all templates</a>
[% FOREACH TemplatesLoo IN TemplatesLoop %]
<a class="dropdown-item" href="/cgi-bin/koha/tools/marc_modification_templates.pl?op=export&template_ids=[% TemplatesLoo.template_id | html %]">Export [% TemplatesLoo.name | html %]</a>
[% END %]
</div>
</div>
<a href="#" class="btn btn-default" data-bs-toggle="modal" data-bs-target="#importTemplate"><i class="fa fa-upload"></i> Import</a>
[% END %]
</div>
@ -375,6 +386,69 @@
</div>
</div>
</div>
<!-- Modal to import templates -->
<div class="modal" id="importTemplate" tabindex="-1" role="dialog" aria-labelledby="LabelimportTemplate" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="LabelimportTemplate">Import MARC modification templates</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="/cgi-bin/koha/tools/marc_modification_templates.pl" enctype="multipart/form-data" id="import_template" class="validated">
[% INCLUDE 'csrf-token.inc' %]
<div class="modal-body">
<fieldset>
<p>
<label for="import_file" class="required">JSON file:</label>
<input type="file" name="import_file" id="import_file" required="required" class="required" />
<span class="required">Required</span>
</p>
<p>
<label>
<input type="checkbox" name="skip_existing" id="skip_existing" value="1" checked />
Skip existing templates (do not overwrite)
</label>
</p>
<input type="hidden" name="op" value="cud-import" />
[% IF import_error %]
[% IF import_error == 'no_file_uploaded' %]
<div class="alert alert-danger">Error: No file was uploaded.</div>
[% END %]
[% END %]
[% IF import_success || import_skipped || import_overwrite %]
<div class="alert alert-success">
[% IF import_success %]
<p>Successfully imported [% import_success | html %] template(s).</p>
[% END %]
[% IF import_skipped %]
<p>Skipped [% import_skipped | html %] template(s) that already exist.</p>
[% END %]
[% IF import_overwrote %]
<p>Overwrote [% import_overwrote | html %] template(s) that already exist.</p>
[% END %]
</div>
[% END %]
[% IF import_errors %]
[% FOREACH error IN import_errors %]
<div class="alert alert-danger">Error: [% error | html %]</div>
[% END %]
[% END %]
</fieldset>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Import</button>
<button type="button" class="btn btn-default" data-bs-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
[% END %]
[% MACRO jsinclude BLOCK %]
@ -384,6 +458,13 @@
[% IF ActionsLoop %]
var mmtas = [% ActionsLoop.json | $raw %]
[% END %]
// Show import modal if there are import results
[% IF show_import_modal %]
$(document).ready(function() {
$('#importTemplate').modal('show');
});
[% END %]
</script>
[% Asset.js("js/marc_modification_templates.js") | $raw %]
[% END %]

View File

@ -27,8 +27,10 @@ use C4::MarcModificationTemplates qw(
AddModificationTemplateAction
DelModificationTemplate
DelModificationTemplateAction
ExportModificationTemplates
GetModificationTemplateActions
GetModificationTemplates
ImportModificationTemplates
ModModificationTemplateAction
MoveModificationTemplateAction
);
@ -115,6 +117,61 @@ if ( $op eq "cud-create_template" ) {
MoveModificationTemplateAction( scalar $cgi->param('mmta_id'), scalar $cgi->param('where') );
} elsif ( $op eq "export" ) {
my @template_ids;
if ( $cgi->param('export_all') ) {
# Export all templates
my @templates = GetModificationTemplates();
@template_ids = map { $_->{template_id} } @templates;
} else {
# Export selected templates
@template_ids = $cgi->multi_param('template_ids');
}
my $json = ExportModificationTemplates( \@template_ids );
print $cgi->header(
-type => 'application/json',
-attachment => 'marc_modification_templates.json',
-charset => 'utf-8',
);
print $json;
exit;
} elsif ( $op eq "cud-import" ) {
my $upload = $cgi->upload('import_file');
if ($upload) {
my $json;
{
local $/;
while ( my $line = <$upload> ) {
$json .= $line;
}
}
my $skip_existing = $cgi->param('skip_existing') ? 1 : 0;
my $result = ImportModificationTemplates( $json, $skip_existing );
$template->param(
import_success => $result->{success},
import_skipped => $result->{skipped},
import_overwrote => $result->{overwrote},
import_errors => $result->{errors},
show_import_modal => 1,
);
} else {
$template->param(
import_error => 'no_file_uploaded',
show_import_modal => 1,
);
}
}
my @templates = GetModificationTemplates($template_id);