<link field="record" reltype="has_a" key="id" map="" class="bre"/>
</links>
</class>
- <class id="mmr" controller="open-ils.cstore" oils_obj:fieldmapper="metabib::metarecord" oils_persist:tablename="metabib.metarecord" reporter:label="Metarecord">
+ <class id="mmr" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::metarecord" oils_persist:tablename="metabib.metarecord" reporter:label="Metarecord">
<fields oils_persist:primary="id" oils_persist:sequence="metabib.metarecord_id_seq">
<field name="fingerprint" reporter:datatype="text"/>
<field name="id" reporter:datatype="id" />
<field name="master_record" reporter:datatype="link"/>
<field name="mods" reporter:datatype="text"/>
<field name="source_records" oils_persist:virtual="true" reporter:datatype="link"/>
+ <field name="source_maps" oils_persist:virtual="true" reporter:datatype="link"/>
</fields>
<links>
<link field="master_record" reltype="has_a" key="id" map="" class="bre"/>
<link field="source_records" reltype="has_many" key="metarecord" map="source" class="mmrsm"/>
+ <link field="source_maps" reltype="has_many" key="metarecord" class="mmrsm"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
</class>
<class id="cnal" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::net_access_level" oils_persist:tablename="config.net_access_level" reporter:label="Net Access Level">
<fields oils_persist:primary="id" oils_persist:sequence="config.net_access_level_id_seq">
</actions>
</permacrud>
</class>
- <class id="mmrsm" controller="open-ils.cstore" oils_obj:fieldmapper="metabib::metarecord_source_map" oils_persist:tablename="metabib.metarecord_source_map" oils_persist:field_safe="true" reporter:label="Metarecord Source Map">
+ <class id="mmrsm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="metabib::metarecord_source_map" oils_persist:tablename="metabib.metarecord_source_map" oils_persist:field_safe="true" reporter:label="Metarecord Source Map">
<fields oils_persist:primary="id" oils_persist:sequence="metabib.metarecord_source_map_id_seq">
<field name="id" reporter:datatype="id" />
<field name="metarecord" reporter:datatype="link"/>
<link field="source" reltype="has_a" key="id" map="" class="bre"/>
<link field="metarecord" reltype="has_a" key="id" map="" class="mmr"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
</class>
<class id="mde" controller="open-ils.cstore open-ils.pcrud"
oils_obj:fieldmapper="metabib::display_entry"
import {mergeMap} from 'rxjs/operators/mergeMap';
import {from} from 'rxjs/observable/from';
import {map} from 'rxjs/operators/map';
+import {tap} from 'rxjs/operators/tap';
import {OrgService} from '@eg/core/org.service';
import {UnapiService} from '@eg/share/catalog/unapi.service';
import {IdlService, IdlObject} from '@eg/core/idl.service';
export class BibRecordSummary {
id: number; // == record.id() for convenience
+ metabibId: number; // If present, this is a metabib summary
orgId: number;
orgDepth: number;
record: IdlObject;
// Any attr can be multi-valued.
this.record.mattrs().forEach(attr => {
if (this.attributes[attr.attr()]) {
- this.attributes[attr.attr()].push(attr.value());
+ // Avoid dupes
+ if (this.attributes[attr.attr()].indexOf(attr.value()) < 0) {
+ this.attributes[attr.attr()].push(attr.value());
+ }
} else {
this.attributes[attr.attr()] = [attr.value()];
}
return Promise.resolve(this.holdCount);
}
+ let method = 'open-ils.circ.bre.holds.count';
+ let target = this.id;
+
+ if (this.metabibId) {
+ method = 'open-ils.circ.mmr.holds.count';
+ target = this.metabibId;
+ }
+
return this.net.request(
- 'open-ils.circ',
- 'open-ils.circ.bre.holds.count', this.id
+ 'open-ils.circ', method, target
).toPromise().then(count => this.holdCount = count);
}
}
// Avoid fetching the MARC blob by specifying which fields on the
- // bre to select. Note that fleshed fields are explicitly selected.
+ // bre to select. Note that fleshed fields are implicitly selected.
fetchableBreFields(): string[] {
return this.idl.classes.bre.fields
.filter(f => !f.virtual && f.name !== 'marc')
}));
}
+ // A Metabib Summary is a BibRecordSummary with the lead record as
+ // its core bib record plus attributes (e.g. formats) from related
+ // records.
+ getMetabibSummary(metabibIds: number | number[],
+ orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
+
+ const ids = [].concat(metabibIds);
+
+ if (ids.length === 0) {
+ return from([]);
+ }
+
+ return this.pcrud.search('mmr', {id: ids},
+ {flesh: 1, flesh_fields: {mmr: ['source_maps']}},
+ {anonymous: true}
+ ).pipe(mergeMap(mmr => this.compileMetabib(mmr, orgId, orgDepth)));
+ }
+
+ // 'metabib' must have its "source_maps" field fleshed.
+ // Get bib summaries for all related bib records so we can
+ // extract data that must be appended to the master record summary.
+ compileMetabib(metabib: IdlObject,
+ orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
+
+ // TODO: Create an API similar to the one that builds a combined
+ // mods blob for metarecords, except using display fields, etc.
+ // For now, this seems to get the job done.
+
+ // Non-master records
+ const relatedBibIds = metabib.source_maps()
+ .map(map => map.source())
+ .filter(id => id !== metabib.master_record());
+
+ let observer;
+ const observable = new Observable<BibRecordSummary>(o => observer = o);
+
+ // NOTE: getBibSummary calls getHoldingsSummary against
+ // the bib record unnecessarily. It's called again below.
+ // Reconsider this approach (see also note above about API).
+ this.getBibSummary(metabib.master_record(), orgId, orgDepth)
+ .subscribe(summary => {
+ summary.metabibId = metabib.id();
+
+ let promise;
+
+ if (relatedBibIds.length > 0) {
+
+ // Grab data for MR bib summary augmentation
+ promise = this.pcrud.search('mraf', {id: relatedBibIds})
+ .pipe(tap(attr => summary.record.mattrs().push(attr)))
+ .toPromise();
+ } else {
+
+ // Metarecord has only one constituent bib.
+ promise = Promise.resolve();
+ }
+
+ promise.then(() => {
+
+ // Re-compile with augmented data
+ summary.compileRecordAttrs();
+
+ // Fetch holdings data for the metarecord
+ this.getHoldingsSummary(metabib.id(), orgId, orgDepth, true)
+ .then(holdingsSummary => {
+ summary.holdingsSummary = holdingsSummary;
+ observer.next(summary);
+ observer.complete();
+ });
+ });
+ });
+
+ return observable;
+ }
+
// Flesh the creator and editor fields.
// Handling this separately lets us pull from the cache and
// avoids the requirement that the main bib query use a staff
}
getHoldingsSummary(recordId: number,
- orgId: number, orgDepth: number): Promise<any> {
+ orgId: number, orgDepth: number, isMetarecord?: boolean): Promise<any> {
const holdingsSummary = [];
return this.unapi.getAsXmlDocument({
- target: 'bre',
+ target: isMetarecord ? 'mmr' : 'bre',
id: recordId,
extras: '{holdings_xml}',
format: 'holdings_xml',
params.joinOp = [];
params.matchOp = [];
- ['format', 'available', 'hasBrowseEntry', 'date1', 'date2', 'dateOp']
+ ['format', 'available', 'hasBrowseEntry', 'date1',
+ 'date2', 'dateOp', 'isMetarecord', 'fromMetarecord']
.forEach(field => {
if (ts[field]) {
params[field] = ts[field];
} else if (params.has('query')) {
// Scalars
- ['format', 'available', 'date1', 'date2', 'dateOp']
+ ['format', 'available', 'date1', 'date2',
+ 'dateOp', 'isMetarecord', 'fromMetarecord']
.forEach(field => {
if (params.has(field)) {
ts[field] = params.get(field);
let fullQuery;
if (ctx.identSearch.isSearchable()) {
- console.log('IDENT IS SEARCHABLE');
fullQuery = ctx.compileIdentSearchQuery();
} else {
fullQuery = ctx.compileTermSearchQuery();
console.debug(`search query: ${fullQuery}`);
let method = 'open-ils.search.biblio.multiclass.query';
+ if (ctx.termSearch.isSearchable()
+ && ctx.termSearch.isMetarecord
+ && !ctx.termSearch.fromMetarecord) {
+ method = 'open-ils.search.metabib.multiclass.query';
+ }
+
if (ctx.isStaff) {
method += '.staff';
}
ctx.org.root().ou_type().depth() :
ctx.searchOrg.ou_type().depth();
- return this.bibService.getBibSummary(
- ctx.currentResultIds(), ctx.searchOrg.id(), depth)
- .pipe(map(summary => {
+ // Term search, looking for metarecords, but no
+ // specific metarecord has been selected for display.
+ const isMeta = (
+ ctx.termSearch.isSearchable() &&
+ ctx.termSearch.isMetarecord &&
+ !ctx.termSearch.fromMetarecord
+ );
+
+ let observable: Observable<BibRecordSummary>;
+
+ if (isMeta) {
+ observable = this.bibService.getMetabibSummary(
+ ctx.currentResultIds(), ctx.searchOrg.id(), depth);
+ } else {
+ observable = this.bibService.getBibSummary(
+ ctx.currentResultIds(), ctx.searchOrg.id(), depth);
+ }
+
+ return observable.pipe(map(summary => {
// Responses are not necessarily returned in request-ID order.
- const idx = ctx.currentResultIds().indexOf(summary.record.id());
+ let idx;
+ if (isMeta) {
+ idx = ctx.currentResultIds().indexOf(summary.metabibId);
+ } else {
+ idx = ctx.currentResultIds().indexOf(summary.id);
+ }
+
if (ctx.result.records) {
// May be reset when quickly navigating results.
ctx.result.records[idx] = summary;
ccvmFilters: {[ccvmCode: string]: string[]};
facetFilters: FacetFilter[];
copyLocations: string[]; // ID's, but treated as strings in the UI.
- isMetarecord: boolean; // TODO
+
+ // True when searching for metarecords
+ isMetarecord: boolean;
+
+ // Filter results by records which link to this metarecord ID.
+ fromMetarecord: number;
+
hasBrowseEntry: string; // "entryId,fieldId"
date1: number;
date2: number;
this.date1 = null;
this.date2 = null;
this.dateOp = 'is';
+ this.fromMetarecord = null;
// Apply empty string values for each ccvm filter
this.ccvmFilters = {};
return (
this.query[0] !== ''
|| this.hasBrowseEntry !== ''
+ || this.fromMetarecord !== null
);
}
str += ` has_browse_entry(${ts.hasBrowseEntry})`;
}
+ if (ts.fromMetarecord) {
+ str += ` from_metarecord(${ts.fromMetarecord})`;
+ }
+
if (ts.format) {
str += ' format(' + ts.format + ')';
}
<div class="col-lg-12 font-weight-bold">
<!-- nbsp allows the column to take shape when no value exists -->
<a href="javascript:void(0)"
- (click)="navigatToRecord(summary.id)">
+ (click)="navigatToRecord(summary)">
{{summary.display.title || ' '}}
</a>
</div>
</div>
<div class="row pt-2">
<div class="col-lg-12">
- <!-- only shows the first icon format -->
- <span *ngIf="summary.attributes.icon_format && summary.attributes.icon_format[0]">
- <img class="pr-1"
- src="/images/format_icons/icon_format/{{summary.attributes.icon_format[0]}}.png"/>
- <span>{{iconFormatLabel(summary.attributes.icon_format[0])}}</span>
- </span>
+ <ng-container *ngIf="summary.attributes.icon_format && summary.attributes.icon_format[0]">
+ <ng-container *ngFor="let icon of summary.attributes.icon_format">
+ <span class="pr-1">
+ <img class="pr-1"
+ src="/images/format_icons/icon_format/{{icon}}.png"/>
+ <span>{{iconFormatLabel(icon)}}</span>
+ </span>
+ </ng-container>
+ </ng-container>
<span class='pl-1'>{{summary.display.edition}}</span>
<span class='pl-1'>{{summary.display.pubdate}}</span>
</div>
<div class="float-right">
<span>
<button (click)="placeHold()"
+ [disabled]="summary.metabibId"
class="btn btn-sm btn-success label-with-material-icon small-text-1">
<span class="material-icons">check</span>
<span i18n>Place Hold</span>
/**
* Propagate the search params along when navigating to each record.
*/
- navigatToRecord(id: number) {
+ navigatToRecord(summary: BibRecordSummary) {
const params = this.catUrl.toUrlParams(this.searchContext);
+ if (summary.metabibId) {
+ this.searchContext.termSearch.fromMetarecord = summary.metabibId;
+ this.staffCat.search();
+ return;
+ }
+
this.router.navigate(
- ['/staff/catalog/record/' + id], {queryParams: params});
+ ['/staff/catalog/record/' + summary.id], {queryParams: params});
}
toggleBasketEntry() {
</div>
<div class="checkbox pl-3">
<label>
- <input type="checkbox" [disabled]="true"
+ <input type="checkbox"
[(ngModel)]="context.termSearch.isMetarecord"/>
<span class="pl-1" i18n>Group Formats/Editions</span>
</label>
this.context.browseSearch.reset();
this.context.identSearch.reset();
this.context.termSearch.hasBrowseEntry = '';
+ this.context.termSearch.fromMetarecord = null;
this.staffCat.search();
break;