</ng-container>
<span *ngIf="fromBibQueue" i18n>Add Records from queue #{{fromBibQueue}} to Bucket</span>
</h4>
- <button type="button" class="close"
+ <button type="button" class="close"
i18n-aria-label aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
<div class="row">
<div class="col-lg-3 font-weight-bold" i18n>Name of existing bucket</div>
<div class="col-lg-5">
- <eg-combobox [entries]="formatBucketEntries()"
+ <eg-combobox [entries]="formatBucketEntries()"
(onChange)="bucketChanged($event)"
placeholder="Existing Bucket..." i18n-placeholder>
</eg-combobox>
</div>
<div class="col-lg-4">
- <button class="btn btn-info" (click)="addToSelected()" i18n
+ <button class="btn btn-info" (click)="addToSelected()" i18n
[disabled]="!selectedBucket">
Add To Selected Bucket
</button>
<div class="row mt-3">
<div class="col-lg-3 font-weight-bold" i18n>Name of new bucket</div>
<div class="col-lg-5">
- <input type="text" class="form-control"
+ <input type="text" class="form-control"
placeholder="New Bucket Name..."
i18n-placeholder
[(ngModel)]="newBucketName"/>
</div>
<div class="col-lg-4">
- <button class="btn btn-info" (click)="addToNew()" i18n
+ <button class="btn btn-info" (click)="addToNew()" i18n
[disabled]="!newBucketName">
Add To New Bucket
</button>
<div class="row mt-3">
<div class="col-lg-3 font-weight-bold" i18n>New bucket description</div>
<div class="col-lg-5">
- <textarea size="3" type="text" class="form-control"
+ <textarea size="3" type="text" class="form-control"
placeholder="Optional New Bucket Description..."
i18n-placeholder
[(ngModel)]="newBucketDesc">
--- /dev/null
+
+<!-- display a single heading as MARC -->
+<ng-template #fieldAsMarc let-field="field">
+ <span>{{field.tag}} {{field.ind1}} {{field.ind2}}</span>
+ <span *ngFor="let sf of field.subfields">
+ <span class="text-danger" i18n>‡</span>{{sf[0]}} {{sf[1]}}
+ </span>
+</ng-template>
+
+<!-- display a single heading as MARC or as the human friendlier string -->
+<ng-template #headingField let-field="field" let-from="from" let-also="also">
+ <button class="btn btn-sm btn-outline-info p-1 mr-1"
+ (click)="applyHeading(field)" i18n>Apply</button>
+ <ng-container *ngIf="showAs == 'heading'">
+ <span *ngIf="from" i18n>See From: {{field.heading}}</span>
+ <span *ngIf="also" i18n>See Also: {{field.heading}}</span>
+ <span *ngIf="!from && !also" i18n>{{field.heading}}</span>
+ </ng-container>
+ <ng-container *ngIf="showAs == 'marc'">
+ <ng-container
+ *ngTemplateOutlet="fieldAsMarc;context:{field:field}">
+ </ng-container>
+ </ng-container>
+</ng-template>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title" i18n>Manage Authority Links</h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row border-bottom border-secondary p-2 d-flex">
+ <div class="flex-1 font-weight-bold p-1 pl-2 pt-2 ml-2">
+ <div>{{bibField.tag}} {{bibField.ind1}} {{bibField.ind2}}</div>
+
+ <div *ngFor="let sf of bibField.subfields">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" id="search-subfield-{{sf[0]}}"
+ type="checkbox" [disabled]="!isControlledBibSf(sf[0])"
+ [(ngModel)]="selectedSubfields[sf[0]]"
+ (change)="getPage(pager.offset)"/>
+
+ <span class="text-danger" i18n>‡</span>
+
+ <label class="form-check-label" for="search-subfield-{{sf[0]}}" i18n>
+ {{sf[0]}} {{sf[1]}}
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="ml-2 p-1">
+ <div class="mb-1" i18n>Create new authority from this field</div>
+ <div>
+ <button class="btn btn-outline-info" [disabled]="true">
+ Immediately
+ </button>
+ <button class="btn btn-outline-info ml-2" [disabled]="true">
+ Create and Edit
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="row border-bottom border-secondary p-2 d-flex">
+ <div class="flex-1">
+ <button class="btn btn-outline-dark" [disabled]="pager.offset == 0"
+ (click)="getPage(0)" i18n>Start</button>
+ <button class="btn btn-outline-dark ml-2"
+ (click)="getPage(-1)" i18n>Previous</button>
+ <button class="btn btn-outline-dark ml-2"
+ (click)="getPage(1)" i18n>Next</button>
+ </div>
+ <div class="pt-2 mb-2">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="radio" value="heading"
+ [(ngModel)]="showAs" name='show-as-heading' id="show-as-heading">
+ <label class="form-check-label" for="show-as-heading" i18n>Show As Heading</label>
+ </div>
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="radio" value="marc"
+ [(ngModel)]="showAs" name='show-as-heading' id="show-as-marc">
+ <label class="form-check-label" for="show-as-marc" i18n>Show As MARC</label>
+ </div>
+ </div>
+ </div>
+ <ul *ngFor="let entry of browseData">
+ <li class="d-flex">
+ <div class="flex-1">
+ <ng-container
+ *ngTemplateOutlet="headingField;context:{field:entry.main_heading}">
+ </ng-container>
+ </div>
+ <div class="font-italic" i18n-title i18n
+ title="Authority Record ID {{entry.authority_id}}">
+ #{{entry.authority_id}}
+ </div>
+ </li>
+ <ul *ngFor="let from of entry.see_froms">
+ <li i18n>
+ <ng-container
+ *ngTemplateOutlet="headingField;context:{field:from, from:true}">
+ </ng-container>
+ </li>
+ </ul>
+ <ul *ngFor="let also of entry.see_alsos">
+ <li i18n>
+ <ng-container
+ *ngTemplateOutlet="headingField;context:{field:also, also:true}">
+ </ng-container>
+ </li>
+ </ul>
+ </ul>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {MarcField} from './marcrecord';
+import {Pager} from '@eg/share/util/pager';
+
+/**
+ * MARC Authority Linking Dialog
+ */
+
+@Component({
+ selector: 'eg-authority-linking-dialog',
+ templateUrl: './authority-linking-dialog.component.html'
+})
+
+export class AuthorityLinkingDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() bibField: MarcField;
+ @Input() thesauri: string = null;
+ @Input() controlSet: number = null;
+ @Input() pager: Pager;
+
+ browseData: any[] = [];
+
+ // If false, show the raw MARC field data.
+ showAs: 'heading' | 'marc' = 'heading';
+
+ authMeta: any;
+
+ selectedSubfields: string[] = [];
+
+ constructor(
+ private modal: NgbModal,
+ private pcrud: PcrudService,
+ private net: NetService) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ if (!this.pager) {
+ this.pager = new Pager();
+ this.pager.limit = 5;
+ }
+
+ this.onOpen$.subscribe(_ => this.initData());
+ }
+
+ fieldHash(field?: MarcField): any {
+ if (!field) { field = this.bibField; }
+
+ return {
+ tag: field.tag,
+ ind1: field.ind1,
+ ind2: field.ind2,
+ subfields: field.subfields.map(sf => [sf[0], sf[1]])
+ };
+ }
+
+ initData() {
+
+ this.pager.offset = 0;
+
+ this.pcrud.search('acsbf',
+ {tag: this.bibField.tag},
+ {flesh: 1, flesh_fields: {acsbf: ['authority_field']}},
+ {atomic: true, anonymous: true}
+
+ ).subscribe(bibMetas => {
+ if (bibMetas.length === 0) { return; }
+
+ let bibMeta;
+ if (this.controlSet) {
+ bibMeta = bibMetas.filter(b =>
+ this.controlSet === +b.authority_field().control_set());
+ } else {
+ bibMeta = bibMetas[0];
+ }
+
+ if (bibMeta) {
+ this.authMeta = bibMeta.authority_field();
+ this.bibField.subfields.forEach(sf =>
+ this.selectedSubfields[sf[0]] =
+ this.isControlledBibSf(sf[0])
+ );
+ }
+
+ this.getPage(0);
+ });
+ }
+
+ getPage(direction: number) {
+ this.browseData = [];
+
+ if (direction > 0) {
+ this.pager.offset++;
+ } else if (direction < 0) {
+ this.pager.offset--;
+ } else {
+ this.pager.offset = 0;
+ }
+
+ const hash = this.fieldHash();
+
+ // Only search the selected subfields
+ hash.subfields =
+ hash.subfields.filter(sf => this.selectedSubfields[sf[0]]);
+
+ if (hash.subfields.length === 0) { return; }
+
+ this.net.request(
+ 'open-ils.cat',
+ 'open-ils.cat.authority.bib_field.linking_browse',
+ hash, this.pager.limit,
+ this.pager.offset, this.thesauri
+ ).subscribe(entry => this.browseData.push(entry));
+ }
+
+ applyHeading(authField: MarcField) {
+ this.net.request(
+ 'open-ils.cat',
+ 'open-ils.cat.authority.bib_field.overlay_authority',
+ this.fieldHash(), this.fieldHash(authField), this.controlSet
+ ).subscribe(field => this.close(field));
+ }
+
+ isControlledBibSf(sf: string): boolean {
+ return this.authMeta ?
+ this.authMeta.sf_list().includes(sf) : false;
+ }
+}
+
min-height: calc(1.5em + .75rem + 2px);
}
-.sf-delimiter {
+.sf-delimiter {
/* match angjs color */
- color: rgb(0, 0, 255)!important;
+ color: rgb(0, 0, 255)!important;
/* snuggle up to my subfield code */
- margin-right: -0.5rem;
+ margin-right: -0.5rem;
}
-.sf-code {
+.sf-code {
/* match angjs color */
- color: rgb(0, 0, 255)!important;
+ color: rgb(0, 0, 255)!important;
+}
+
+.auth-invalid {
+ color: rgb(255, 0, 0)!important;
}
-<!--
-Some context menus have additional static options.
+<!--
+Some context menus have additional static options.
Track their labels here.
-->
<eg-string #add006 text="Add 006" i18n-text></eg-string>
<ng-container *ngIf="bigText">
<div contenteditable
- id='{{randId}}'
+ id='{{randId}}'
spellcheck="false"
class="d-inline-block text-dark text-break {{moreClasses}}"
+ [ngClass]="{'auth-invalid': isAuthInvalid()}"
[attr.tabindex]="fieldText ? -1 : ''"
[attr.aria-label]="ariaLabel"
[egContextMenu]="contextMenuEntries()"
</ng-container>
<ng-container *ngIf="!bigText">
- <input
- id='{{randId}}'
+ <input
+ id='{{randId}}'
spellcheck="false"
class="text-dark rounded-0 form-control {{moreClasses}}"
- [size]="inputSize()"
+ [ngClass]="{'auth-invalid': isAuthInvalid()}"
+ [size]="inputSize()"
[maxlength]="maxLength || ''"
- [disabled]="fieldText"
+ [disabled]="fieldText"
[attr.tabindex]="fieldText ? -1 : ''"
[attr.aria-label]="ariaLabel"
[egContextMenu]="contextMenuEntries()"
// Context menus can steal focus.
this.context.requestFieldFocus(this.context.lastFocused);
}
+
+ isAuthInvalid(): boolean {
+ return (
+ this.fieldType === 'sfv' &&
+ this.field.authChecked &&
+ !this.field.authValid
+ );
+ }
+
+ isAuthValid(): boolean {
+ return (
+ this.fieldType === 'sfv' &&
+ this.field.authChecked &&
+ this.field.authValid
+ );
+ }
+
+ isLastSubfieldValue(): boolean {
+ if (this.fieldType === 'sfv') {
+ const myIdx = this.subfield[2];
+ for (let idx = 0; idx < this.field.subfields.length; idx++) {
+ if (idx > myIdx) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
}
// Which stack do we toss this on once it's been applied?
isRedo: boolean;
+
+ // Grouped actions are tracked as multiple undo / redo actions, but
+ // are done and un-done as a unit.
+ groupSize?: number;
}
export class TextUndoRedoAction extends UndoRedoAction {
requestFieldFocus(req: FieldFocusRequest) {
// timeout allows for new components to be built before the
// focus request is emitted.
- setTimeout(() => this.fieldFocusRequest.emit(req));
+ if (req) {
+ setTimeout(() => this.fieldFocusRequest.emit(req));
+ }
}
resetUndos() {
}
requestUndo() {
- const undo = this.undoStack.shift();
- if (undo) {
- undo.isRedo = false;
- this.distributeUndoRedo(undo);
- }
+ let remaining = null;
+
+ do {
+ const action = this.undoStack.shift();
+ if (!action) { return; }
+
+ if (remaining === null) {
+ remaining = action.groupSize || 1;
+ }
+ remaining--;
+
+ action.isRedo = false;
+ this.distributeUndoRedo(action);
+
+ } while (remaining > 0);
}
requestRedo() {
- const redo = this.redoStack.shift();
- if (redo) {
- redo.isRedo = true;
- this.distributeUndoRedo(redo);
+ let remaining = null;
+
+ do {
+ const action = this.redoStack.shift();
+ if (!action) { return; }
+
+ if (remaining === null) {
+ remaining = action.groupSize || 1;
+ }
+ remaining--;
+
+ action.isRedo = true;
+ this.distributeUndoRedo(action);
+
+ } while (remaining > 0);
+ }
+
+ // Calculate stack action count taking groupSize (atomic action
+ // sets) into consideration.
+ stackCount(stack: UndoRedoAction[]): number {
+ let size = 0;
+ let skip = 0;
+
+ stack.forEach(action => {
+ if (action.groupSize > 1) {
+ if (skip) { return; }
+ skip = 1;
+ } else {
+ skip = 0;
+ }
+ size++;
+ });
+
+ return size;
+ }
+
+ undoCount(): number {
+ return this.stackCount(this.undoStack);
+ }
+
+ redoCount(): number {
+ return this.stackCount(this.redoStack);
+ }
+
+ // Stamp the most recent 'size' entries in the undo stack
+ // as being an atomic undo/redo set.
+ setUndoGroupSize(size: number) {
+ for (let idx = 0; idx < size; idx++) {
+ if (this.undoStack[idx]) {
+ this.undoStack[idx].groupSize = size;
+ }
}
}
import {FixedFieldComponent} from './fixed-field.component';
import {TagTableService} from './tagtable.service';
import {EditableContentComponent} from './editable-content.component';
+import {AuthorityLinkingDialogComponent} from './authority-linking-dialog.component';
@NgModule({
declarations: [
MarcFlatEditorComponent,
FixedFieldsEditorComponent,
FixedFieldComponent,
- EditableContentComponent
+ EditableContentComponent,
+ AuthorityLinkingDialogComponent
],
imports: [
StaffCommonModule,
ind2?: string;
subfields?: MarcSubfield[];
+ // For authority validation
+ authValid: boolean;
+ authChecked: boolean;
+
// Fields are immutable when it comes to controlfield vs.
// data field. Stamp the value when stamping field IDs.
isCtrlField: boolean;
border-bottom: 1px solid gray;
*/
}
+
+.link-button .material-icons {
+ font-size: 17px;
+ display: inline-flex;
+ vertical-align: middle;
+ align-items: center;
+}
</div>
</ng-container>
+<eg-authority-linking-dialog #authLinker></eg-authority-linking-dialog>
<ng-template #subfieldChunk let-field="field" let-subfield="subfield">
</ng-template>
+<ng-template #postSubfieldsChunk let-field="field">
+
+ <ng-container *ngIf="isControlledBibTag(field.tag)">
+ <button class="btn btn-sm btn-info link-button"
+ (click)="openLinkerDialog(field)">
+ <span class="material-icons">link</span>
+ </button>
+ </ng-container>
+
+ <ng-container *ngIf="field.authChecked">
+ <span class="pl-2 pt-2">
+ <span *ngIf="field.authValid"
+ title="Authority Validation Succeeded" i18n-title
+ class="material-icons label-with-material-icon text-success">
+ check_circle_outline
+ </span>
+ <span *ngIf="!field.authValid"
+ title="Authority Validation Failed" i18n-title
+ class="material-icons label-with-material-icon text-danger">
+ error_outline
+ </span>
+ </span>
+ </ng-container>
+</ng-template>
+
<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>
<div class="mt-2"><button class="btn btn-outline-dark"
- [disabled]="true"
(click)="validate()" i18n>Validate</button></div>
<div class="mt-2">
- <button type="button" class="btn btn-outline-info"
+ <button type="button" class="btn btn-outline-info"
[disabled]="undoCount() < 1" (click)="undo()">
Undo <span class="badge badge-info">{{undoCount()}}</span>
</button>
- <button type="button" class="btn btn-outline-info ml-2"
+ <button type="button" class="btn btn-outline-info ml-2"
[disabled]="redoCount() < 1" (click)="redo()">
Redo <span class="badge badge-info">{{redoCount()}}</span>
</button>
</div>
<div class="mt-2">
<div class="form-check">
- <input class="form-check-input" type="checkbox"
+ <input class="form-check-input" type="checkbox"
(change)="stackSubfieldsChange()"
[(ngModel)]="stackSubfields" id="stack-subfields-{{randId}}">
<label class="form-check-label" for="stack-subfields-{{randId}}">
<!-- when not stacking subfields, render them inline -->
<ng-container *ngIf="!stackSubfields">
- <ng-container *ngFor="let subfield of field.subfields">
- <ng-container
+ <ng-container *ngFor="let subfield of field.subfields; let last = last">
+ <ng-container
*ngTemplateOutlet="subfieldChunk;context:{field:field,subfield:subfield}">
</ng-container>
+ <ng-container *ngIf="last">
+ <ng-container
+ *ngTemplateOutlet="postSubfieldsChunk;context:{field:field}">
+ </ng-container>
+ </ng-container>
</ng-container>
</ng-container>
+
</div>
- <!-- when stacking subfields, each subfield gets its own row
+ <!-- when stacking subfields, each subfield gets its own row
preceeded by a placeholder for the tag as a way to 'tab' right -->
<ng-container *ngIf="stackSubfields">
<div class="form-inline" *ngFor="let subfield of field.subfields">
<eg-marc-editable-content fieldText=" " moreClasses="p-1 invisible">
</eg-marc-editable-content>
- <ng-container
+ <ng-container
*ngTemplateOutlet="subfieldChunk;context:{field:field,subfield:subfield}">
</ng-container>
</div>
import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
- OnDestroy} from '@angular/core';
+ ViewChild, OnDestroy} from '@angular/core';
import {filter} from 'rxjs/operators';
import {IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
import {OrgService} from '@eg/core/org.service';
import {ServerStoreService} from '@eg/core/server-store.service';
import {TagTableService} from './tagtable.service';
import {MarcRecord, MarcField} from './marcrecord';
import {MarcEditContext} from './editor-context';
+import {AuthorityLinkingDialogComponent} from './authority-linking-dialog.component';
/**
showHelp: boolean;
randId = Math.floor(Math.random() * 100000);
stackSubfields: boolean;
+ controlledBibTags: string[] = [];
+
+ @ViewChild('authLinker', {static: false})
+ authLinker: AuthorityLinkingDialogComponent;
constructor(
private idl: IdlService,
+ private net: NetService,
private org: OrgService,
private store: ServerStoreService,
private tagTable: TagTableService
return Promise.all([
this.tagTable.loadTagTable({marcRecordType: this.context.recordType}),
this.tagTable.getFfPosTable(this.record.recordType()),
- this.tagTable.getFfValueTable(this.record.recordType())
+ this.tagTable.getFfValueTable(this.record.recordType()),
+ this.tagTable.getControlledBibTags().then(
+ tags => this.controlledBibTags = tags)
]).then(_ =>
// setTimeout forces all of our sub-components to rerender
// themselves each time init() is called. Without this,
}
undoCount(): number {
- return this.context.undoStack.length;
+ return this.context.undoCount();
}
redoCount(): number {
- return this.context.redoStack.length;
+ return this.context.redoCount();
}
undo() {
dataFields(): MarcField[] {
return this.record.fields.filter(f => !f.isCtrlField);
}
+
+ validate() {
+ const fields = [];
+ this.record.fields.filter(f => this.isControlledBibTag(f.tag))
+
+ .forEach(f => {
+ f.authValid = false;
+ fields.push({
+ id: f.fieldId, // ignored and echoed by server
+ tag: f.tag,
+ ind1: f.ind1,
+ ind2: f.ind2,
+ subfields: f.subfields.map(sf => ({code: sf[0], value: sf[1]}))
+ });
+ });
+
+ this.net.request('open-ils.cat',
+ 'open-ils.cat.authority.validate.bib_field', fields)
+ .subscribe(checkedField => {
+ const bibField = this.record.fields
+ .filter(f => f.fieldId === +checkedField.id)[0];
+
+ bibField.authChecked = true;
+ bibField.authValid = checkedField.valid;
+ });
+ }
+
+ isControlledBibTag(tag: string): boolean {
+ return this.controlledBibTags && this.controlledBibTags.includes(tag);
+ }
+
+ openLinkerDialog(field: MarcField) {
+ this.authLinker.bibField = field;
+ this.authLinker.open({size: 'lg'}).subscribe(newField => {
+ if (!newField) { return; }
+
+ // Performs an insert followed by a delete, so the two
+ // fields can be tracked separately for undo/redo actions.
+ const marcField = this.record.newField(newField);
+ this.context.insertField(field, marcField);
+ this.context.deleteField(field);
+
+ // Mark the insert and delete as an atomic undo/redo action.
+ this.context.setUndoGroupSize(2);
+ });
+ }
}
import {Injectable, EventEmitter} from '@angular/core';
-import {map, tap} from 'rxjs/operators';
+import {Observable} from 'rxjs';
+import {map, tap, distinct} from 'rxjs/operators';
import {StoreService} from '@eg/core/store.service';
import {IdlObject} from '@eg/core/idl.service';
import {AuthService} from '@eg/core/auth.service';
tagMap: {[tag: string]: any} = {};
ffPosMap: {[rtype: string]: any[]} = {};
ffValueMap: {[rtype: string]: any} = {};
+ controlledBibTags: string[];
extractedValuesCache:
{[valueType: string]: {[which: string]: any}} = {};
return this.toCache('ffvalues', recordType, fieldCode, values);
}
+
+ getControlledBibTags(): Promise<string[]> {
+ if (this.controlledBibTags) {
+ return Promise.resolve(this.controlledBibTags);
+ }
+
+ this.controlledBibTags = [];
+ return this.pcrud.retrieveAll('acsbf', {select: ['tag']})
+ .pipe(
+ map(field => field.tag()),
+ distinct(),
+ map(tag => this.controlledBibTags.push(tag))
+ ).toPromise().then(_ => this.controlledBibTags);
+ }
}
package OpenILS::Application::Cat::Authority;
use strict; use warnings;
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'utf8', RecordFormat => 'USMARC');
use base qw/OpenILS::Application/;
use OpenILS::Utils::CStoreEditor q/:funcs/;
use OpenILS::Application::Cat::AuthCommon;
return undef;
}
+__PACKAGE__->register_method(
+ method => "bib_field_overlay_authority_field",
+ api_name => "open-ils.cat.authority.bib_field.overlay_authority",
+ api_level => 1,
+ stream => 1,
+ argc => 2,
+ signature => {
+ desc => q/Given a bib field hash and an authority field hash,
+ merge the authority data for controlled fields into the
+ bib field./,
+ params => [
+ {name => 'Bib Field',
+ desc => '{tag:., ind1:., ind2:.,subfields:[[code, value],...]}'},
+ {name => 'Authority Field',
+ desc => '{tag:., ind1:., ind2:.,subfields:[[code, value],...]}'},
+ {name => 'Control Set ID',
+ desc => q/Optional control set limiter. If no control set
+ is provided, the first matching authority field
+ definition will be used./}
+ ],
+ return => q/The modified bib field/
+ }
+);
+
+# Returns the first found field.
+sub get_auth_field {
+ my ($atag, $cset_id) = @_;
+
+ my $e = new_editor();
+
+ my $where = {tag => $atag};
+
+ $where->{control_set} = $cset_id if $cset_id;
+
+ return $e->search_authority_control_set_authority_field($where)->[0];
+}
+
+sub bib_field_overlay_authority_field {
+ my ($self, $client, $bib_field, $auth_field, $cset_id) = @_;
+
+ return $bib_field unless $bib_field && $auth_field;
+
+ my $btag = $bib_field->{'tag'};
+ my $atag = $auth_field->{'tag'};
+
+ # Find the controlled subfields. Here we assume the authority
+ # field provided should be used as the source of which subfields
+ # are controlled. If passed a set of bib and auth data that are
+ # not consistent with the control set, it may produce unexpected
+ # results.
+ my $sf_list = '';
+ my $acsaf = get_auth_field($atag, $cset_id);
+
+ if ($acsaf) {
+ $sf_list = $acsaf->sf_list;
+
+ } else {
+
+ # Handle 4XX and 5XX
+ (my $alt_atag = $atag) =~ s/^./1/;
+ $acsaf = get_auth_field($alt_atag, $cset_id) if $alt_atag ne $atag;
+
+ $sf_list = $acsaf->sf_list if $acsaf;
+ }
+
+ my $subfields = [];
+ my $auth_sf_zero;
+
+ # Add the controlled authority subfields
+ for my $sf (@{$auth_field->{subfields}}) {
+ my $c = $sf->[0]; # subfield code
+ my $v = $sf->[1]; # subfield value
+
+ if ($c eq '0') {
+ $auth_sf_zero = $v;
+
+ } elsif (index($sf_list, $c) > -1) {
+ push(@$subfields, [$c, $v]);
+ }
+ }
+
+ # Add the uncontrolled bib subfields
+ for my $sf (@{$bib_field->{subfields}}) {
+ my $c = $sf->[0]; # subfield code
+ my $v = $sf->[1]; # subfield value
+
+ # Discard the bib '0' since the link is no longer valid,
+ # given we're replacing the contents of the field.
+ if (index($sf_list, $c) < 0 && $c ne '0') {
+ push(@$subfields, [$c, $v]);
+ }
+ }
+
+ # The data on this authority field may link to yet
+ # another authority record. Track that in our bib field
+ # as the last subfield;
+ push(@$subfields, ['0', $auth_sf_zero]) if $auth_sf_zero;
+
+ my $new_bib_field = {
+ tag => $bib_field->{tag},
+ ind1 => $auth_field->{'ind1'},
+ ind2 => $auth_field->{'ind2'},
+ subfields => $subfields
+ };
+
+ $new_bib_field->{ind1} = $auth_field->{'ind2'}
+ if $atag eq '130' && $btag eq '130';
+
+ return $new_bib_field;
+}
+
+__PACKAGE__->register_method(
+ method => "validate_bib_fields",
+ api_name => "open-ils.cat.authority.validate.bib_field",
+ stream => 1,
+ signature => {
+ desc => q/Returns a stream of bib field objects with a 'valid'
+ attribute added, set to 1 or 0, indicating whether the field
+ has a matching authority entry. If no control set ID is provided
+ all configured control sets will be tested. Testing will stop
+ with the first positive validation./,
+ params => [
+ {type => 'object', name => 'Bib Fields',
+ description => q/
+ List of objects like this
+ {
+ tag: tag,
+ ind1: ind1,
+ ind2: ind2,
+ subfields: [[code, value], ...]
+ }
+
+ For example:
+srfsh# request open-ils.cat open-ils.cat.authority.validate.bib_field
+ [{"tag":"600","ind1":"", "ind2":"", "subfields":[["a","shakespeare william"], ...]}]
+ /
+ },
+ {type => 'number', name => 'Optional Control Set ID'},
+ ]
+ }
+);
+
+# for stub records sent to
+# open-ils.cat.authority.simple_heading
+my $auth_leader = '00000czm a2200205Ka 4500';
+
+sub validate_bib_fields {
+ my ($self, $client, $bib_fields, $control_set) = @_;
+
+ $bib_fields = [$bib_fields] unless ref $bib_fields eq 'ARRAY';
+
+ my $e = new_editor();
+
+ for my $bib_field (@$bib_fields) {
+
+ $bib_field->{valid} = 0;
+
+ my $where = {'+acsbf' => {tag => $bib_field->{tag}}};
+ $where->{'+acsaf'} = {control_set => $control_set} if $control_set;
+
+ my $auth_field_list = $e->json_query({
+ select => {
+ acsbf => ['authority_field'],
+ acsaf => ['id', 'tag', 'sf_list', 'control_set']
+ },
+ from => {acsbf => {acsaf => {}}},
+ where => $where
+ });
+
+ my @seen_subfields;
+ for my $auth_field (@$auth_field_list) {
+
+ my $sf_list = $auth_field->{sf_list};
+
+ # Some auth fields have the same sf_list values. Track the
+ # ones we've already tested.
+ next if grep {$_ eq $sf_list} @seen_subfields;
+
+ push(@seen_subfields, $sf_list);
+
+ my @sf_values;
+ for my $subfield (@{$bib_field->{subfields}}) {
+ my $code = $subfield->[0];
+ my $value = $subfield->[1];
+
+ next unless defined $value && $value ne '';
+
+ # is this a controlled subfield?
+ next unless index($sf_list, $code) > -1;
+
+ push(@sf_values, $code, $value);
+ }
+
+ next unless @sf_values;
+
+ my $record = MARC::Record->new;
+ $record->leader($auth_leader);
+
+ my $field = MARC::Field->new($auth_field->{tag},
+ $bib_field->{ind1}, $bib_field->{ind2}, @sf_values);
+
+ $record->append_fields($field);
+
+ my $match = $U->simplereq(
+ 'open-ils.cat',
+ 'open-ils.cat.authority.simple_heading.from_xml',
+ $record->as_xml_record, $control_set);
+
+ if ($match) {
+ $bib_field->{valid} = 1;
+ $bib_field->{authority_record} = $match;
+ $bib_field->{authority_field} = $auth_field->{id};
+ $bib_field->{control_set} = $auth_field->{control_set};
+ last;
+ }
+ }
+
+ # Present our findings.
+ $client->respond($bib_field);
+ }
+
+ return undef;
+}
+
+
+__PACKAGE__->register_method(
+ method => "bib_field_authority_linking_browse",
+ api_name => "open-ils.cat.authority.bib_field.linking_browse",
+ stream => 1,
+ signature => {
+ desc => q/Returns a stream of authority record blobs including
+ information on its main heading and its see froms and see
+ alsos, based on an axis-based browse search. This was
+ initially created to move some MARC editor authority linking
+ logic to the server. The browse axis is derived from the
+ bib field data provided.
+ ...
+ /,
+ params => [
+ {type => 'object', name => 'MARC Field hash {tag:.,ind1:.,ind2:,subfields:[[code,value],.]}'},
+ {type => 'number', name => 'Page size / limit'},
+ {type => 'number', name => 'Page offset'},
+ {type => 'string', name => 'Optional thesauri, comma separated'}
+ ]
+ }
+);
+
+sub get_heading_string {
+ my $field = shift;
+
+ my $heading = '';
+ for my $subfield ($field->subfields) {
+ $heading .= ' --' if index('xyz', $subfield->[0]) > -1;
+ $heading .= ' ' if $heading;
+ $heading .= $subfield->[1];
+ }
+
+ return $heading;
+}
+
+# Turns a MARC::Field into a hash and adds the field's heading string.
+sub hashify_field {
+ my $field = shift;
+ return {
+ heading => get_heading_string($field),
+ tag => $field->tag,
+ ind1 => $field->indicator(1),
+ ind2 => $field->indicator(2),
+ subfields => [$field->subfields]
+ };
+}
+
+sub bib_field_authority_linking_browse {
+ my ($self, $client, $bib_field, $limit, $offset, $thesauri) = @_;
+
+ $offset ||= 0;
+ $limit ||= 5;
+ $thesauri ||= '';
+ my $e = new_editor();
+
+ return [] unless $bib_field;
+
+ my $term = join(' ', map {$_->[0]} @{$bib_field->{subfields}});
+
+ return [] unless $term;
+
+ my $axis = $e->json_query({
+ select => {abaafm => ['axis']},
+ from => {acsbf => {acsaf => {join => 'abaafm'}}},
+ where => {'+acsbf' => {tag => $bib_field->{tag}}}
+ })->[0];
+
+ return [] unless $axis && ($axis = $axis->{axis});
+
+ # See https://bugs.launchpad.net/evergreen/+bug/1403098
+ my $are_ids = $U->simplereq(
+ 'open-ils.supercat',
+ 'open-ils.supercat.authority.browse_center.by_axis.refs',
+ $axis, $term, $offset, $limit, $thesauri);
+
+ for my $are_id (@$are_ids) {
+
+ my $are = $e->retrieve_authority_record_entry($are_id);
+ my $rec = MARC::Record->new_from_xml($are->marc, 'UTF-8');
+
+ my $main_field = $rec->field('1..');
+ my $auth_org_field = $rec->field('003');
+ my $auth_org = $auth_org_field ? $auth_org_field->data : undef;
+
+ my $resp = {
+ authority_id => $are_id,
+ main_heading => hashify_field($main_field),
+ auth_org => $auth_org,
+ see_alsos => [],
+ see_froms => []
+ };
+
+ for my $also_field ($rec->field('5..')) {
+ push(@{$resp->{see_alsos}}, hashify_field($also_field));
+ }
+
+ for my $from_field ($rec->field('4..')) {
+ push(@{$resp->{see_froms}}, hashify_field($from_field));
+ }
+
+ $client->respond($resp);
+ }
+
+ return undef;
+}
+
1;
{{main.heading}}
(<span style="font-family: 'Lucida Console', Monaco, monospace;">
<span ng-repeat="sf in main.headingField.subfields">
- <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
+ <span class="marcsfcodedelimiter">‡{{sf[0]}}</span> {{sf[1]}}
</span>
</span>)
</div>
[% l('See from: [_1]', '{{seefrom.heading}}') %]
(<span style="font-family: 'Lucida Console', Monaco, monospace;">
<span ng-repeat="sf in seefrom.headingField.subfields">
- <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
+ <span class="marcsfcodedelimiter">‡{{sf[0]}}</span> {{sf[1]}}
</span>
</span>)
</div>
[% l('See also from: [_1]', '{{seealso.heading}}') %]
(<span style="font-family: 'Lucida Console', Monaco, monospace;">
<span ng-repeat="sf in seealso.headingField.subfields">
- <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
+ <span class="marcsfcodedelimiter">‡{{sf[0]}}</span> {{sf[1]}}
</span>
</span>)
</div>
<div class="row form-inline" style="font-family: 'Lucida Console', Monaco, monospace;">
{{bibField.tag}} {{bibField.ind1}}{{bibField.ind2}}
<div ng-repeat="sf in bibField.subfields">
- <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
+ <span class="marcsfcodedelimiter">‡{{sf[0]}}</span> {{sf[1]}}
<input type="checkbox" ng-model="sf.selected" ng-if="sf.selectable" />
</div>
</div>