proof of concept: MARC record checks for bib records user/gmcharlt/marc-lint
authorGalen Charlton <gmc@equinoxinitiative.org>
Mon, 8 Mar 2021 15:21:13 +0000 (10:21 -0500)
committerGalen Charlton <gmc@equinoxinitiative.org>
Tue, 9 Mar 2021 16:24:05 +0000 (11:24 -0500)
This adds a new method to pass a MARC bib record through
MARC::Lint and MARC::Errorchecks and return a list of warnings. A
new "Lint" button in the Enhanced MARC Editor displays the results.

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Open-ILS/src/eg2/src/app/staff/share/marc-edit/lint-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/lint-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/marc-edit.module.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.html
Open-ILS/src/eg2/src/app/staff/share/marc-edit/rich-editor.component.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm

diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/lint-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/lint-dialog.component.html
new file mode 100644 (file)
index 0000000..30bbc29
--- /dev/null
@@ -0,0 +1,29 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">
+      <span i18n>MARC Record Lint Results</span>
+    </h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row p-2" *ngIf="warnings.length === 0">
+        <span i18n>No warnings!</span>
+    </div>
+    <div class="row p-2" *ngIf="warnings.length > 0">
+      <ul>
+        <li *ngFor="let warning of warnings">
+          {{warning}}
+        </li>
+      </ul>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <ng-container *ngIf="!chargeResponse">
+      <button type="button" class="btn btn-warning" 
+        (click)="close()" i18n>Close</button>
+    </ng-container>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/lint-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/lint-dialog.component.ts
new file mode 100644 (file)
index 0000000..2009e4e
--- /dev/null
@@ -0,0 +1,25 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Dialog for merging authority records.
+ */
+
+@Component({
+  selector: 'eg-marc-record-lint-dialog',
+  templateUrl: 'lint-dialog.component.html'
+})
+
+export class MarcRecordLintDialogComponent
+    extends DialogComponent implements OnInit {
+
+    // lint warnings
+    @Input() warnings: any[] = [];
+
+    constructor(
+        private modal: NgbModal // required for passing to parent
+    ) {
+        super(modal); // required for subclassing
+    }
+}
index 4f2a39f..a4ebc1b 100644 (file)
@@ -12,6 +12,7 @@ import {AuthorityLinkingDialogComponent} from './authority-linking-dialog.compon
 import {MarcEditorDialogComponent} from './editor-dialog.component';
 import {PhysCharDialogComponent} from './phys-char-dialog.component';
 import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
+import {MarcRecordLintDialogComponent} from './lint-dialog.component';
 
 @NgModule({
     declarations: [
@@ -23,7 +24,8 @@ import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
         EditableContentComponent,
         MarcEditorDialogComponent,
         PhysCharDialogComponent,
-        AuthorityLinkingDialogComponent
+        AuthorityLinkingDialogComponent,
+        MarcRecordLintDialogComponent
     ],
     imports: [
         StaffCommonModule,
index e42517a..a0e11d8 100644 (file)
@@ -63,6 +63,9 @@
   </ng-container>
 </ng-template>
 
+
+<eg-marc-record-lint-dialog #lintDialog></eg-marc-record-lint-dialog>
+
 <ng-container *ngIf="dataLoaded">
   <div class="mt-3 text-monospace"
     (contextmenu)="$event.preventDefault()">
         <div><button class="btn btn-outline-dark"
           (click)="showHelp = !showHelp" i18n>Help</button></div>
         <ng-container *ngIf="context.recordType === 'biblio'">
-          <div class="mt-2"><button class="btn btn-outline-dark"
-            (click)="validate()" i18n>Validate</button></div>
+          <div class="mt-2">
+            <button class="btn btn-outline-dark"
+            (click)="validate()" i18n>Validate</button>
+            <button class="btn btn-outline-dark ml-2"
+            (click)="lint()" i18n>Lint</button>
+        </div>
         </ng-container>
         <div class="mt-2">
           <button type="button" class="btn btn-outline-info"
index 4555e20..1ec5526 100644 (file)
@@ -10,6 +10,7 @@ import {MarcRecord, MarcField} from './marcrecord';
 import {MarcEditContext} from './editor-context';
 import {AuthorityLinkingDialogComponent} from './authority-linking-dialog.component';
 import {PhysCharDialogComponent} from './phys-char-dialog.component';
+import {MarcRecordLintDialogComponent} from './lint-dialog.component';
 
 
 /**
@@ -39,6 +40,9 @@ export class MarcRichEditorComponent implements OnInit {
     @ViewChild('physCharDialog', {static: false})
         physCharDialog: PhysCharDialogComponent;
 
+    @ViewChild('lintDialog', {static: false})
+        lintDialog: MarcRecordLintDialogComponent;
+
     constructor(
         private idl: IdlService,
         private net: NetService,
@@ -146,6 +150,16 @@ export class MarcRichEditorComponent implements OnInit {
         });
     }
 
+    lint() {
+        const xml = this.record.toXml();
+        this.net.request('open-ils.cat',
+            'open-ils.cat.biblio.record.lint', xml)
+        .subscribe(warnings => {
+            this.lintDialog.warnings = warnings;
+            this.lintDialog.open({size: 'lg'});
+        });
+    }
+
     isControlledBibTag(tag: string): boolean {
         return this.controlledBibTags && this.controlledBibTags.includes(tag);
     }
index d072dd1..8a087ea 100644 (file)
@@ -23,6 +23,11 @@ use OpenSRF::Utils::SettingsClient;
 use OpenSRF::Utils::Logger qw($logger);
 use OpenSRF::AppSession;
 
+use MARC::File::XML (BinaryEncoding => 'utf8', RecordFormat => 'USMARC');
+use MARC::Record;
+use MARC::Lint;
+use MARC::Errorchecks;
+
 my $U = "OpenILS::Application::AppUtils";
 my $conf;
 my %marctemplates;
@@ -1929,6 +1934,51 @@ sub retrieve_tag_table {
     }
 }
 
+__PACKAGE__->register_method(
+    method    => "lint_marc_bib",
+    api_name  => "open-ils.cat.biblio.record.lint",
+    argc      => 1,
+    signature => {
+        desc   => 'Return a list of warnings about a bibliograhpic MARC record',
+        params => [
+            {desc => 'MARC record (as MARCXML)', type => 'string'},
+        ]
+    },
+    return => {desc => 'List of warnings about the MARC record', type => 'object' }
+);
+
+sub lint_marc_bib {
+    my ($self, $client, $marc) = @_;
+
+open F, '>', '/tmp/f';
+print F $marc;
+close F;
+    my $rec;
+    eval {
+        $rec = MARC::Record->new_from_xml($marc);
+    };
+    if ($@) {
+        return [ 'Record cannot be parsed at all (and crashed MARC::Record)' ];
+    };
+    unless (defined $rec) {
+        return [ 'Record cannot be parsed at all' ];
+    }
+
+    my @errors = ();
+    eval {
+        @errors = @{MARC::Errorchecks::check_all_subs($rec)};
+    };
+    if ($@) {
+        return [ 'Record could not be parsed by MARC::Errorchecks' ];
+    }
+
+    my $lint = MARC::Lint->new;
+    $lint->check_record($rec);
+    my @warnings = $lint->warnings;
+
+    return [ sort (@warnings, @errors) ];
+}
+
 1;
 
 # vi:et:ts=4:sw=4