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