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