--- /dev/null
+package OpenILS::WWW::TemplateBatchBibUpdate;
+use strict;
+use warnings;
+use bytes;
+
+use Apache2::Log;
+use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log);
+use APR::Const -compile => qw(:error SUCCESS);
+use APR::Table;
+
+use Apache2::RequestRec ();
+use Apache2::RequestIO ();
+use Apache2::RequestUtil;
+use CGI;
+use Data::Dumper;
+use Text::CSV;
+
+use OpenSRF::EX qw(:try);
+use OpenSRF::Utils qw/:datetime/;
+use OpenSRF::Utils::Cache;
+use OpenSRF::System;
+use OpenSRF::AppSession;
+use XML::LibXML;
+use XML::LibXSLT;
+
+use Encode;
+use Unicode::Normalize;
+use OpenILS::Utils::Fieldmapper;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+use MARC::Record;
+use MARC::File::XML;
+
+use UNIVERSAL::require;
+
+our @formats = qw/USMARC UNIMARC XML BRE/;
+
+# set the bootstrap config and template include directory when
+# this module is loaded
+my $bootstrap;
+
+sub import {
+ my $self = shift;
+ $bootstrap = shift;
+}
+
+
+sub child_init {
+ OpenSRF::System->bootstrap_client( config_file => $bootstrap );
+ Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
+}
+
+sub handler {
+ my $r = shift;
+ my $cgi = new CGI;
+
+ my $authid = $cgi->cookie('ses') || $cgi->param('ses');
+ my $usr = verify_login($authid);
+ return show_template($r) unless ($usr);
+
+
+ my $template = $cgi->param('template');
+ return show_template($r) unless ($template);
+
+ # find some IDs ...
+ my @records;
+
+ @records = map { $_ ? ($_) : () } $cgi->param('recid');
+
+ if (!@records) { # try for a file
+ my $file = $cgi->param('idfile');
+ if ($file) {
+ my $col = $cgi->param('idcolumn') || 0;
+ my $csv = new Text::CSV;
+
+ while (<$file>) {
+ $csv->parse($_);
+ my @data = $csv->fields;
+ my $id = $data[$col];
+ $id =~ s/\D+//o;
+ next unless ($id);
+ push @records, $id;
+ }
+ }
+ }
+
+ my $e = OpenSRF::AppSession->connect('open-ils.cstore');
+ $e->request('open-ils.cstore.transaction.begin')->gather(1);
+
+ # still no records ...
+ my $container = $cgi->param('containerid');
+ if ($container) {
+ my $bucket = $e->request(
+ 'open-ils.cstore.direct.container.biblio_record_entry_bucket.retrieve',
+ $container
+ )->gather(1);
+ unless($bucket) {
+ $e->request('open-ils.cstore.transaction.rollback')->gather(1);
+ $e->disconnect;
+ $r->log->error("No such bucket $container");
+ $logger->error("No such bucket $container");
+ return Apache2::Const::NOT_FOUND;
+ }
+ my $recs = $e->request(
+ 'open-ils.cstore.direct.container.biblio_record_entry_bucket_item.search.atomic',
+ { bucket => $container }
+ )->gather(1);
+ @records = map { ($_->target_biblio_record_entry) } @$recs;
+ }
+
+ unless (@records) {
+ $e->request('open-ils.cstore.transaction.rollback')->gather(1);
+ $e->disconnect;
+ return show_template($r);
+ }
+
+ # we have a template and some record ids, so...
+
+ # insert the template record
+ my $min_id = $e->request(
+ 'open-ils.cstore.json_query',
+ { select => { bre => [{ column => 'id', transform => 'min', aggregate => 1}] }, from => 'bre' }
+ )->gather(1)->{id} - 1;
+
+ warn "new template bib id = $min_id\n";
+
+ my $tmpl_rec = Fieldmapper::biblio::record_entry->new;
+ $tmpl_rec->id($min_id);
+ $tmpl_rec->deleted('t');
+ $tmpl_rec->active('f');
+ $tmpl_rec->marc($template);
+ $tmpl_rec->creator($usr->id);
+ $tmpl_rec->editor($usr->id);
+
+ warn "about to create bib $min_id\n";
+ $e->request('open-ils.cstore.direct.biblio.record_entry.create', $tmpl_rec )->gather(1);
+
+ # create the new container for the records and the template
+ my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
+ $bucket->owner($usr->id);
+ $bucket->name('Temporary Merge Bucket');
+
+ $bucket = $e->request('open-ils.cstore.direct.container.biblio_record_entry_bucket.create', $bucket )->gather(1);
+
+ # create items in the bucket
+ my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
+ $item->bucket($bucket->id);
+ $item->target_biblio_record_entry($min_id);
+
+ $e->request('open-ils.cstore.direct.container.biblio_record_entry_bucket_item.create', $item )->gather(1);
+
+ for my $r (@records) {
+ $item->target_biblio_record_entry($r);
+ $e->request('open-ils.cstore.direct.container.biblio_record_entry_bucket_item.create', $item )->gather(1);
+ }
+
+ $e->request('open-ils.cstore.transaction.commit')->gather(1);
+ $e->disconnect;
+
+ # fire the background bucket processor
+ my $cache_key = OpenSRF::AppSession
+ ->create('open-ils.actor')
+ ->request('open-ils.cat.container.template_overlay.background', $bucket->id)
+ ->gather(1);
+
+ return show_processing_template($r, $bucket->id, \@records, $cache_key);
+}
+
+sub verify_login {
+ my $auth_token = shift;
+ return undef unless $auth_token;
+
+ my $user = OpenSRF::AppSession
+ ->create("open-ils.auth")
+ ->request( "open-ils.auth.session.retrieve", $auth_token )
+ ->gather(1);
+
+ if (ref($user) eq 'HASH' && $user->{ilsevent} == 1001) {
+ return undef;
+ }
+
+ return $user if ref($user);
+ return undef;
+}
+
+sub show_processing_template {
+ my $r = shift;
+ my $bid = shift;
+ my $recs = shift;
+ my $cache_key = shift;
+
+ my $rec_string = @$recs;
+
+ $r->content_type('text/html');
+ $r->print(<<HTML);
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+ <head>
+ <title>Merging records...</title>
+ <style type="text/css">
+ \@import '/js/dojo/dojo/resources/dojo.css';
+ \@import '/js/dojo/dijit/themes/tundra/tundra.css';
+ .hide_me { display: none; visibility: hidden; }
+ th { font-weight: bold; }
+ </style>
+
+ <script type="text/javascript">
+ var djConfig= {
+ isDebug: false,
+ parseOnLoad: true,
+ AutoIDL: ['aou','aout','pgt','au','cbreb']
+ }
+ </script>
+
+ <script src='/js/dojo/dojo/dojo.js'></script>
+ <!-- <script src="/js/dojo/dojo/openils_dojo.js"></script> -->
+
+ <script type="text/javascript">
+
+ dojo.require('fieldmapper.AutoIDL');
+ dojo.require('fieldmapper.dojoData');
+ dojo.require('openils.User');
+ dojo.require('openils.CGI');
+ dojo.require('openils.widget.PogressDialog');
+
+ var cgi = new openils.CGI();
+ var u = new openils.User({ authcookie : 'ses' });
+
+ var rec_count = $rec_string;
+ var cache_key = '$cache_key';
+
+ dojo.addOnLoad(function () {
+ progress_dialog.show(true);
+ progress_dialog.update({maximum:rec_ids.length});
+
+ setInterval( function() {
+ fieldmapper.standardRequest(
+ ['open-ils.actor','open-ils.actor.anon_cache.get_value'],
+ { async : false,
+ params: [ cache_key ],
+ onresponse : function (r) {
+ var counter = { success : 0, fail : 0, total : 0 };
+ dojo.forEach( openils.Util.readResponse(r), function(x) {
+ if (x.complete) {
+ progress_dialog.hide();
+ if (x.success == 'f') dojo.byId('complete_msg').innerHTML = 'Overlay completed successfully';
+ else dojo.byId('complete_msg').innerHTML = 'Overlay did not complet successfully';
+ } else {
+ counter.total++;
+ switch (x.success) {
+ case 't':
+ counter.success++;
+ break;
+ default:
+ counter.fail++;
+ break;
+ }
+ }
+ });
+
+ // update the progress dialog
+ progress_dialog.update({progress:counter.total});
+ dojo.byId('success_count').innerHTML = counter.success;
+ dojo.byId('fail_count').innerHTML = counter.fail;
+ dojo.byId('total_count').innerHTML = counter.total;
+ }
+ }
+ );
+ }, 1000);
+
+ });
+ </script>
+ </head>
+
+ <body class='tundra'>
+ <div class="hide_me"><div dojoType="openils.widget.ProgressDialog" jsId="progress_dialog"></div></div>
+
+ <table style="width:100%; margin-top:100px;">
+ <th>
+ <td>Status</td>
+ <td>Record Count</td>
+ </th>
+ <tr>
+ <td>Success</td>
+ <td id='success_count'></td>
+ </tr>
+ <tr>
+ <td>Failure</td>
+ <td id='fail_count'></td>
+ </tr>
+ <tr>
+ <td></td>
+ <td id='total_count'></td>
+ </tr>
+ </table>
+
+ <div id='complete_msg'></div>
+
+ </body>
+</html>
+HTML
+
+ return Apache2::Const::OK;
+}
+
+
+sub show_template {
+ my $r = shift;
+
+ $r->content_type('text/html');
+ $r->print(<<'HTML');
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+ <head>
+ <title>Merge Template Builder</title>
+ <style type="text/css">
+ @import '/js/dojo/dojo/resources/dojo.css';
+ @import '/js/dojo/dijit/themes/tundra/tundra.css';
+ .hide_me { display: none; visibility: hidden; }
+ th { font-weight: bold; }
+ </style>
+
+ <script type="text/javascript">
+ var djConfig= {
+ isDebug: false,
+ parseOnLoad: true,
+ AutoIDL: ['aou','aout','pgt','au','cbreb']
+ }
+ </script>
+
+ <script src='/js/dojo/dojo/dojo.js'></script>
+ <!-- <script src="/js/dojo/dojo/openils_dojo.js"></script> -->
+
+ <script type="text/javascript">
+
+ dojo.require('dojo.data.ItemFileReadStore');
+ dojo.require('dijit.form.Form');
+ dojo.require('dijit.form.NumberSpinner');
+ dojo.require('dijit.form.FilteringSelect');
+ dojo.require('dijit.form.TextBox');
+ dojo.require('dijit.form.Textarea');
+ dojo.require('dijit.form.Button');
+ dojo.require('MARC.Batch');
+ dojo.require('fieldmapper.AutoIDL');
+ dojo.require('fieldmapper.dojoData');
+ dojo.require('openils.User');
+ dojo.require('openils.CGI');
+
+ var cgi = new openils.CGI();
+ var u = new openils.User({ authcookie : 'ses' });
+
+ var bucketStore = new dojo.data.ItemFileReadStore(
+ { data : cbreb.toStoreData(
+ fieldmapper.standardRequest(
+ ['open-ils.actor','open-ils.actor.container.retrieve_by_class'],
+ [u.authtoken, u.user.id(), 'biblio', 'staff_client']
+ )
+ )
+ }
+ );
+
+ function render_preview () {
+ var rec = ruleset_to_record();
+ dojo.byId('marcPreview').innerHTML = rec.toBreaker();
+ }
+
+ function render_from_template () {
+ var kid_number = dojo.byId('ruleList').childNodes.length;
+ var clone = dojo.query('*[name=ruleTable]', dojo.byId('ruleTemplate'))[0].cloneNode(true);
+
+ var typeSelect = dojo.query('*[name=typeSelect]',clone).instantiate(dijit.form.FilteringSelect, {
+ onChange : function (val) {
+ switch (val) {
+ case 'a':
+ case 'r':
+ dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',clone)[0]).attr('disabled',false);
+ break;
+ default :
+ dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',clone)[0]).attr('disabled',true);
+ };
+ render_preview();
+ }
+ })[0];
+
+ var marcData = dojo.query('*[name=marcData]',clone).instantiate(dijit.form.TextBox, {
+ onChange : render_preview
+ })[0];
+
+
+ var tag = dojo.query('*[name=tag]',clone).instantiate(dijit.form.TextBox, {
+ onChange : function (newtag) {
+ var md = dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',clone)[0]);
+ var current_marc = md.attr('value');
+
+ if (newtag.length == 3) {
+ if (current_marc.length == 0) newtag += ' \\\\';
+ if (current_marc.substr(0,3) != newtag) current_marc = newtag + current_marc.substr(3);
+ }
+ md.attr('value', current_marc);
+ render_preview();
+ }
+ })[0];
+
+ var sf = dojo.query('*[name=sf]',clone).instantiate(dijit.form.TextBox, {
+ onChange : function (newsf) {
+ var md = dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',clone)[0]);
+ var current_marc = md.attr('value');
+ var sf_list = newsf.split('');
+
+ for (var i in sf_list) {
+ var re = '\\$' + sf_list[i];
+ if (current_marc.match(re)) continue;
+ current_marc += '$' + sf_list[i];
+ }
+
+ md.attr('value', current_marc);
+ render_preview();
+ }
+ })[0];
+
+ var matchSF = dojo.query('*[name=matchSF]',clone).instantiate(dijit.form.TextBox, {
+ onChange : render_preview
+ })[0];
+
+ var matchRE = dojo.query('*[name=matchRE]',clone).instantiate(dijit.form.TextBox, {
+ onChange : render_preview
+ })[0];
+
+ var removeButton = dojo.query('*[name=removeButton]',clone).instantiate(dijit.form.Button, {
+ onClick : function() {
+ dojo.addClass(
+ dojo.byId('ruleList').childNodes[kid_number],
+ 'hide_me'
+ );
+ render_preview();
+ }
+ })[0];
+
+ dojo.place(clone,'ruleList');
+ }
+
+ function ruleset_to_record () {
+ var rec = new MARC.Record ({ delimiter : '$' });
+
+ dojo.forEach(
+ dojo.query('#ruleList *[name=ruleTable]').filter( function (node) {
+ if (node.className.match(/hide_me/)) return false;
+ return true;
+ }),
+ function (tbl) {
+ var rule_tag = new MARC.Field ({
+ tag : '905',
+ ind1 : ' ',
+ ind2 : ' '
+ });
+ var rule_txt = dijit.byNode(dojo.query('*[name=tagContainer] .dijit',tbl)[0]).attr('value');
+ rule_txt += dijit.byNode(dojo.query('*[name=sfContainer] .dijit',tbl)[0]).attr('value');
+
+ var reSF = dijit.byNode(dojo.query('*[name=matchSFContainer] .dijit',tbl)[0]).attr('value');
+ if (reSF) {
+ var reRE = dijit.byNode(dojo.query('*[name=matchREContainer] .dijit',tbl)[0]).attr('value');
+ rule_txt += '[' + reSF + '~' + reRE + ']';
+ }
+
+ var rtype = dijit.byNode(dojo.query('*[name=typeSelectContainer] .dijit',tbl)[0]).attr('value');
+ rule_tag.addSubfields( rtype, rule_txt )
+ rec.appendFields( rule_tag );
+
+ if (rtype == 'a' || rtype == 'r') {
+ rec.appendFields(
+ new MARC.Record ({
+ delimiter : '$',
+ marcbreaker : dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',tbl)[0]).attr('value')
+ }).fields[0]
+ );
+ }
+ }
+ );
+
+ return rec;
+ }
+ </script>
+ </head>
+
+ <body class='tundra'>
+
+ <div dojoType="dijit.form.Form" id="myForm" jsId="myForm" encType="multipart/form-data" action="" method="POST">
+ <script type='dojo/method' event='onSubmit'>
+ var rec = ruleset_to_record();
+ dojo.byId('template_value').value = rec.toXmlString();
+ return true;
+ </script>
+
+ <input type='hidden' id='template_value' name='template'/>
+
+ <div>
+ <label for='bucketList'>Batch update records in Bucket:</label>
+ <div name='containerid' jsId='bucketList' dojoType='dijit.form.FilteringSelect' store='bucketStore' searchAttr='name' id='bucketList'>
+ <script type='dojo/method' event='postCreate'>
+ if (cgi.param('containerid')) this.attr('value',cgi.param('containerid'));
+ </script>
+ </div>
+ </div>
+
+ <div style='text-align: center; width: 50%;'>or</div>
+
+ <div>
+ <label for='idfile'>Batch update records from column <input style='width:75px;' type='text' dojoType='dijit.form.NumberSpinner' name='idcolumn' value='0' constraints='{min:0,max:100,places:0}' /> (starting from 0) in CSV file:</label>
+ <input id='idfile' type="file" name="idfile"/>
+ </div>
+
+ <div style='text-align: center; width: 50%;'>or</div>
+
+ <label for='recid'>Test Ruleset by applying to one record:</label> <input name='recid' style='width:75px;' type='text' dojoType='dijit.form.NumberTextBox' name='id' value='' constraints='{min:0}' />
+
+ <br/>
+ <br/>
+
+ <button type="submit" dojoType='dijit.form.Button'>Apply Ruleset</button>
+
+ </div> <!-- end of the form -->
+
+ <hr/>
+ <table style='width: 100%'>
+ <tr>
+ <td style='width: 50%'><div id='ruleList'></div></td>
+ <td valign='top'>Update Template Preview:<br/><pre id='marcPreview'></pre></td>
+ </tr>
+ </table>
+
+ <button dojoType='dijit.form.Button'>Add Merge Rule
+ <script type='dojo/connect' event='onClick'>render_from_template()</script>
+ <script type='dojo/method' event='postCreate'>render_from_template()</script>
+ </button>
+
+ <div class='hide_me' id='ruleTemplate'>
+ <div name='ruleTable'>
+ <table>
+ <tbody>
+ <tr>
+ <th>Rule Type</th>
+ <td name='typeSelectContainer'>
+ <select name='typeSelect'>
+ <option value='a'>Add</option>
+ <option value='d'>Delete</option>
+ <option value='r'>Replace</option>
+ <option value='p'>Preserve</option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <th>MARC Tag</th>
+ <td name='tagContainer'><input style='with: 2em;' name='tag' type='text'></input</td>
+ </td>
+ <tr>
+ <th>Optional Subfields</th>
+ <td name='sfContainer'><input name='sf' type='text'/></td>
+ </tr>
+ <tr>
+ <th>MARC Data</th>
+ <td name='marcDataContainer'><input name='marcData' type='text'/></td>
+ </tr>
+ <tr>
+ <th colspan='2' style='padding-top: 10px; text-align: center;'>Optionally Restrict By</th>
+ </tr>
+ <tr>
+ <th>Subfield</th>
+ <td name='matchSFContainer'><input style='with: 2em;' name='matchSF' type='text'></input</td>
+ </td>
+ <tr>
+ <th>Regular Expression</th>
+ <td name='matchREContainer'><input name='matchRE' type='text'/></td>
+ </tr>
+ <tr>
+ <td></td>
+ <td>
+ <button name='removeButton'>Remove Merge Rule
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <hr/>
+ </div>
+ </div>
+
+ </body>
+</html>
+HTML
+
+ return Apache2::Const::OK;
+}
+
+1;
+
+