LP1806087 Group formats and editions (WIP)
authorBill Erickson <berickxx@gmail.com>
Tue, 18 Dec 2018 18:15:56 +0000 (13:15 -0500)
committerBill Erickson <berickxx@gmail.com>
Mon, 7 Jan 2019 14:58:44 +0000 (09:58 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts
Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts
Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html

index 1be2c88..e934ae6 100644 (file)
@@ -3899,18 +3899,25 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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">
@@ -3943,7 +3950,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             </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"/>
@@ -3953,6 +3960,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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" 
index 19924d9..c29c33a 100644 (file)
@@ -22,6 +22,7 @@ export const HOLDINGS_XPATH =
 
 export class BibRecordSummary {
     id: number; // == record.id() for convenience
+    metabibId: number; // If present, this is a metabib summary
     orgId: number;
     orgDepth: number;
     record: IdlObject;
@@ -69,7 +70,10 @@ export class BibRecordSummary {
         // 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()];
             }
@@ -83,9 +87,16 @@ export class BibRecordSummary {
             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);
     }
 
@@ -133,7 +144,7 @@ export class BibRecordService {
     }
 
     // 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')
@@ -169,6 +180,75 @@ export class BibRecordService {
         }));
     }
 
+    // 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> {
+
+        const bibIds = metabib.source_maps().map(map => map.source());
+        let observer;
+        const observable = 
+            new Observable<BibRecordSummary>(o => observer = o);
+
+        let master;
+        const summaries = [];
+
+        // NOTE if we only need to augment with 'mattrs' it would be
+        // faster to just grab those than to fetch the full bib-summary
+        // for related records.  Revisit.
+
+        this.getBibSummary(bibIds, orgId, orgDepth).subscribe(
+            sum => {
+                summaries.push(sum);
+                if (sum.id === Number(metabib.master_record())) {
+                    master = sum;
+                    master.metabibId = metabib.id();
+                }
+            },
+            err => {},
+            ()  => {
+                summaries.forEach(sum => {
+                    if (sum.id !== master.id) {
+                        master.record.mattrs(
+                            master.record.mattrs().concat(sum.record.mattrs()))
+                    }
+                });
+
+                // Recompile attributes now that the data has been augmented
+                master.compileRecordAttrs();
+
+                // Fetch holdings data for the metarecord
+                this.getHoldingsSummary(metabib.id(), orgId, orgDepth, true)
+                .then(holdingsSummary => {
+                    master.holdingsSummary = holdingsSummary;
+                    observer.next(master);
+                    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
@@ -209,12 +289,12 @@ export class BibRecordService {
     }
 
     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',
index 60c3128..1b85d71 100644 (file)
@@ -80,7 +80,8 @@ export class CatalogUrlService {
             params.joinOp = [];
             params.matchOp = [];
 
-            ['format', 'available', 'hasBrowseEntry', 'date1', 'date2', 'dateOp']
+            ['format', 'available', 'hasBrowseEntry', 
+                'date1', 'date2', 'dateOp', 'isMetarecord']
             .forEach(field => {
                 if (ts[field]) {
                     params[field] = ts[field];
@@ -195,7 +196,7 @@ export class CatalogUrlService {
         } else if (params.has('query')) {
 
             // Scalars
-            ['format', 'available', 'date1', 'date2', 'dateOp']
+            ['format', 'available', 'date1', 'date2', 'dateOp', 'isMetarecord']
             .forEach(field => {
                 if (params.has(field)) {
                     ts[field] = params.get(field);
index 93eb964..b210103 100644 (file)
@@ -114,7 +114,6 @@ export class CatalogService {
         let fullQuery;
 
         if (ctx.identSearch.isSearchable()) {
-            console.log('IDENT IS SEARCHABLE');
             fullQuery = ctx.compileIdentSearchQuery();
         } else {
             fullQuery = ctx.compileTermSearchQuery();
@@ -123,6 +122,10 @@ export class CatalogService {
         console.debug(`search query: ${fullQuery}`);
 
         let method = 'open-ils.search.biblio.multiclass.query';
+        if (ctx.termSearch.isSearchable() && ctx.termSearch.isMetarecord) {
+            method = 'open-ils.search.metabib.multiclass.query';
+        }
+
         if (ctx.isStaff) {
             method += '.staff';
         }
@@ -166,11 +169,30 @@ export class CatalogService {
             ctx.org.root().ou_type().depth() :
             ctx.searchOrg.ou_type().depth();
 
-        return this.bibService.getBibSummary(
-            ctx.currentResultIds(), ctx.searchOrg.id(), depth)
-        .pipe(map(summary => {
+        const isMeta = (
+            ctx.termSearch.isSearchable() && 
+            ctx.termSearch.isMetarecord
+        );
+
+        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;
index e4d65d4..c4bb493 100644 (file)
@@ -122,7 +122,7 @@ export class CatalogTermContext {
     ccvmFilters: {[ccvmCode: string]: string[]};
     facetFilters: FacetFilter[];
     copyLocations: string[]; // ID's, but treated as strings in the UI.
-    isMetarecord: boolean; // TODO
+    isMetarecord: boolean;
     hasBrowseEntry: string; // "entryId,fieldId"
     date1: number;
     date2: number;
index 54b79c1..928d1a1 100644 (file)
           </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>
index 8b6cc8c..4327e8d 100644 (file)
@@ -112,7 +112,7 @@ TODO focus search input
                 </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>