]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/TemplateBatchBibUpdate.pm
5bd561a3ef6002ef656d06dcbe577984565e457d
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / TemplateBatchBibUpdate.pm
1 package OpenILS::WWW::TemplateBatchBibUpdate;
2 use strict;
3 use warnings;
4 use bytes;
5
6 use Apache2::Log;
7 use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND HTTP_BAD_REQUEST :log);
8 use APR::Const    -compile => qw(:error SUCCESS);
9 use APR::Table;
10
11 use Apache2::RequestRec ();
12 use Apache2::RequestIO ();
13 use Apache2::RequestUtil;
14 use CGI;
15 use Data::Dumper;
16 use Text::CSV;
17
18 use OpenSRF::EX qw(:try);
19 use OpenILS::Utils::DateTime qw/:datetime/;
20 use OpenSRF::Utils::Cache;
21 use OpenSRF::System;
22 use OpenSRF::AppSession;
23 use XML::LibXML;
24 use XML::LibXSLT;
25
26 use Encode;
27 use Unicode::Normalize;
28 use OpenILS::Utils::Fieldmapper;
29 use OpenSRF::Utils::Logger qw/$logger/;
30
31 use MARC::Record;
32 use MARC::File::XML ( BinaryEncoding => 'UTF-8' );
33
34 use UNIVERSAL::require;
35
36 our @formats = qw/USMARC UNIMARC XML BRE/;
37
38 # set the bootstrap config and template include directory when
39 # this module is loaded
40 my $bootstrap;
41
42 sub import {
43     my $self = shift;
44     $bootstrap = shift;
45 }
46
47
48 sub child_init {
49     OpenSRF::System->bootstrap_client( config_file => $bootstrap );
50     Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
51     return Apache2::Const::OK;
52 }
53
54 sub handler {
55     my $r = shift;
56     my $cgi = new CGI;
57
58     my $authid = $cgi->cookie('eg.auth.token') || $cgi->cookie('ses') || $cgi->param('ses');
59     if ($authid =~ /^"(.+)"$/) { # came from eg2 login, is json encoded
60         $authid = $1;
61     }
62
63     # Avoid sending the HTML to the caller.  Final response will
64     # will just be the cache key or HTTP_BAD_REQUEST on error.
65     my $skipui = $cgi->param('skipui');
66
67     my $usr = verify_login($authid);
68     return show_template($r, $skipui) unless ($usr);
69
70     my $template = $cgi->param('template');
71     return show_template($r, $skipui) unless ($template);
72
73
74     my $rsource = $cgi->param('recordSource');
75     my $xact_per = $cgi->param('xactPerRecord');
76
77     # find some IDs ...
78     my @records;
79
80     if ($rsource eq 'r') {
81         @records = map { $_ ? ($_) : () } $cgi->param('recid');
82     }
83
84     if ($rsource eq 'c') { # try for a file
85         my $file = $cgi->param('idfile');
86         if ($file) {
87             my $col = $cgi->param('idcolumn') || 0;
88             my $csv = new Text::CSV;
89
90             while (<$file>) {
91                 $csv->parse($_);
92                 my @data = $csv->fields;
93                 my $id = $data[$col];
94                 $id =~ s/\D+//o;
95                 next unless ($id);
96                 push @records, $id;
97             }
98         }
99     }
100
101     my $e = OpenSRF::AppSession->connect('open-ils.cstore');
102     $e->request('open-ils.cstore.transaction.begin')->gather(1);
103     $e->request('open-ils.cstore.set_audit_info', $authid, $usr->id, $usr->wsid)->gather(1);
104
105     # still no records ...
106     my $container = $cgi->param('containerid');
107     if ($rsource eq 'b') {
108         if ($container) {
109             my $bucket = $e->request(
110                 'open-ils.cstore.direct.container.biblio_record_entry_bucket.retrieve',
111                 $container
112             )->gather(1);
113             unless($bucket) {
114                 $e->request('open-ils.cstore.transaction.rollback')->gather(1);
115                 $e->disconnect;
116                 $r->log->error("No such bucket $container");
117                 $logger->error("No such bucket $container");
118                 return Apache2::Const::NOT_FOUND;
119             }
120             my $recs = $e->request(
121                 'open-ils.cstore.direct.container.biblio_record_entry_bucket_item.search.atomic',
122                 { bucket => $container }
123             )->gather(1);
124             @records = map { ($_->target_biblio_record_entry) } @$recs;
125         }
126     }
127
128     unless (@records) {
129         $e->request('open-ils.cstore.transaction.rollback')->gather(1);
130         $e->disconnect;
131         return show_template($r, $skipui);
132     }
133
134     # we have a template and some record ids, so...
135
136     # insert the template record
137     my $min_id = $e->request(
138         'open-ils.cstore.json_query',
139         { select => { bre => [{ column => 'id', transform => 'min', aggregate => 1}] }, from => 'bre' }
140     )->gather(1)->{id} - 1;
141
142     warn "new template bib id = $min_id\n";
143
144     my $tmpl_rec = Fieldmapper::biblio::record_entry->new;
145     $tmpl_rec->id($min_id);
146     $tmpl_rec->deleted('t');
147     $tmpl_rec->active('f');
148     $tmpl_rec->marc($template);
149     $tmpl_rec->creator($usr->id);
150     $tmpl_rec->editor($usr->id);
151
152     warn "about to create bib $min_id\n";
153     $e->request('open-ils.cstore.direct.biblio.record_entry.create', $tmpl_rec )->gather(1);
154
155     # create the new container for the records and the template
156     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
157     $bucket->owner($usr->id);
158     $bucket->btype('template_merge');
159
160     my $bname = $cgi->param('bname') || 'Temporary Merge Bucket '. localtime() . ' ' . $usr->id;
161     $bucket->name($bname);
162
163     $bucket = $e->request('open-ils.cstore.direct.container.biblio_record_entry_bucket.create', $bucket )->gather(1);
164
165     # create items in the bucket
166     my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
167     $item->bucket($bucket->id);
168     $item->target_biblio_record_entry($min_id);
169
170     $e->request('open-ils.cstore.direct.container.biblio_record_entry_bucket_item.create', $item )->gather(1);
171
172     my %seen;
173     for my $r (@records) {
174         next if ($seen{$r});
175         $item->target_biblio_record_entry($r);
176         $e->request('open-ils.cstore.direct.container.biblio_record_entry_bucket_item.create', $item )->gather(1);
177         $seen{$r}++;
178     }
179
180     $e->request('open-ils.cstore.transaction.commit')->gather(1);
181     $e->disconnect;
182
183     # fire the background bucket processor
184     my $cache_key = OpenSRF::AppSession
185         ->create('open-ils.cat')
186         ->request('open-ils.cat.container.template_overlay.background', 
187             $authid, $bucket->id, undef, {xact_per_record => $xact_per})
188         ->gather(1);
189
190     return show_processing_template(
191       $r, $bucket->id, \@records, $cache_key, $skipui);
192 }
193
194 sub verify_login {
195         my $auth_token = shift;
196         return undef unless $auth_token;
197
198         my $user = OpenSRF::AppSession
199                 ->create("open-ils.auth")
200                 ->request( "open-ils.auth.session.retrieve", $auth_token )
201                 ->gather(1);
202
203         if (ref($user) eq 'HASH' && $user->{ilsevent} == 1001) {
204                 return undef;
205         }
206
207         return $user if ref($user);
208         return undef;
209 }
210
211 sub show_processing_template {
212     my $r = shift;
213     my $bid = shift;
214     my $recs = shift;
215     my $cache_key = shift;
216     my $skipui = shift;
217
218     if ($skipui) {
219         $r->content_type('text/plain');
220         $r->print($cache_key);
221         return Apache2::Const::OK;
222     }
223
224     my $rec_string = @$recs;
225
226     $r->content_type('text/html');
227     $r->print(<<HTML);
228 <html xmlns="http://www.w3.org/1999/xhtml">
229
230     <head>
231         <title>Merging records...</title>
232         <style type="text/css">
233             \@import '/js/dojo/dojo/resources/dojo.css';
234             \@import '/js/dojo/dijit/themes/tundra/tundra.css';
235             .hide_me { display: none; visibility: hidden; }
236             th       { font-weight: bold; }
237         </style>
238
239         <script type="text/javascript">
240             var djConfig= {
241                 isDebug: false,
242                 parseOnLoad: true,
243                 AutoIDL: ['aou','aout','pgt','au','cbreb']
244             }
245         </script>
246
247         <script src='/js/dojo/dojo/dojo.js'></script>
248         <!-- <script src="/js/dojo/dojo/openils_dojo.js"></script> -->
249
250         <script type="text/javascript">
251
252             dojo.require('fieldmapper.AutoIDL');
253             dojo.require('fieldmapper.dojoData');
254             dojo.require('openils.User');
255             dojo.require('openils.XUL');
256             dojo.require('dojo.cookie');
257             dojo.require('openils.CGI');
258             dojo.require('openils.widget.ProgressDialog');
259
260             var cgi = new openils.CGI();
261             var authtoken = dojo.cookie('ses') || cgi.param('ses');
262             if (!authtoken && openils.XUL.isXUL()) {
263                 var stash = openils.XUL.getStash();
264                 authtoken = stash.session.key;
265             }
266             var u = new openils.User({ authtoken: authtoken });
267
268             dojo.addOnLoad(function () {
269                 
270                 progress_dialog.update({maximum: $rec_string});
271                 progress_dialog.attr("title", "MARC Batch Editor Progress......");
272                 progress_dialog.show();
273
274                 var interval;
275                 interval = setInterval( function() {
276                     fieldmapper.standardRequest(
277                         ['open-ils.actor','open-ils.actor.anon_cache.get_value'],
278                         { async : false,
279                           params: [ u.authtoken, 'batch_edit_progress' ],
280                           onerror : function (r) { progress_dialog.hide(); },
281                           onresponse : function (r) {
282                             var counter = { success : 0, fail : 0, total : 0 };
283                             if (x = openils.Util.readResponse(r)) {
284                                 counter.success = x.succeeded;
285                                 counter.fail    = x.failed;
286                                 counter.total = counter.success + counter.fail;
287                                 if (x.complete) {
288                                     clearInterval(interval);
289                                     progress_dialog.hide();
290                                     if (x.success == 't') dojo.byId('complete_msg').innerHTML = 'Overlay completed successfully';
291                                     else dojo.byId('complete_msg').innerHTML = 'Overlay did not complet successfully';
292                                 }
293                             };
294
295                             // update the progress dialog
296                             progress_dialog.update({progress:counter.total});
297                             dojo.byId('success_count').innerHTML = counter.success;
298                             dojo.byId('fail_count').innerHTML = counter.fail;
299                             dojo.byId('total_count').innerHTML = counter.total;
300                           }
301                         }
302                     );
303                 }, 1000);
304
305             });
306         </script>
307 <style>
308 table {
309     #width:100%;
310 }
311 table, th, td {
312     border: 1px solid black;
313     border-collapse: collapse;
314 }
315 th, td {
316     padding: 5px;
317     text-align: left;
318 }
319 table tr:nth-child(even) {
320     background-color: #eee;
321 }
322 table tr:nth-child(odd) {
323    background-color:#fff;
324 }
325 table th        {
326     background-color: black;
327     color: white;
328 }
329 tr#fail {
330     color: red;
331 }
332 tr#processed    {
333     font-weight: bold;
334 }
335 div#complete_msg {
336     font-weight:bold;
337     color: green;
338     font-size: larger;
339     text-decoration: underline;
340 }
341 </style>
342     </head>
343     
344
345     <body style="margin:10px;font-size: 130%" class='tundra'>
346         <div class="hide_me"><div dojoType="openils.widget.ProgressDialog" jsId="progress_dialog"></div></div>
347
348         <h1>MARC Batch Editor Status</h1>
349
350         <table>
351             <tr>
352                 <th>Status</th>
353                 <th>Record Count</th>
354             </tr>
355             <tr>
356                 <td>Success</td>
357                 <td id='success_count'></td>
358             </tr>
359             <tr id='fail'>
360                 <td>Failure</td>
361                 <td id='fail_count'></td>
362             </tr>
363             <tr id='processed' >
364                 <td>Total Processed</td>
365                 <td id='total_count'></td>
366             </tr>
367             <tr>
368                <td></td>
369             </tr>
370             <tr>
371                 <td>Total To Process</td>
372                 <td>$rec_string</td>
373             </tr>
374         </table>
375         <br>
376         <div id='complete_msg'></div>
377
378     </body>
379 </html>
380 HTML
381
382     return Apache2::Const::OK;
383 }
384
385
386 sub show_template {
387     my $r = shift;
388     my $skipui = shift;
389
390     # Makes no sense to call the API in such a way that the caller
391     # is returned the UI code if skipui is set.
392     return Apache2::Const::HTTP_BAD_REQUEST if $skipui;
393
394     $r->content_type('text/html');
395     $r->print(<<'HTML');
396 <html xmlns="http://www.w3.org/1999/xhtml">
397
398     <head>
399         <title>Merge Template Builder</title>
400         <style type="text/css">
401             @import '/js/dojo/dojo/resources/dojo.css';
402             @import '/js/dojo/dijit/themes/tundra/tundra.css';
403             .hide_me { display: none; visibility: hidden; }
404             table.ruleTable th { padding: 5px; border-collapse: collapse; border-bottom: solid 1px gray; font-weight: bold; }
405             table.ruleTable td { padding: 5px; border-collapse: collapse; border-bottom: solid 1px gray; }
406         </style>
407
408         <script type="text/javascript">
409             var djConfig= {
410                 isDebug: false,
411                 parseOnLoad: true,
412                 AutoIDL: ['aou','aout','pgt','au','cbreb']
413             }
414         </script>
415
416         <script src='/js/dojo/dojo/dojo.js'></script>
417         <!-- <script src="/js/dojo/dojo/openils_dojo.js"></script> -->
418
419         <script type="text/javascript">
420
421             dojo.require('dojo.data.ItemFileReadStore');
422             dojo.require('dijit.form.Form');
423             dojo.require('dijit.form.NumberSpinner');
424             dojo.require('dijit.form.FilteringSelect');
425             dojo.require('dijit.form.TextBox');
426             dojo.require('dijit.form.Textarea');
427             dojo.require('dijit.form.Button');
428             dojo.require('MARC.Batch');
429             dojo.require('fieldmapper.AutoIDL');
430             dojo.require('fieldmapper.dojoData');
431             dojo.require('openils.User');
432             dojo.require('openils.CGI');
433             dojo.require('openils.XUL');
434             dojo.require('dojo.cookie');
435
436             var cgi = new openils.CGI();
437             var authtoken = dojo.cookie('ses') || cgi.param('ses');
438             if (!authtoken && openils.XUL.isXUL()) {
439                 var stash = openils.XUL.getStash();
440                 authtoken = stash.session.key;
441             }
442             var u = new openils.User({ authtoken: authtoken });
443
444             var bucketStore = new dojo.data.ItemFileReadStore(
445                 { data : cbreb.toStoreData(
446                         fieldmapper.standardRequest(
447                             ['open-ils.actor','open-ils.actor.container.retrieve_by_class.authoritative'],
448                             [u.authtoken, u.user.id(), 'biblio', ['staff_client','vandelay_queue']]
449                         )
450                     )
451                 }
452             );
453
454             function render_preview () {
455                 var rec = ruleset_to_record();
456                 dojo.byId('marcPreview').innerHTML = rec.toBreaker();
457             }
458
459             function render_from_template () {
460                 var kid_number = dojo.byId('ruleList').childNodes.length;
461                 var clone = dojo.query('*[name=ruleTable]', dojo.byId('ruleTemplate'))[0].cloneNode(true);
462
463                 var typeSelect = dojo.query('*[name=typeSelect]',clone).instantiate(dijit.form.FilteringSelect, {
464                     onChange : function (val) {
465                         switch (val) {
466                             case 'a':
467                             case 'r':
468                                 dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',clone)[0]).attr('disabled',false);
469                                 break;
470                             default :
471                                 dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',clone)[0]).attr('disabled',true);
472                         };
473                         render_preview();
474                     }
475                 })[0];
476
477                 var marcData = dojo.query('*[name=marcData]',clone).instantiate(dijit.form.TextBox, {
478                     onChange : render_preview
479                 })[0];
480
481
482                 var tag = dojo.query('*[name=tag]',clone).instantiate(dijit.form.TextBox, {
483                     onChange : function (newtag) {
484                         var md = dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',clone)[0]);
485                         var current_marc = md.attr('value');
486
487                         if (newtag.length == 3) {
488                             if (current_marc.length == 0) newtag += ' \\\\';
489                             if (current_marc.substr(0,3) != newtag) current_marc = newtag + current_marc.substr(3);
490                         }
491                         md.attr('value', current_marc);
492                         render_preview();
493                     }
494                 })[0];
495
496                 var sf = dojo.query('*[name=sf]',clone).instantiate(dijit.form.TextBox, {
497                     onChange : function (newsf) {
498                         var md = dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',clone)[0]);
499                         var current_marc = md.attr('value');
500                         var sf_list = newsf.split('');
501
502                         for (var i in sf_list) {
503                             var re = '\\$' + sf_list[i];
504                             if (current_marc.match(re)) continue;
505                             current_marc += '$' + sf_list[i];
506                         }
507
508                         md.attr('value', current_marc);
509                         render_preview();
510                     }
511                 })[0];
512
513                 var matchSF = dojo.query('*[name=matchSF]',clone).instantiate(dijit.form.TextBox, {
514                     onChange : render_preview
515                 })[0];
516
517                 var matchRE = dojo.query('*[name=matchRE]',clone).instantiate(dijit.form.TextBox, {
518                     onChange : render_preview
519                 })[0];
520
521                 var removeButton = dojo.query('*[name=removeButton]',clone).instantiate(dijit.form.Button, {
522                     onClick : function() {
523                         dojo.addClass(
524                             dojo.byId('ruleList').childNodes[kid_number],
525                             'hide_me'
526                         );
527                         render_preview();
528                     }
529                 })[0];
530
531                 dojo.place(clone,'ruleList');
532             }
533
534             function ruleset_to_record () {
535                 var rec = new MARC.Record ({ delimiter : '$' });
536
537                 dojo.forEach( 
538                     dojo.query('#ruleList *[name=ruleTable]').filter( function (node) {
539                         if (node.className.match(/hide_me/)) return false;
540                         return true;
541                     }),
542                     function (tbl) {
543                         var rule_tag = new MARC.Field ({
544                             tag : '905',
545                             ind1 : ' ',
546                             ind2 : ' '
547                         });
548                         var rule_txt = dijit.byNode(dojo.query('*[name=tagContainer] .dijit',tbl)[0]).attr('value');
549                         rule_txt += dijit.byNode(dojo.query('*[name=sfContainer] .dijit',tbl)[0]).attr('value');
550
551                         var reSF = dijit.byNode(dojo.query('*[name=matchSFContainer] .dijit',tbl)[0]).attr('value');
552                         if (reSF) {
553                             var reRE = dijit.byNode(dojo.query('*[name=matchREContainer] .dijit',tbl)[0]).attr('value');
554                             rule_txt += '[' + reSF + '~' + reRE + ']';
555                         }
556
557                         var rtype = dijit.byNode(dojo.query('*[name=typeSelectContainer] .dijit',tbl)[0]).attr('value');
558                         rule_tag.addSubfields( rtype, rule_txt )
559                         rec.appendFields( rule_tag );
560
561                         if (rtype == 'a' || rtype == 'r') {
562                             rec.appendFields(
563                                 new MARC.Record ({
564                                     delimiter : '$',
565                                     marcbreaker : dijit.byNode(dojo.query('*[name=marcDataContainer] .dijit',tbl)[0]).attr('value')
566                                 }).fields[0]
567                             );
568                         }
569                     }
570                 );
571
572                 return rec;
573             }
574         </script>
575     </head>
576
577     <body style="margin:10px;" class='tundra'>
578
579         <div dojoType="dijit.form.Form" id="myForm" jsId="myForm" encType="multipart/form-data" action="" method="POST">
580                 <script type='dojo/method' event='onSubmit'>
581                     var rec = ruleset_to_record();
582
583                     if (rec.subfield('905','r') == '') { // no-op to force replace mode
584                         rec.appendFields(
585                             new MARC.Field ({
586                                 tag : '905',
587                                 ind1 : ' ',
588                                 ind2 : ' ',
589                                 subfields : [['r','901c']]
590                             })
591                         );
592                     }
593
594                     dojo.byId('template_value').value = rec.toXmlString();
595                     return true;
596                 </script>
597
598             <input type='hidden' id='template_value' name='template'/>
599
600             <label for='inputTypeSelect'>Record source:</label>
601             <select name='recordSource' dojoType='dijit.form.FilteringSelect'>
602                 <script type='dojo/method' event='onChange' args="val">
603                     switch (val) {
604                         case 'b':
605                             dojo.removeClass('bucketListContainer','hide_me');
606                             dojo.addClass('csvContainer','hide_me');
607                             dojo.addClass('recordContainer','hide_me');
608                             break;
609                         case 'c':
610                             dojo.addClass('bucketListContainer','hide_me');
611                             dojo.removeClass('csvContainer','hide_me');
612                             dojo.addClass('recordContainer','hide_me');
613                             break;
614                         case 'r':
615                             dojo.addClass('bucketListContainer','hide_me');
616                             dojo.addClass('csvContainer','hide_me');
617                             dojo.removeClass('recordContainer','hide_me');
618                             break;
619                     };
620                 </script>
621                 <script type='dojo/method' event='postCreate'>
622                     if (cgi.param('recordSource')) {
623                         this.attr('value',cgi.param('recordSource'));
624                         this.onChange(cgi.param('recordSource'));
625                     }
626                 </script>
627                 <option value='b'>a Bucket</option>
628                 <option value='c'>a CSV File</option>
629                 <option value='r'>a specific record ID</option>
630             </select>
631
632             <table style='margin:10px; margin-bottom:20px;'>
633 <!--
634                 <tr>
635                     <th>Merge template name (optional):</th>
636                     <td><input id='bucketName' jsId='bucketName' type='text' dojoType='dijit.form.TextBox' name='bname' value=''/></td>
637                 </tr>
638 -->
639                 <tr class='' id='bucketListContainer'>
640                     <td>Bucket named: 
641                         <div name='containerid' jsId='bucketList' dojoType='dijit.form.FilteringSelect' store='bucketStore' searchAttr='name' id='bucketList'>
642                             <script type='dojo/method' event='postCreate'>
643                                 if (cgi.param('containerid')) this.attr('value',cgi.param('containerid'));
644                             </script>
645                         </div>
646                     </td>
647                 </tr>
648                 <tr class='hide_me' id='csvContainer'>
649                     <td>
650                         Column <input style='width:75px;' type='text' dojoType='dijit.form.NumberSpinner' name='idcolumn' value='0' constraints='{min:0,max:100,places:0}' /> of: 
651                         <input id='idfile' type="file" name="idfile"/>
652                         <br/>
653                         <br/>
654                         Columns are numbered starting at 0.  For instance, when looking at a CSV file in Excel, the column labeled A is the same as column 0, and the column labeled B is the same as column 1.
655                     </td>
656                 </tr>
657                 <tr class='hide_me' id='recordContainer'>
658                     <td>Record ID: <input dojoType='dijit.form.TextBox' name='recid' style='width:75px;' type='text' value=''/></td>
659                 </tr>
660             </table>
661
662             <button type="submit" dojoType='dijit.form.Button'>GO!</button> (After setting up your template below.)
663
664             <br/>
665             <br/>
666
667         </div> <!-- end of the form -->
668
669         <hr/>
670         <table style='width: 100%'>
671             <tr>
672                 <td style='width: 50%'><div id='ruleList'></div></td>
673                 <td valign='top'>Update Template Preview:<br/><pre id='marcPreview'></pre></td>
674             </tr>
675         </table>
676
677         <button dojoType='dijit.form.Button'>Add Merge Rule
678             <script type='dojo/connect' event='onClick'>render_from_template()</script>
679             <script type='dojo/method' event='postCreate'>render_from_template()</script>
680         </button>
681
682         <div class='hide_me' id='ruleTemplate'>
683         <div name='ruleTable'>
684             <table class='ruleTable'>
685                 <tbody>
686                     <tr>
687                         <th style="text-align:center;">Rule Setup</th>
688                         <th style="text-align:center;">Data</th>
689                         <th style="text-align:center;">Help</th>
690                     </tr>
691                     <tr>
692                         <th>Action (Rule Type)</th>
693                         <td name='typeSelectContainer'>
694                             <select name='typeSelect'>
695                                 <option value='r'>Replace</option>
696                                 <option value='a'>Add</option>
697                                 <option value='d'>Delete</option>
698                             </select>
699                         </td>
700                         <td>How to change the existing records</td>
701                     </tr>
702                     <tr>
703                         <th>MARC Tag</th>
704                         <td name='tagContainer'><input style='with: 2em;' name='tag' type='text'></input</td>
705                         <td>Three characters, no spaces, no indicators, etc. eg: 245</td>
706                     </td>
707                     <tr>
708                         <th>Subfields (optional)</th>
709                         <td name='sfContainer'><input name='sf' type='text'/></td>
710                         <td>No spaces, no delimiters, eg: abcnp</td>
711                     </tr>
712                     <tr>
713                         <th>MARC Data</th>
714                         <td name='marcDataContainer'><input name='marcData' type='text'/></td>
715                         <td>MARC-Breaker formatted data with indicators and subfield delimiters, eg:<br/>245 04$aThe End</td>
716                     </tr>
717                     <tr>
718                         <th colspan='3' style='padding-top: 20px; text-align: center;'>Advanced Matching Restriction (Optional)</th>
719                     </tr>
720                     <tr>
721                         <th>Subfield</th>
722                         <td name='matchSFContainer'><input style='with: 2em;' name='matchSF' type='text'></input</td>
723                         <td>A single subfield code, no delimiters, eg: a</td>
724                     <tr>
725                         <th>Regular Expression</th>
726                         <td name='matchREContainer'><input name='matchRE' type='text'/></td>
727                         <td>See <a href="http://perldoc.perl.org/perlre.html#Regular-Expressions" target="_blank">the Perl documentation</a>
728                             for an explanation of Regular Expressions.
729                         </td>
730                     </tr>
731                     <tr>
732                         <td colspan='3' style='padding-top: 20px; text-align: center;'>
733                             <button name='removeButton'>Remove this Template Rule</button>
734                         </td>
735                     </tr>
736                 </tbody>
737             </table>
738         <hr/>
739         </div>
740         </div>
741
742     </body>
743 </html>
744 HTML
745
746     return Apache2::Const::OK;
747 }
748
749 1;
750
751