LP#1775466 Catalog uses display fields; more extensible guts
authorBill Erickson <berickxx@gmail.com>
Tue, 26 Jun 2018 21:26:48 +0000 (17:26 -0400)
committerBill Erickson <berickxx@gmail.com>
Tue, 26 Jun 2018 21:26:48 +0000 (17:26 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
16 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/core/idl.service.ts
Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts [new file with mode: 0644]
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/staff/catalog/catalog.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html
Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html
Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html
Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts

index b272b9f..bae69af 100644 (file)
@@ -3187,7 +3187,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="authority_links" reltype="has_many" key="bib" map="" class="abl"/>
                        <link field="subscriptions" reltype="has_many" key="record_entry" map="" class="ssub"/>
                        <link field="attrs" reltype="might_have" key="id" map="" class="mra"/>
-                       <link field="mattrs" reltype="might_have" key="id" map="" class="mraf"/>
+                       <link field="mattrs" reltype="has_many" key="id" map="" class="mraf"/>
                        <link field="source" reltype="has_a" key="id" map="" class="cbs"/>
                        <link field="display_entries" reltype="has_many" key="source" map="" class="mde"/>
                        <link field="flat_display_entries" reltype="has_many" key="source" map="" class="mfde"/>
index 9ec6c6e..3f9bbe8 100644 (file)
@@ -17,7 +17,7 @@ export interface IdlObject {
 @Injectable()
 export class IdlService {
 
-    classes = {}; // IDL class metadata
+    classes: any = {}; // IDL class metadata
     constructors = {}; // IDL instance generators
 
     /**
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts
new file mode 100644 (file)
index 0000000..cf080aa
--- /dev/null
@@ -0,0 +1,269 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {mergeMap} from 'rxjs/operators/mergeMap';
+import {from} from 'rxjs/observable/from';
+import {map} from 'rxjs/operators/map';
+import {OrgService} from '@eg/core/org.service';
+import {UnapiService} from '@eg/share/catalog/unapi.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+export const NAMESPACE_MAPS = {
+    'mods':     'http://www.loc.gov/mods/v3',
+    'biblio':   'http://open-ils.org/spec/biblio/v1',
+    'holdings': 'http://open-ils.org/spec/holdings/v1',
+    'indexing': 'http://open-ils.org/spec/indexing/v1'
+};
+
+export const HOLDINGS_XPATH = 
+    '/holdings:holdings/holdings:counts/holdings:count';
+
+@Injectable()
+export class BibRecordService {
+
+    // Cache of bib editor / creator objects
+    // Assumption is this list will be limited in size.
+    userCache: {[id: number]: IdlObject};
+
+    constructor(
+        private idl: IdlService,
+        private net: NetService,
+        private org: OrgService,
+        private unapi: UnapiService,
+        private pcrud: PcrudService
+    ) {
+        this.userCache = {};
+    }
+
+    // Avoid fetching the MARC blob by specifying which fields on the
+    // bre to select.  Note that fleshed fields are explicitly selected.
+    fetchableBreFields(): string[] {
+        return this.idl.classes.bre.fields
+            .filter(f => !f.virtual && f.name !== 'marc')
+            .map(f => f.name);
+    }
+
+    // Note responses are returned upon receipt, not necessarily in
+    // request ID order.
+    getBibSummaryBatch(bibIds: number[], 
+        orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
+
+        if (bibIds.length === 0) {
+            return from([]);
+        }
+
+        return this.pcrud.search('bre', {id: bibIds},
+            {   flesh: 1,
+                flesh_fields: {bre: ['flat_display_entries', 'mattrs']}, 
+                select: {bre : this.fetchableBreFields()}
+            },
+            {anonymous: true} // skip unneccesary auth
+        ).pipe(mergeMap(bib => {
+            const summary = new BibRecordSummary(bib, orgId, orgDepth);
+            summary.net = this.net; // inject
+            summary.ingest();
+            return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
+            .then(holdingsSummary => {
+                summary.holdingsSummary = holdingsSummary;
+                return summary;
+            });
+        }));
+    }
+
+    getBibSummary(bibId: number, 
+        orgId?: number, orgDepth?: number): Promise<BibRecordSummary> {
+
+        return this.pcrud.retrieve('bre', bibId,
+            {   flesh: 1,
+                flesh_fields: {bre: ['flat_display_entries', 'mattrs']}, 
+                select: {bre : this.fetchableBreFields()}
+            },
+            {anonymous: true} // skip unneccesary auth
+        ).pipe(mergeMap(bib => {
+            const summary = new BibRecordSummary(bib, orgId, orgDepth);
+            summary.net = this.net; // inject
+            summary.ingest();
+            return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
+            .then(holdingsSummary => {
+                summary.holdingsSummary = holdingsSummary;
+                return summary;
+            });
+        })).toPromise();
+    }
+
+    // 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 
+    // (VIEW_USER) auth token.
+    fleshBibUsers(records: IdlObject[]): Promise<void> {
+
+        const search = [];
+
+        records.forEach(rec => {
+            ['creator', 'editor'].forEach(field => {
+                const id = rec[field]();
+                if (Number.isInteger(id)) {
+                    if (this.userCache[id]) {
+                        rec[field](this.userCache[id]);
+                    } else if (!search.includes(id)) {
+                        search.push(id);
+                    }
+                }
+            });
+        });
+
+        if (search.length === 0) {
+            return Promise.resolve();
+        }
+
+        return this.pcrud.search('au', {id: search})
+        .pipe(map(user => {
+            this.userCache[user.id()] = user;
+            records.forEach(rec => {
+                if (user.id() === rec.creator()) {
+                    rec.creator(user);
+                }
+                if (user.id() === rec.editor()) {
+                    rec.editor(user);
+                }
+            });
+        })).toPromise();
+    }
+
+    getHoldingsSummary(recordId: number, 
+        orgId: number, orgDepth: number): Promise<any> {
+
+        const holdingsSummary = [];
+
+        return this.unapi.getAsXmlDocument({
+            target: 'bre',
+            id: recordId,
+            extras: '{holdings_xml}',
+            format: 'holdings_xml',
+            orgId: orgId,
+            depth: orgDepth
+        }).then(xmlDoc => {
+
+            // namespace resolver
+            const resolver: any = (prefix: string): string => {
+                return NAMESPACE_MAPS[prefix] || null;
+            };
+
+            // Extract the holdings data from the unapi xml doc
+            const result = xmlDoc.evaluate(HOLDINGS_XPATH, 
+                xmlDoc, resolver, XPathResult.ANY_TYPE, null);
+
+            let node;
+            while (node = result.iterateNext()) {
+                const counts = {type : node.getAttribute('type')};
+                ['depth', 'org_unit', 'transcendant',
+                    'available', 'count', 'unshadow'].forEach(field => {
+                    counts[field] = Number(node.getAttribute(field));
+                });
+                holdingsSummary.push(counts);
+            }
+
+            return holdingsSummary;
+        });
+    }
+}
+
+export class BibRecordSummary {
+
+    id: number; // == record.id() for convenience
+    orgId: number;
+    orgDepth: number;
+    record: IdlObject;
+    display: any;
+    attributes: any;
+    holdingsSummary: any;
+    holdCount: number;
+    bibCallNumber: string;
+    net: NetService;
+
+    constructor(record: IdlObject, orgId: number, orgDepth: number) {
+        this.id = record.id();
+        this.record = record;
+        this.orgId = orgId;
+        this.orgDepth = orgDepth;
+        this.display = {};
+        this.attributes = {};
+        this.bibCallNumber = null;
+    }
+
+    ingest() {
+        this.compileDisplayFields();
+        this.compileRecordAttrs();
+
+        // Normalize some data for JS consistency
+        this.record.creator(Number(this.record.creator()));
+        this.record.editor(Number(this.record.editor()));
+    }
+
+    compileDisplayFields() {
+        this.record.flat_display_entries().forEach(entry => {
+            if (entry.multi() === 't') {
+                if (this.display[entry.name()]) {
+                    this.display[entry.name()].push(entry.value());
+                } else {
+                    this.display[entry.name()] = [entry.value()];
+                }
+            } else {
+                this.display[entry.name()] = entry.value();
+            }
+        });
+    }
+
+    compileRecordAttrs() {
+        // Any attr can be multi-valued.
+        this.record.mattrs().forEach(attr => {
+            if (this.attributes[attr.attr()]) {
+                this.attributes[attr.attr()].push(attr.value());
+            } else {
+                this.attributes[attr.attr()] = [attr.value()];
+            }
+        });
+    }
+
+    // Get -> Set -> Return bib hold count
+    getHoldCount(): Promise<number> {
+
+        if (Number.isInteger(this.holdCount)) {
+            return Promise.resolve(this.holdCount);
+        }
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.bre.holds.count', this.id
+        ).toPromise().then(count => this.holdCount = count);
+    }
+
+    // Get -> Set -> Return bib-level call number
+    getBibCallNumber(): Promise<string> {
+
+        if (this.bibCallNumber !== null) {
+            return Promise.resolve(this.bibCallNumber);
+        }
+
+        // TODO labelClass = cat.default_classification_scheme YAOUS
+        const labelClass = 1;
+
+        return this.net.request(
+            'open-ils.cat',
+            'open-ils.cat.biblio.record.marc_cn.retrieve',
+            this.id, labelClass
+        ).toPromise().then(cnArray => {
+            if (cnArray && cnArray.length > 0) {
+                const key1 = Object.keys(cnArray[0])[0];
+                this.bibCallNumber = cnArray[0][key1];
+            } else {
+                this.bibCallNumber = '';
+            }
+            return this.bibCallNumber;
+        });
+    }
+}
+
+
+
index 0974cb1..253e3aa 100644 (file)
@@ -32,6 +32,8 @@ export class CatalogUrlService {
             offset: null
         };
 
+        params.org = context.searchOrg.id();
+
         params.limit = context.pager.limit;
         if (context.pager.offset) {
             params.offset = context.pager.offset;
@@ -46,6 +48,11 @@ export class CatalogUrlService {
             }
         });
 
+        if (params.identQuery) {
+            // Ident queries (e.g. tcn search) discards all remaining filters
+            return params;
+        }
+
         context.query.forEach((q, idx) => {
             ['query', 'fieldClass', 'joinOp', 'matchOp'].forEach(field => {
                 // Propagate all array-based fields regardless of
@@ -72,8 +79,6 @@ export class CatalogUrlService {
             }));
         });
 
-        params.org = context.searchOrg.id();
-
         return params;
     }
 
index 6c1c6b9..a5cfe1e 100644 (file)
@@ -1,11 +1,17 @@
 import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {mergeMap} from 'rxjs/operators/mergeMap';
+import {map} from 'rxjs/operators/map';
 import {OrgService} from '@eg/core/org.service';
 import {UnapiService} from '@eg/share/catalog/unapi.service';
-import {IdlObject} from '@eg/core/idl.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {CatalogSearchContext, CatalogSearchState} from './search-context';
+import {BibRecordService, BibRecordSummary} from './bib-record.service';
 
+// CCVM's we care about in a catalog context
+// Don't fetch them all because there are a lot.
 export const CATALOG_CCVM_FILTERS = [
     'item_type',
     'item_form',
@@ -15,30 +21,10 @@ export const CATALOG_CCVM_FILTERS = [
     'vr_format',
     'bib_level',
     'lit_form',
-    'search_format'
+    'search_format',
+    'icon_format'
 ];
 
-const MODS_XPATH_AUTO = {
-    title :  '/mods:mods/mods:titleInfo/mods:title',
-    author:  '/mods:mods/mods:name/mods:namePart',
-    edition: '/mods:mods/mods:originInfo/mods:edition',
-    pubdate: '/mods:mods/mods:originInfo/mods:dateIssued',
-    genre:   '/mods:mods/mods:genre'
-};
-
-const MODS_XPATH = {
-    extern:  '/mods:mods/biblio:extern',
-    copyCounts: '/mods:mods/holdings:holdings/holdings:counts/holdings:count',
-    attributes: '/mods:mods/indexing:attributes/indexing:field'
-};
-
-const NAMESPACE_MAPS = {
-    'mods':     'http://www.loc.gov/mods/v3',
-    'biblio':   'http://open-ils.org/spec/biblio/v1',
-    'holdings': 'http://open-ils.org/spec/holdings/v1',
-    'indexing': 'http://open-ils.org/spec/indexing/v1'
-};
-
 @Injectable()
 export class CatalogService {
 
@@ -52,10 +38,12 @@ export class CatalogService {
     lastFacetKey: string;
 
     constructor(
+        private idl: IdlService,
         private net: NetService,
         private org: OrgService,
         private unapi: UnapiService,
-        private pcrud: PcrudService
+        private pcrud: PcrudService,
+           private bibService: BibRecordService        
     ) {}
 
     search(ctx: CatalogSearchContext): Promise<void> {
@@ -99,28 +87,24 @@ export class CatalogService {
         result.ids.forEach((blob, idx) => ctx.addResultId(blob[0], idx));
     }
 
-    fetchBibSummaries(ctx: CatalogSearchContext): Promise<any> {
-        const promises = [];
+    // Appends records to the search result set as they arrive.
+    // Returns a void promise once all records have been retrieved
+    fetchBibSummaries(ctx: CatalogSearchContext): Promise<void> {
+
         const depth = ctx.global ?
             ctx.org.root().ou_type().depth() :
             ctx.searchOrg.ou_type().depth();
 
-        ctx.currentResultIds().forEach((recId, idx) => {
-            promises.push(
-                this.getBibSummary(recId, ctx.searchOrg.id(), depth)
-                .then(
-                    // idx maintains result sort order
-                    summary => {
-                        if (ctx.result.records) {
-                            // May be reset when quickly navigating results.
-                            ctx.result.records[idx] = summary;
-                        }
-                    }
-                )
-            );
-        });
-
-        return Promise.all(promises);
+        return this.bibService.getBibSummaryBatch(
+            ctx.currentResultIds(), ctx.searchOrg.id(), depth)
+        .pipe(map(summary => {
+            // Responses are not necessarily returned in request-ID order.
+            const idx = ctx.currentResultIds().indexOf(summary.record.id()); 
+            if (ctx.result.records) {
+                // May be reset when quickly navigating results.
+                ctx.result.records[idx] = summary;
+            }
+        })).toPromise();
     }
 
     fetchFacets(ctx: CatalogSearchContext): Promise<void> {
@@ -223,94 +207,4 @@ export class CatalogService {
             );
         });
     }
-
-
-    /**
-     * Bib record via UNAPI as mods (for now) with holdings summary
-     * and record attributes.
-     */
-    getBibSummary(bibId: number, orgId?: number, depth?: number): Promise<any> {
-        return new Promise((resolve, reject) => {
-            this.unapi.getAsXmlDocument({
-                target: 'bre',
-                id: bibId,
-                extras: '{bre.extern,holdings_xml,mra}',
-                format: 'mods32',
-                orgId: orgId,
-                depth: depth
-            }).then(xmlDoc => {
-                const summary = this.translateBibSummary(xmlDoc);
-                summary.id = bibId;
-                resolve(summary);
-            });
-        });
-    }
-
-    /**
-     * Probably don't want to require navigating the bare UNAPI
-     * blob in the template, plus that's quite a lot of stuff
-     * to sit in the scope / watch for changes.  Translate the
-     * UNAPI content into a more digestable form.
-     * TODO: Add display field support
-     */
-    translateBibSummary(xmlDoc: XMLDocument): any { // TODO: bib summary interface
-
-        const response: any = {
-            copyCounts : [],
-            ccvms : {}
-        };
-
-        const resolver: any = (prefix: string): string => {
-            return NAMESPACE_MAPS[prefix] || null;
-        };
-
-        Object.keys(MODS_XPATH_AUTO).forEach(key => {
-            const res = xmlDoc.evaluate(MODS_XPATH_AUTO[key], xmlDoc,
-                resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
-
-            const modsNode = res.singleNodeValue;
-            if (modsNode) {
-                response[key] = modsNode.textContent;
-            }
-        });
-
-        let result = xmlDoc.evaluate(MODS_XPATH.extern, xmlDoc,
-            resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
-
-        let node: any = result.singleNodeValue;
-        if (node) {
-            const attrs = node.attributes;
-            for (let i = attrs.length - 1; i >= 0; i--) {
-                response[attrs[i].name] = attrs[i].value;
-            }
-        }
-
-        result = xmlDoc.evaluate(MODS_XPATH.attributes, xmlDoc,
-            resolver, XPathResult.ANY_TYPE, null);
-
-        while (node = result.iterateNext()) {
-            response.ccvms[node.getAttribute('name')] = {
-                code : node.textContent,
-                label : node.getAttribute('coded-value')
-            };
-        }
-
-        result = xmlDoc.evaluate(MODS_XPATH.copyCounts, xmlDoc,
-            resolver, XPathResult.ANY_TYPE, null);
-
-        while (node = result.iterateNext()) {
-            const counts = {type : node.getAttribute('type')};
-            ['depth', 'org_unit', 'transcendant',
-                'available', 'count', 'unshadow'].forEach(field => {
-                counts[field] = Number(node.getAttribute(field));
-            });
-            response.copyCounts.push(counts);
-        }
-
-        response.creator = Number(response.creator);
-        response.editor = Number(response.editor);
-
-        // console.log(response);
-        return response;
-    }
 }
index 6f65fed..b2aa894 100644 (file)
@@ -3,6 +3,7 @@ import {StaffCommonModule} from '@eg/staff/common.module';
 import {GridModule} from '@eg/share/grid/grid.module';
 import {UnapiService} from '@eg/share/catalog/unapi.service';
 import {CatalogRoutingModule} from './routing.module';
+import {BibRecordService} from '@eg/share/catalog/bib-record.service';
 import {CatalogService} from '@eg/share/catalog/catalog.service';
 import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
 import {CatalogComponent} from './catalog.component';
@@ -44,6 +45,7 @@ import {HoldingsService} from '@eg/staff/share/holdings.service';
   ],
   providers: [
     UnapiService,
+    BibRecordService,
     CatalogService,
     CatalogUrlService,
     StaffCatalogService,
index 66fe2b9..f2b1ee4 100644 (file)
@@ -15,7 +15,7 @@
     </div>
   </div>
   <div id='staff-catalog-bib-summary-container' class='mt-1'>
-    <eg-bib-summary [bibSummary]="bibSummary">
+    <eg-bib-summary [bibSummary]="summary">
     </eg-bib-summary>
   </div>
   <div id='staff-catalog-bib-tabs-container' class='mt-3'>
index 437b440..2d51b05 100644 (file)
@@ -5,6 +5,7 @@ import {PcrudService} from '@eg/core/pcrud.service';
 import {IdlObject} from '@eg/core/idl.service';
 import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
 import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
 import {StaffCatalogService} from '../catalog.service';
 import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
 
@@ -16,7 +17,7 @@ export class RecordComponent implements OnInit {
 
     recordId: number;
     recordTab: string;
-    bibSummary: any;
+    summary: BibRecordSummary;
     searchContext: CatalogSearchContext;
     @ViewChild('recordTabs') recordTabs: NgbTabset;
 
@@ -24,6 +25,7 @@ export class RecordComponent implements OnInit {
         private router: Router,
         private route: ActivatedRoute,
         private pcrud: PcrudService,
+        private bib: BibRecordService,
         private cat: CatalogService,
         private staffCat: StaffCatalogService
     ) {}
@@ -62,27 +64,19 @@ export class RecordComponent implements OnInit {
         // Avoid re-fetching the same record summary during tab navigation.
         if (this.staffCat.currentDetailRecordSummary &&
             this.recordId === this.staffCat.currentDetailRecordSummary.id) {
-            this.bibSummary = this.staffCat.currentDetailRecordSummary;
+            this.summary = this.staffCat.currentDetailRecordSummary;
             return;
         }
 
-        this.bibSummary = null;
-        this.cat.getBibSummary(
+        this.summary = null;
+        this.bib.getBibSummary(
             this.recordId,
             this.searchContext.searchOrg.id(),
             this.searchContext.searchOrg.ou_type().depth()
         ).then(summary => {
-            this.bibSummary =
+            this.summary =
                 this.staffCat.currentDetailRecordSummary = summary;
-            this.pcrud.search('au', {id: [summary.creator, summary.editor]})
-            .subscribe(user => {
-                if (user.id() === summary.creator) {
-                    summary.creator = user;
-                }
-                if (user.id() === summary.editor) {
-                    summary.editor = user;
-                }
-            });
+            this.bib.fleshBibUsers([summary.record]);
         });
     }
 }
index d7b3945..d26e9a0 100644 (file)
@@ -9,9 +9,9 @@
     <div class="row">
       <div class="col-lg-1">
         <!-- TODO router links -->
-        <a href="./cat/catalog/record/{{bibSummary.id}}">
+        <a href="./cat/catalog/record/{{summary.id}}">
           <img style="height:80px"
-            src="/opac/extras/ac/jacket/small/r/{{bibSummary.id}}"/>
+            src="/opac/extras/ac/jacket/small/r/{{summary.id}}"/>
         </a>
       </div>
       <div class="col-lg-5">
@@ -22,8 +22,8 @@
               #{{index + 1 + searchContext.pager.offset}}
             </span>
             <a href="javascript:void(0)"
-              (click)="navigatToRecord(bibSummary.id)">
-              {{bibSummary.title || '&nbsp;'}}
+              (click)="navigatToRecord(summary.id)">
+              {{summary.display.title || '&nbsp;'}}
             </a>
           </div>
         </div>
           <div class="col-lg-12">
             <!-- nbsp allows the column to take shape when no value exists -->
             <a href="javascript:void(0)"
-              (click)="searchAuthor(bibSummary)">
-              {{bibSummary.author || '&nbsp;'}}
+              (click)="searchAuthor(summary)">
+              {{summary.display.author || '&nbsp;'}}
             </a>
           </div>
         </div>
         <div class="row pt-2">
           <div class="col-lg-12">
-            <span *ngIf="bibSummary.ccvms.icon_format">
-              <img class="pad-right-min"
-                src="/images/format_icons/icon_format/{{bibSummary.ccvms.icon_format.code}}.png"/>
-              <span>{{bibSummary.ccvms.icon_format.label}}</span>
+            <!-- only shows the first icon format -->
+            <span *ngIf="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>
-            <span style='pl-2'>{{bibSummary.edition}}</span>
-            <span style='pl-2'>{{bibSummary.pubdate}}</span>
+            <span class='pl-1'>{{summary.display.edition}}</span>
+            <span class='pl-1'>{{summary.display.pubdate}}</span>
           </div>
         </div>
       </div>
       <div class="col-lg-2">
         <div class="row" [ngClass]="{'pt-2':copyIndex > 0}" 
-          *ngFor="let copyCount of bibSummary.copyCounts; let copyIdx = index">
+          *ngFor="let copyCount of summary.holdingsSummary; let copyIdx = index">
           <div class="w-100" *ngIf="copyCount.type == 'staff'">
             <div class="float-left text-left w-50">
               <span class="pr-1">
       <div class="col-lg-1">
         <div class="row">
           <div class="w-100">
-            TCN: {{bibSummary.tcn_value}}
+            TCN: {{summary.record.tcn_value()}}
           </div>
         </div>
         <div class="row">
           <div class="w-100">
-            Holds: {{bibSummary.holdCount}}
+            Holds: {{summary.holdCount}}
           </div>
         </div>
       </div>
         <div class="row">
           <div class="col-lg-12">
             <div class="float-right small-text-1">
-              Created {{bibSummary.create_date | date:'shortDate'}} by
+              Created {{summary.create_date | date:'shortDate'}} by
               <!-- creator if fleshed after the initial data set is loaded -->
-              <a *ngIf="bibSummary.creator.usrname" target="_self" 
-                href="/eg/staff/circ/patron/{{bibSummary.creator.id()}}/checkout">
-                  {{bibSummary.creator.usrname()}}
+              <a *ngIf="summary.record.creator().usrname" target="_self" 
+                href="/eg/staff/circ/patron/{{summary.record.creator().id()}}/checkout">
+                  {{summary.record.creator().usrname()}}
               </a>
               <!-- add a spacer pending data to reduce page shuffle -->
-              <span *ngIf="!bibSummary.creator.usrname"> ... </span>
+              <span *ngIf="!summary.record.creator().usrname"> ... </span>
             </div>
           </div>
         </div>
         <div class="row pt-2">
           <div class="col-lg-12">
-            <div class="float-right small-text-1">
-              Edited {{bibSummary.edit_date | date:'shortDate'}} by
-              <a *ngIf="bibSummary.editor.usrname" target="_self" 
-                href="/eg/staff/circ/patron/{{bibSummary.editor.id()}}/checkout">
-                  {{bibSummary.editor.usrname()}}
+            <div class="float-right small-text-1" i18n>
+              Edited {{summary.edit_date | date:'shortDate'}} by
+              <a *ngIf="summary.record.editor().usrname" target="_self" 
+                href="/eg/staff/circ/patron/{{summary.record.editor().id()}}/checkout">
+                  {{summary.record.editor().usrname()}}
               </a>
-              <span *ngIf="!bibSummary.editor.usrname"> ... </span>
+              <span *ngIf="!summary.record.editor().usrname"> ... </span>
             </div>
           </div>
         </div>
index 352eeb1..bfcfd45 100644 (file)
@@ -3,6 +3,7 @@ import {Router} from '@angular/router';
 import {OrgService} from '@eg/core/org.service';
 import {NetService} from '@eg/core/net.service';
 import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
 import {CatalogSearchContext} from '@eg/share/catalog/search-context';
 import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
 import {StaffCatalogService} from '../catalog.service';
@@ -14,13 +15,14 @@ import {StaffCatalogService} from '../catalog.service';
 export class ResultRecordComponent implements OnInit {
 
     @Input() index: number;  // 0-index display row
-    @Input() bibSummary: any;
+    @Input() summary: BibRecordSummary;
     searchContext: CatalogSearchContext;
 
     constructor(
         private router: Router,
         private org: OrgService,
         private net: NetService,
+        private bib: BibRecordService,
         private cat: CatalogService,
         private catUrl: CatalogUrlService,
         private staffCat: StaffCatalogService
@@ -28,32 +30,35 @@ export class ResultRecordComponent implements OnInit {
 
     ngOnInit() {
         this.searchContext = this.staffCat.searchContext;
-        this.fleshHoldCount();
-    }
-
-    fleshHoldCount(): void {
-        this.net.request(
-            'open-ils.circ',
-            'open-ils.circ.bre.holds.count', this.bibSummary.id
-        ).subscribe(count => this.bibSummary.holdCount = count);
+        this.summary.getHoldCount();
     }
 
     orgName(orgId: number): string {
         return this.org.get(orgId).shortname();
     }
 
+    iconFormatLabel(code: string): string {
+        if (this.cat.ccvmMap) {
+            const ccvm = this.cat.ccvmMap.icon_format.filter(
+                format => format.code() === code)[0];
+            if (ccvm) {
+                return ccvm.search_label();
+            }
+        }
+    }
+
     placeHold(): void {
-        alert('Placing hold on bib ' + this.bibSummary.id);
+        alert('Placing hold on bib ' + this.summary.id);
     }
 
     addToList(): void {
-        alert('Adding to list for bib ' + this.bibSummary.id);
+        alert('Adding to list for bib ' + this.summary.id);
     }
 
-    searchAuthor(bibSummary: any) {
+    searchAuthor(summary: any) {
         this.searchContext.reset();
         this.searchContext.fieldClass = ['author'];
-        this.searchContext.query = [bibSummary.author];
+        this.searchContext.query = [summary.display.author];
         this.staffCat.search();
     }
 
index f357a6c..ee9ca8d 100644 (file)
@@ -17,9 +17,9 @@
                </div>
                <div class="col-lg-10">
                        <div *ngIf="searchContext.result">
-                               <div *ngFor="let bibSummary of searchContext.result.records; let idx = index">
-          <div *ngIf="bibSummary">
-                                         <eg-catalog-result-record [bibSummary]="bibSummary" [index]="idx">
+                               <div *ngFor="let summary of searchContext.result.records; let idx = index">
+          <div *ngIf="summary">
+                                         <eg-catalog-result-record [summary]="summary" [index]="idx">
                                          </eg-catalog-result-record>
           </div>
                                </div>
index dca1dfa..d9b7062 100644 (file)
@@ -3,6 +3,7 @@ import {Observable} from 'rxjs/Observable';
 import {map, switchMap, distinctUntilChanged} from 'rxjs/operators';
 import {ActivatedRoute, ParamMap} from '@angular/router';
 import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService} from '@eg/share/catalog/bib-record.service';
 import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
 import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
 import {PcrudService} from '@eg/core/pcrud.service';
@@ -25,6 +26,7 @@ export class ResultsComponent implements OnInit {
         private route: ActivatedRoute,
         private pcrud: PcrudService,
         private cat: CatalogService,
+        private bib: BibRecordService,
         private catUrl: CatalogUrlService,
         private staffCat: StaffCatalogService
     ) {}
@@ -70,37 +72,7 @@ export class ResultsComponent implements OnInit {
         if (!records || records.length === 0) { return; }
 
         // Flesh the creator / editor fields with the user object.
-        // Handle the user fleshing here (instead of record.component so
-        // we only need to grab one copy of each user.
-        const userIds: {[id: number]: boolean} = {};
-        records.forEach(recSum => {
-            if (this.userCache[recSum.creator]) {
-                recSum.creator = this.userCache[recSum.creator];
-            } else {
-                userIds[Number(recSum.creator)] = true;
-            }
-
-            if (this.userCache[recSum.editor]) {
-                recSum.editor = this.userCache[recSum.editor];
-            } else {
-                userIds[Number(recSum.editor)] = true;
-            }
-        });
-
-        if (!Object.keys(userIds).length) { return; }
-
-        this.pcrud.search('au', {id : Object.keys(userIds)})
-        .subscribe(usr => {
-            this.userCache[usr.id()] = usr;
-            records.forEach(recSum => {
-                if (recSum.creator === usr.id()) {
-                    recSum.creator = usr;
-                }
-                if (recSum.editor === usr.id()) {
-                    recSum.editor = usr;
-                }
-            });
-        });
+        this.bib.fleshBibUsers(records.map(r => r.record));
     }
 
     searchIsDone(): boolean {
index ae9170c..da54f4a 100644 (file)
@@ -48,13 +48,13 @@ TODO focus search input
             <input type="text" class="form-control"
               id='first-query-input'
               [(ngModel)]="searchContext.query[idx]"
-              (keyup.enter)="formEnter()"
+              (keyup.enter)="formEnter('query')"
               placeholder="Query..."/>
           </div>
           <div *ngIf="idx > 0">
             <input type="text" class="form-control"
               [(ngModel)]="searchContext.query[idx]"
-              (keyup.enter)="formEnter()"
+              (keyup.enter)="formEnter('query')"
               placeholder="Query..."/>
           </div>
         </div>
@@ -207,7 +207,7 @@ TODO focus search input
     <div class="col-lg-2">
       <input id='ident-query-input' type="text" class="form-control"
         [(ngModel)]="searchContext.identQuery"
-        (keyup.enter)="formEnter()"
+        (keyup.enter)="formEnter('ident')"
         placeholder="Numeric Query..."/>
     </div>
   </div>
index dbffe9c..aa9877a 100644 (file)
@@ -92,8 +92,30 @@ export class SearchFormComponent implements OnInit, AfterViewInit {
         this.searchContext.matchOp.splice(index, 1);
     }
 
-    formEnter() {
+    formEnter(source) {
         this.searchContext.pager.offset = 0;
+
+        switch (source) {
+
+            case 'query': // main search form query input
+
+                // Be sure a previous ident search does not take precedence
+                // over the newly entered/submitted search query
+                this.searchContext.identQuery = null;
+                break;
+
+            case 'ident': // identifier query input
+                const iq = this.searchContext.identQuery;
+                const qt = this.searchContext.identQueryType
+                if (iq) {
+                    // Ident queries ignore search-specific filters.
+                    this.searchContext.reset();
+                    this.searchContext.identQuery = iq;
+                    this.searchContext.identQueryType = qt;
+                }
+                break;
+        }
+        
         this.searchByForm();
     }
 
index 6626608..ac35bdb 100644 (file)
       <li class="list-group-item">
         <div class="d-flex">
           <div class="flex-1 font-weight-bold" i18n>Title:</div>
-          <div class="flex-3">{{summary.title}}</div>
+          <div class="flex-3">{{summary.display.title}}</div>
           <div class="flex-1 font-weight-bold pl-1" i18n>Edition:</div>
-          <div class="flex-1">{{summary.edition}}</div>
+          <div class="flex-1">{{summary.display.edition}}</div>
           <div class="flex-1 font-weight-bold" i18n>TCN:</div>
-          <div class="flex-1">{{summary.tcn_value}}</div>
+          <div class="flex-1">{{summary.record.tcn_value()}}</div>
           <div class="flex-1 font-weight-bold pl-1" i18n>Created By:</div>
-          <div class="flex-1" *ngIf="summary.creator.usrname">
-            {{summary.creator.usrname()}}
+          <div class="flex-1" *ngIf="summary.record.creator().usrname">
+            {{summary.record.creator().usrname()}}
           </div>
         </div>
       </li>
       <li class="list-group-item" *ngIf="expandDisplay">
         <div class="d-flex">
           <div class="flex-1 font-weight-bold" i18n>Author:</div>
-          <div class="flex-3">{{summary.author}}</div>
+          <div class="flex-3">{{summary.display.author}}</div>
           <div class="flex-1 font-weight-bold pl-1" i18n>Pubdate:</div>
-          <div class="flex-1">{{summary.pubdate}}</div>
+          <div class="flex-1">{{summary.display.pubdate}}</div>
           <div class="flex-1 font-weight-bold" i18n>Database ID:</div>
           <div class="flex-1">{{summary.id}}</div>
           <div class="flex-1 font-weight-bold pl-1" i18n>Last Edited By:</div>
-          <div class="flex-1" *ngIf="summary.editor.usrname">
-            {{summary.editor.usrname()}}
+          <div class="flex-1" *ngIf="summary.record.editor().usrname">
+            {{summary.record.editor().usrname()}}
           </div>
         </div>
       </li>
index 75a7993..aa7247d 100644 (file)
@@ -2,6 +2,7 @@ import {Component, OnInit, Input} from '@angular/core';
 import {NetService} from '@eg/core/net.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
 
 @Component({
   selector: 'eg-bib-summary',
@@ -17,15 +18,16 @@ export class BibSummaryComponent implements OnInit {
     @Input() recordId: number;
 
     // Otherwise, we'll use the provided bib summary object.
-    summary: any;
+    summary: BibRecordSummary;
     @Input() set bibSummary(s: any) {
         this.summary = s;
         if (this.initDone) {
-            this.fetchBibCallNumber();
+            this.summary.getBibCallNumber();
         }
     }
 
     constructor(
+        private bib: BibRecordService,
         private cat: CatalogService,
         private net: NetService,
         private pcrud: PcrudService
@@ -34,7 +36,7 @@ export class BibSummaryComponent implements OnInit {
     ngOnInit() {
         this.initDone = true;
         if (this.summary) {
-            this.fetchBibCallNumber();
+            this.summary.getBibCallNumber();
         } else {
             if (this.recordId) {
                 this.loadSummary();
@@ -43,40 +45,11 @@ export class BibSummaryComponent implements OnInit {
     }
 
     loadSummary(): void {
-        this.cat.getBibSummary(this.recordId).then(summary => {
+        this.bib.getBibSummary(this.recordId).then(summary => {
+            summary.getBibCallNumber();
+            this.bib.fleshBibUsers([summary.record]);
             this.summary = summary;
-            this.fetchBibCallNumber();
-
-            // Flesh the user data
-            this.pcrud.search('au', {id: [summary.creator, summary.editor]})
-            .subscribe(user => {
-                if (user.id() === summary.creator) {
-                    summary.creator = user;
-                }
-                if (user.id() === summary.editor) {
-                    summary.editor = user;
-                }
-            });
-        });
-    }
-
-    fetchBibCallNumber(): void {
-        if (!this.summary || this.summary.callNumber) {
-            return;
-        }
-
-        // TODO labelClass = cat.default_classification_scheme YAOUS
-        const labelClass = 1;
-
-        this.net.request(
-            'open-ils.cat',
-            'open-ils.cat.biblio.record.marc_cn.retrieve',
-            this.summary.id, labelClass
-        ).subscribe(cnArray => {
-            if (cnArray && cnArray.length > 0) {
-                const key1 = Object.keys(cnArray[0])[0];
-                this.summary.callNumber = cnArray[0][key1];
-            }
+            console.log(this.summary.display);
         });
     }
 }