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