Bug 11395: Add a batch record modification
authorJonathan Druart <jonathan.druart@biblibre.com>
Thu, 12 Dec 2013 20:13:53 +0000 (21:13 +0100)
committerTomas Cohen Arazi <tomascohen@gmail.com>
Thu, 5 Mar 2015 13:55:15 +0000 (14:55 +0100)
This patch offers a new tool to modify records in a batch.

This feature adds:
- a new pl/tt files tools/batch_record_modification
- a new permission: tools > records_batchmod

Test plan for biblios:
0/ Create a new marc modification template with some actions.
1/ Generate a list of biblionumbers you want to modify.
There are two ways to generate a list of biblionumbers:
- using the basket: do a search, add some biblio to your basket, open
  the basket and click on the "Action" button > "Modify"
- generating a list from a report
2/ On the "Batch record modification" tool verify:
- information is correct.
- the preview link show you the needed record.
3/ After clicking on the "Modify selected recors" button, verify
the records have been modified as you wanted.

Test plan for authority:
0/ Create a new marc modification template with some actions.
1/ Generate a list of authid using a report:
2/ On the "Batch record modification" tool verify:
- authorities are display with the summary.
- the preview link show you the needed record.
3/ After clicking on the "Modify selected recors" button, verify
the records have been modified as you wanted.

Catch of errors:
if an error occurs during the modification process, the tool
displays an error message.

Signed-off-by: Brendan Gallagher <brendan@bywatersolutions.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@gmail.com>
Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_record_modification.tt [new file with mode: 0644]
tools/batch_record_modification.pl [new file with mode: 0755]

diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_record_modification.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_record_modification.tt
new file mode 100644 (file)
index 0000000..2ce3481
--- /dev/null
@@ -0,0 +1,237 @@
+[% PROCESS 'authorities-search-results.inc' %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; Tools &rsaquo; Batch record modification</title>
+[% INCLUDE 'doc-head-close.inc' %]
+[% INCLUDE 'greybox.inc' %]
+<link rel="stylesheet" type="text/css" href="[% themelang %]/css/datatables.css" />
+[% INCLUDE 'datatables.inc' %]
+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script>
+<script type="text/javascript" src="[% themelang %]/js/background-job-progressbar.js"></script>
+<script type="text/javascript">
+//<![CDATA[
+$(document).ready(function() {
+  $("#selectall").click(function(e){
+    e.preventDefault();
+    $(".records").checkCheckboxes();
+  });
+  $("#clearall").click(function(e){
+    e.preventDefault();
+    $(".records").unCheckCheckboxes();
+  });
+  $("#selectall").click();
+
+  $("table#biblios").dataTable($.extend(true, {}, dataTablesDefaults, {
+    "aoColumnDefs": [
+      { "aTargets": [ 0, 3 ], "bSortable": false, "bSearchable": false },
+      { "aTargets": [ 1 ], "sType": "num-html" }
+    ],
+    "sDom": 't',
+    "aaSorting": [],
+    "bPaginate": false
+  }));
+
+  $("table#authorities").dataTable($.extend(true, {}, dataTablesDefaults, {
+    "aoColumnDefs": [
+      { "aTargets": [ 0, 3 ], "bSortable": false, "bSearchable": false },
+      { "aTargets": [ 1 ], "sType": "num-html" }
+    ],
+    "sDom": 't',
+    "aaSorting": [],
+    "bPaginate": false
+  }));
+
+  $("#mainformsubmit").click(function(){
+    return submitBackgroundJob(document.getElementById("process"));
+  });
+
+});
+//]]>
+</script>
+</head>
+<body id="tools_batch_record_modification" class="tools">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'cat-search.inc' %]
+
+<div id="breadcrumbs">
+    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
+    <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> &rsaquo;
+    <a href="/cgi-bin/koha/tools/batch_record_modification.pl">Batch record modification</a>
+</div>
+
+<div id="doc3" class="yui-t2">
+<div id="bd">
+<div id="yui-main">
+<div class="yui-b">
+  <h1>Batch record modification</h1>
+  [% FOREACH message IN messages %]
+    [% IF message.type == 'success' %]
+      <div class="dialog message">
+    [% ELSIF message.type == 'warning' %]
+      <div class="dialog alert">
+    [% ELSIF message.type == 'error' %]
+      <div class="dialog error" style="margin:auto;">
+    [% END %]
+                  <td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblio.biblionumber %]">[% biblio.title %]</a></td>
+    [% IF message.code == 'no_action_defined_for_the_template' %]
+      The selected template (id=[% message.mmtid%]) does not exist or no action is defined.
+    [% ELSIF message.code == 'biblio_not_exists' %]
+      The biblionumber [% message.biblionumber %] does not exist in the database.
+    [% ELSIF message.code == 'authority_not_exists' %]
+      The authority id [% message.authid %] does not exist in the database.
+    [% ELSIF message.code == 'biblio_not_modified' %]
+      The biblio <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% message.biblionumber %]">[% message.biblionumber %]</a> has not been modified. An error occurred on modifying it.
+    [% ELSIF message.code == 'authority_not_modified' %]
+      The authority <a href="/cgi-bin/koha/authorities/detail.pl?authid=[% message.authid %]">[% message.authid %]</a> has not been modified. An error occurred on modifying it.
+    [% ELSIF message.code == 'biblio_modified' %]
+      The biblio <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% message.biblionumber %]">[% message.biblionumber %]</a> has successfully been modified.
+    [% ELSIF message.code == 'authority_modified' %]
+      The authority <a href="/cgi-bin/koha/authorities/detail.pl?authid=[% message.authid %]">[% message.authid %]</a> has successfully been modified.
+    [% END %]
+    [% IF message.error %]
+      (The error was: [% message.error%], see the Koha logfile for more information).
+    [% END %]
+    </div>
+  [% END %]
+  [% IF view == 'form' %]
+    <form method="post" enctype="multipart/form-data" action="/cgi-bin/koha/tools/batch_record_modification.pl">
+      <fieldset class="rows">
+        <legend>Record type</legend>
+        <ol>
+          <li><label for="biblio_type">Biblios: </label><input type="radio" name="recordtype" value="biblio" id="biblio_type" checked="checked" /></li>
+          <li><label for="authority_type">Authorities: </label><input type="radio" name="recordtype" value="authority" id="authority_type" /></li>
+        </ol>
+      </fieldset>
+      <fieldset class="rows">
+        <legend>Use a file</legend>
+        <ol>
+          <li><label for="uploadfile">File: </label> <input type="file" id="uploadfile" name="uploadfile" /></li>
+        </ol>
+      </fieldset>
+      <fieldset class="rows">
+        <legend>Or enter a list of record numbers</legend>
+        <ol>
+          <li>
+            <label for="recordnumber_list">Record number list (one per line): </label>
+            <textarea rows="10" cols="30" id="recordnumber_list" name="recordnumber_list"></textarea>
+          </li>
+        </ol>
+      </fieldset>
+      <fieldset class="rows">
+        <legend>Use MARC Modification Template:</legend>
+        <ol>
+          <li>
+            <label for="marc_modification_template_id" class="required">Modify record using the following template: </label>
+            <select name="marc_modification_template_id" id="marc_modification_template_id" required="required">
+              <option value="">Select a template</option>
+                [% FOREACH mmt IN MarcModificationTemplatesLoop %]
+                  <option value="[% mmt.template_id %]">[% mmt.name %]</option>
+                [% END %]
+            </select>
+          </li>
+        </ol>
+      </fieldset>
+      <fieldset class="action">
+        <input type="hidden" name="op" value="list" />
+        <input type="submit" value="Continue" class="button" />
+        <a class="cancel" href="/cgi-bin/koha/tools/tools-home.pl">Cancel</a>
+      </fieldset>
+    </form>
+  [% ELSIF view == 'list' %]
+    [% IF records %]
+      [% IF recordtype == 'biblio' %]
+        <div id="toolbar">
+          <a id="selectall" href="#">Select All</a>
+          | <a id="clearall" href="#">Clear All</a>
+        </div>
+        <form action="/cgi-bin/koha/tools/batch_record_modification.pl" method="post" id="process">
+          <table id="biblios" class="records">
+            <thead>
+              <tr>
+                <th></th>
+                <th>Biblionumber</th>
+                <th>Title</th>
+                <th>Preview</th>
+              </tr>
+            </thead>
+            <tbody>
+              [% FOR biblio IN records %]
+                <tr>
+                  <td><input type="checkbox" name="record_id" value="[% biblio.biblionumber %]" data-items="[% biblio.itemnumbers.size %]" data-issues="[% biblio.issues_count %]" data-reserves="[% biblio.reserves.size %]" /></td>
+                  <td>[% biblio.biblionumber %]</td>
+                  <td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblio.biblionumber %]">[% biblio.title %]</a></td>
+                  <td><a href="/cgi-bin/koha/svc/records/preview?record_type=biblio&record_id=[% biblio.biblionumber %]&mmtid=[% mmtid %]" title="MARC preview" rel="gb_page_center[600,500]">Preview MARC</a>
+
+                </tr>
+              [% END %]
+            </tbody>
+          </table>
+          <div class="note">Reminder: this action will modify all selected biblios!</div>
+      [% ELSE %]
+        <div id="toolbar">
+          <a id="selectall" href="#">Select All</a>
+          | <a id="clearall" href="#">Clear All</a>
+        </div>
+        <form action="/cgi-bin/koha/tools/batch_record_modification.pl" method="post" id="process">
+          <table id="authorities" class="records">
+            <thead>
+              <tr>
+                <th></th>
+                <th>Authid</th>
+                <th>Summary</th>
+                <th>Preview</th>
+              </tr>
+            </thead>
+            <tbody>
+              [% FOR authority IN records %]
+                <tr>
+                  <td><input type="checkbox" name="record_id" value="[% authority.authid %]" data-usage="[% authority.count_usage %]" /></td>
+                  <td><a href="/cgi-bin/koha/authorities/detail.pl?authid=[% authority.authid %]">[% authority.authid %]</a></td>
+                  <td>[% PROCESS authresult summary=authority.summary %]</td>
+                  <td><a href="/cgi-bin/koha/svc/records/preview?record_type=authority&record_id=[% authority.authid %]&mmtid=[% mmtid %]" title="MARC preview" rel="gb_page_center[600,500]">Preview MARC</a>
+                </tr>
+              [% END %]
+            </tbody>
+          </table>
+          <div class="note">Reminder: this action will modify all selected authorities!</div>
+      [% END %]
+        <fieldset class="action">
+          <input type="hidden" name="op" value="modify" />
+          <input type="hidden" name="recordtype" value="[% recordtype %]" />
+          <input type="button" id="mainformsubmit" value="Modify selected records" class="button" />
+          <input type="hidden" name="runinbackground" id="runinbackground" value="" />
+          <input type="hidden" name="completedJobID" id="completedJobID" value="" />
+          <input type="hidden" name="marc_modification_template_id" value="[% mmtid %]" />
+          <a class="cancel" href="/cgi-bin/koha/tools/batch_record_modification.pl">Cancel</a>
+        </fieldset>
+        <div id="jobpanel">
+          <div id="jobstatus">Job progress: <div id="jobprogress"></div> <span id="jobprogresspercent">0</span>%</div>
+          <div id="jobfailed"></div>
+        </div>
+      </form>
+    [% ELSE %]
+      There is no record ids defined.
+    [% END %]
+  [% ELSIF view == 'report' %]
+    [% IF report.total_records == report.total_success %]
+      All records have successfully been modified!
+    [% ELSE %]
+      [% report.total_success %] / [% report.total_records %] records have successfully been modified.
+      Some errors occurred.
+    [% END %]
+    <p><a href="/cgi-bin/koha/tools/batch_record_modification.pl" title="New batch record modification">New batch record modification</a></p>
+  [% ELSIF view == 'errors' %]
+    [% FOR error IN errors %]
+      [% IF error == 'no_template_defined' %]
+        No MARC modification template is defined. You have <a href="/cgi-bin/koha/tools/marc_modification_templates.pl">to create</a> at least one template for using this tool.
+      [% END %]
+    [% END %]
+  [% ELSE %]
+    No action defined for the template.
+  [% END %]
+</div>
+</div>
+<div class="yui-b">
+  [% INCLUDE 'tools-menu.inc' %]
+</div>
+</div>
+[% INCLUDE 'intranet-bottom.inc' %]
diff --git a/tools/batch_record_modification.pl b/tools/batch_record_modification.pl
new file mode 100755 (executable)
index 0000000..bec2f9e
--- /dev/null
@@ -0,0 +1,261 @@
+#!/usr/bin/perl
+
+# This file is part of Koha.
+#
+# Copyright 2013 BibLibre
+#
+# 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
+# <http://www.gnu.org/licenses>
+
+use Modern::Perl;
+
+use CGI;
+use List::MoreUtils qw( uniq );
+
+use C4::Auth qw( get_template_and_user );
+use C4::Output qw( output_html_with_http_headers );
+use C4::AuthoritiesMarc qw( BuildSummary GetAuthTypeCode ModAuthority );
+use C4::BackgroundJob;
+use C4::Biblio qw( GetMarcBiblio ModBiblio );
+use C4::MarcModificationTemplates qw( GetModificationTemplateActions GetModificationTemplates ModifyRecordWithTemplate );
+use Koha::Authority;
+
+my $input = new CGI;
+our $dbh = C4::Context->dbh;
+my $op = $input->param('op') // q|form|;
+my $recordtype = $input->param('recordtype') // 'biblio';
+my $mmtid = $input->param('marc_modification_template_id');
+
+my ( @messages );
+
+my ( $template, $loggedinuser, $cookie ) = get_template_and_user({
+        template_name => 'tools/batch_record_modification.tt',
+        query => $input,
+        type => "intranet",
+        authnotrequired => 0,
+        flagsrequired => { tools => 'biblio_batchmod' },
+});
+
+
+my $sessionID = $input->cookie("CGISESSID");
+
+my $runinbackground = $input->param('runinbackground');
+my $completedJobID = $input->param('completedJobID');
+if ( $completedJobID ) {
+    my $job = C4::BackgroundJob->fetch($sessionID, $completedJobID);
+    my $report = $job->get('report');
+    my $messages = $job->get('messages');
+    $template->param(
+        report => $report,
+        messages => $messages,
+        view => 'report',
+    );
+    output_html_with_http_headers $input, $cookie, $template->output;
+    exit;
+}
+
+my @templates = GetModificationTemplates();
+unless ( @templates ) {
+    $op = 'error';
+    $template->param(
+        view => 'errors',
+        errors => ['no_template_defined'],
+    );
+    output_html_with_http_headers $input, $cookie, $template->output;
+}
+
+if ( $mmtid ) {
+    my @actions = GetModificationTemplateActions( $mmtid );
+    unless ( @actions ) {
+        $op = 'form';
+        push @messages, {
+            type => 'error',
+            code => 'no_action_defined_for_the_template',
+            mmtid => $mmtid,
+        };
+    }
+}
+
+if ( $op eq 'form' ) {
+    # Display the form
+    $template->param(
+        view => 'form',
+        MarcModificationTemplatesLoop => \@templates,
+    );
+} elsif ( $op eq 'list' ) {
+    # List all records to process
+    my ( @records, @record_ids );
+    if ( my $bib_list = $input->param('bib_list') ) {
+        # Come from the basket
+        @record_ids = split /\//, $bib_list;
+        $recordtype = 'biblio';
+    } elsif ( my $uploadfile = $input->param('uploadfile') ) {
+        # A file of id is given
+        while ( my $content = <$uploadfile> ) {
+            next unless $content;
+            $content =~ s/[\r\n]*$//;
+            push @record_ids, $content if $content;
+        }
+    } else {
+        # The user enters manually the list of id
+        push @record_ids, split( /\s\n/, $input->param('recordnumber_list') );
+    }
+
+    for my $record_id ( uniq @record_ids ) {
+        if ( $recordtype eq 'biblio' ) {
+            # Retrieve biblio information
+            my $biblio = C4::Biblio::GetBiblio( $record_id );
+            unless ( $biblio ) {
+                push @messages, {
+                    type => 'warning',
+                    code => 'biblio_not_exists',
+                    biblionumber => $record_id,
+                };
+                next;
+            }
+            push @records, $biblio;
+        } else {
+            # Retrieve authority information
+            my $authority = Koha::Authority->get_from_authid( $record_id );
+            unless ( $authority ) {
+                push @messages, {
+                    type => 'warning',
+                    code => 'authority_not_exists',
+                    authid => $record_id,
+                };
+                next;
+            }
+
+            push @records, {
+                authid => $record_id,
+                summary => C4::AuthoritiesMarc::BuildSummary( $authority->record, $record_id ),
+            };
+        }
+    }
+    $template->param(
+        records => \@records,
+        mmtid => $mmtid,
+        view => 'list',
+    );
+} elsif ( $op eq 'modify' ) {
+    # We want to modify selected records!
+    my @record_ids = $input->param('record_id');
+
+    my ( $job );
+    if ( $runinbackground ) {
+        my $job_size = scalar( @record_ids );
+        $job = C4::BackgroundJob->new( $sessionID, "FIXME", $ENV{SCRIPT_NAME}, $job_size );
+        my $job_id = $job->id;
+        if (my $pid = fork) {
+            $dbh->{InactiveDestroy}  = 1;
+
+            my $reply = CGI->new("");
+            print $reply->header(-type => 'text/html');
+            print '{"jobID":"' . $job_id . '"}';
+            exit 0;
+        } elsif (defined $pid) {
+            close STDOUT;
+            close STDERR;
+        } else {
+            warn "fork failed while attempting to run $ENV{'SCRIPT_NAME'} as a background job";
+            exit 0;
+        }
+    }
+
+    my $report = {
+        total_records => 0,
+        total_success => 0,
+    };
+    my $progress = 0;
+    $dbh->{RaiseError} = 1;
+    RECORD_IDS: for my $record_id ( sort { $a <=> $b } @record_ids ) {
+        $report->{total_records}++;
+        next unless $record_id;
+
+        if ( $recordtype eq 'biblio' ) {
+            # Biblios
+            my $biblionumber = $record_id;
+
+            # Finally, modify the biblio
+            my $error = eval {
+                my $record = GetMarcBiblio( $biblionumber );
+                ModifyRecordWithTemplate( $mmtid, $record );
+                ModBiblio( $record, $biblionumber );
+            };
+            if ( $error and $error != 1 or $@ ) { # ModBiblio returns 1 if everything as gone well
+                push @messages, {
+                    type => 'error',
+                    code => 'biblio_not_modified',
+                    biblionumber => $biblionumber,
+                    error => ($@ ? $@ : $error),
+                };
+            } else {
+                push @messages, {
+                    type => 'success',
+                    code => 'biblio_modified',
+                    biblionumber => $biblionumber,
+                };
+                $report->{total_success}++;
+            }
+        } else {
+            # Authorities
+            my $authid = $record_id;
+            my $error = eval {
+                my $authority = Koha::Authority->get_from_authid( $authid );
+                my $record = $authority->record;
+                ModifyRecordWithTemplate( $mmtid, $record );
+                ModAuthority( $authid, $record, GetAuthTypeCode( $authid ) );
+            };
+            if ( $error and $error != $authid or $@ ) {
+                push @messages, {
+                    type => 'error',
+                    code => 'authority_not_modified',
+                    authid => $authid,
+                    error => ($@ ? $@ : 0),
+                };
+            } else {
+                push @messages, {
+                    type => 'success',
+                    code => 'authority_modified',
+                    authid => $authid,
+                };
+                $report->{total_success}++;
+            }
+        }
+
+        $job->set({
+            view => 'report',
+            report => $report,
+            messages => \@messages,
+        });
+        $job->progress( ++$progress ) if $runinbackground;
+    }
+
+    if ($runinbackground) {
+        $job->finish if defined $job;
+    } else {
+        $template->param(
+            view => 'report',
+            report => $report,
+            messages => \@messages,
+        );
+    }
+}
+
+$template->param(
+    messages => \@messages,
+    recordtype => $recordtype,
+);
+
+output_html_with_http_headers $input, $cookie, $template->output;