]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/marc-edit/tagtable.service.ts
LP1849212: Users can attach brief bib records and e-resources to courses
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / marc-edit / tagtable.service.ts
1 import {Injectable, EventEmitter} from '@angular/core';
2 import {Observable} from 'rxjs';
3 import {map, tap, distinct} from 'rxjs/operators';
4 import {StoreService} from '@eg/core/store.service';
5 import {IdlObject} from '@eg/core/idl.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {NetService} from '@eg/core/net.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
10
11 const DEFAULT_MARC_FORMAT = 'marc21';
12
13 interface TagTableSelector {
14     marcFormat?: string;
15     marcRecordType: 'biblio' | 'authority' | 'serial';
16
17     // MARC record fixed field "Type" value.
18     ffType: string;
19 }
20
21 export class TagTable {
22
23     store: StoreService;
24     auth: AuthService;
25     net: NetService;
26     pcrud: PcrudService;
27
28     selector: TagTableSelector;
29
30     // Current set of tags in list and map form.
31     tagMap: {[tag: string]: any};
32     ffPosTable: any;
33     ffValueTable: any;
34     fieldTags: ContextMenuEntry[];
35
36     // Cache of compiled, sorted, munged data.  Stuff the UI requests
37     // frequently for selectors, etc.
38     cache: {[valueType: string]: {[which: string]: any}} = {
39         indicators: {},
40         sfcodes: {},
41         sfvalues: {},
42         ffvalues: {}
43     };
44
45     constructor(
46         store: StoreService,
47         auth: AuthService,
48         net: NetService,
49         pcrud: PcrudService,
50         selector: TagTableSelector
51     ) {
52         this.store = store;
53         this.auth = auth;
54         this.net = net;
55         this.pcrud = pcrud;
56         this.selector = selector;
57     }
58
59
60     load(): Promise<any> {
61         return Promise.all([
62             this.loadTagTable(),
63             this.getFfPosTable(),
64             this.getFfValueTable(),
65         ]);
66     }
67
68     // Various data needs munging for display.  Cached the modified
69     // values since they are refernced repeatedly by the UI code.
70     fromCache(dataType: string, which?: string, which2?: string): ContextMenuEntry[] {
71         const part1 = this.cache[dataType][which];
72         if (which2) {
73             if (part1) {
74                 return part1[which2];
75             }
76         } else {
77             return part1;
78         }
79     }
80
81     toCache(dataType: string, which: string,
82         which2: string, values: ContextMenuEntry[]): ContextMenuEntry[] {
83         const base = this.cache[dataType];
84         const part1 = base[which];
85
86         if (which2) {
87             if (!base[which]) { base[which] = {}; }
88             base[which][which2] = values;
89         } else {
90             base[which] = values;
91         }
92
93         return values;
94     }
95
96     getFfPosTable(): Promise<any> {
97         const storeKey = 'FFPosTable_' + this.selector.ffType;
98
99         if (this.ffPosTable) {
100             return Promise.resolve(this.ffPosTable);
101         }
102
103         this.ffPosTable = this.store.getLocalItem(storeKey);
104
105         if (this.ffPosTable) {
106             return Promise.resolve(this.ffPosTable);
107         }
108
109         return this.net.request(
110             'open-ils.fielder', 'open-ils.fielder.cmfpm.atomic',
111             {query: {tag: {'!=' : '006'}, rec_type: this.selector.ffType}}
112
113         ).toPromise().then(table => {
114             this.store.setLocalItem(storeKey, table);
115             return this.ffPosTable = table;
116         });
117     }
118
119     // ffType is the fixed field Type value. BKS, AUT, etc.
120     // See config.marc21_rec_type_map
121     getFfValueTable(): Promise<any> {
122
123         const storeKey = 'FFValueTable_' + this.selector.ffType;
124
125         if (this.ffValueTable) {
126             return Promise.resolve(this.ffValueTable);
127         }
128
129         this.ffValueTable = this.store.getLocalItem(storeKey);
130
131         if (this.ffValueTable) {
132             return Promise.resolve(this.ffValueTable);
133         }
134
135         return this.net.request(
136             'open-ils.cat',
137             'open-ils.cat.biblio.fixed_field_values.by_rec_type',
138             this.selector.ffType
139
140         ).toPromise().then(table => {
141             this.store.setLocalItem(storeKey, table);
142             return this.ffValueTable = table;
143         });
144     }
145
146     loadTagTable(): Promise<any> {
147
148         const sel = this.selector;
149
150         const cacheKey =
151             `current_tag_table_${sel.marcFormat}_${sel.marcRecordType}`;
152
153         this.tagMap = this.store.getLocalItem(cacheKey);
154
155         if (this.tagMap) {
156             return Promise.resolve(this.tagMap);
157         }
158
159         return this.fetchTagTable().then(_ => {
160             this.store.setLocalItem(cacheKey, this.tagMap);
161             return this.tagMap;
162         });
163     }
164
165     fetchTagTable(): Promise<any> {
166         this.tagMap = [];
167         return this.net.request(
168             'open-ils.cat',
169             'open-ils.cat.tag_table.all.retrieve.local',
170             this.auth.token(), this.selector.marcFormat,
171             this.selector.marcRecordType
172         ).pipe(tap(tagData => {
173             this.tagMap[tagData.tag] = tagData;
174         })).toPromise();
175     }
176
177     getSubfieldCodes(tag: string): ContextMenuEntry[] {
178         if (!tag || !this.tagMap[tag]) { return null; }
179
180         const cached = this.fromCache('sfcodes', tag);
181
182         const list = this.tagMap[tag].subfields.map(sf => ({
183             value: sf.code,
184             label: `${sf.code}: ${sf.description}`
185         }))
186         .sort((a, b) => a.label < b.label ? -1 : 1);
187
188         return this.toCache('sfcodes', tag, null, list);
189     }
190
191     getSubfieldLabel(tag: string, sfCode: string): string {
192         if (!tag || !this.tagMap[tag]) { return null; }
193         const subfieldResults = this.tagMap[tag].subfields.filter(sf => sf.code === sfCode);
194         return subfieldResults.length ? subfieldResults[0].description : null;
195     }
196
197
198     getFieldTags(): ContextMenuEntry[] {
199
200         if (!this.fieldTags) {
201             this.fieldTags = Object.keys(this.tagMap)
202             .filter(tag => Boolean(this.tagMap[tag]))
203             .map(tag => ({
204                 value: tag,
205                 label: `${tag}: ${this.tagMap[tag].name}`
206             }))
207             .sort((a, b) => a.label < b.label ? -1 : 1);
208         }
209
210         return this.fieldTags;
211     }
212
213     getSubfieldValues(tag: string, sfCode: string): ContextMenuEntry[] {
214         if (!tag || !this.tagMap[tag]) { return []; }
215
216         const cached = this.fromCache('sfvalues', tag, sfCode);
217         if (cached) { return cached; }
218
219         const list: ContextMenuEntry[] = [];
220
221         this.tagMap[tag].subfields
222         .filter(sf =>
223             sf.code === sfCode && sf.hasOwnProperty('value_list'))
224         .forEach(sf => {
225             sf.value_list.forEach(value => {
226
227                 let label = value.description || value.code;
228                 const code = value.code || label;
229                 if (code !== label) { label = `${code}: ${label}`; }
230
231                 list.push({value: code, label: label});
232             });
233         });
234
235         return this.toCache('sfvalues', tag, sfCode, list);
236     }
237
238     getIndicatorValues(tag: string, which: 'ind1' | 'ind2'): ContextMenuEntry[] {
239         if (!tag || !this.tagMap[tag]) { return; }
240
241         const cached = this.fromCache('indicators', tag, which);
242         if (cached) { return cached; }
243
244         let values = this.tagMap[tag][which];
245         if (!values) { return; }
246
247         values = values.map(value => ({
248             value: value.code,
249             label: `${value.code}: ${value.description}`
250         }))
251         .sort((a, b) => a.label < b.label ? -1 : 1);
252
253         return this.toCache('indicators', tag, which, values);
254     }
255
256
257     getFfFieldMeta(fieldCode: string): Promise<IdlObject> {
258         return this.getFfPosTable().then(table => {
259
260             // Best I can tell, the AngJS MARC editor stores the
261             // full POS table for all record types in every copy of
262             // the table, hence the seemingly extraneous check in ffType.
263             return table.filter(
264                 field =>
265                     field.fixed_field === fieldCode
266                  && field.rec_type === this.selector.ffType
267             )[0];
268         });
269     }
270
271
272     // Assumes getFfPosTable and getFfValueTable have already been
273     // invoked for the requested record type.
274     getFfValues(fieldCode: string): ContextMenuEntry[] {
275
276         const cached = this.fromCache('ffvalues', fieldCode);
277         if (cached) { return cached; }
278
279         let values = this.ffValueTable;
280
281         if (!values || !values[fieldCode]) { return null; }
282
283         // extract the canned set of possible values for our
284         // fixed field.  Ignore those whose value exceeds the
285         // specified field length.
286         values = values[fieldCode]
287             .filter(val => val[0].length <= val[2])
288             .map(val => ({value: val[0], label: `${val[0]}: ${val[1]}`}))
289             .sort((a, b) => a.label < b.label ? -1 : 1);
290
291         return this.toCache('ffvalues', fieldCode, null, values);
292     }
293
294 }
295
296 @Injectable()
297 export class TagTableService {
298
299     tagTables: {[marcRecordType: string]: TagTable} = {};
300     controlledBibTags: string[];
301
302     constructor(
303         private store: StoreService,
304         private auth: AuthService,
305         private net: NetService,
306         private pcrud: PcrudService,
307     ) {}
308
309     loadTags(selector: TagTableSelector): Promise<TagTable> {
310         if (!selector.marcFormat) {
311             selector.marcFormat = DEFAULT_MARC_FORMAT;
312         }
313
314         // Tag tables of a given marc record type are identical.
315         if (this.tagTables[selector.marcRecordType]) {
316             return Promise.resolve(this.tagTables[selector.marcRecordType]);
317         }
318
319         const tt = new TagTable(
320             this.store, this.auth, this.net, this.pcrud, selector);
321
322         this.tagTables[selector.marcRecordType] = tt;
323
324         return tt.load().then(_ => tt);
325     }
326
327     getControlledBibTags(): Promise<string[]> {
328         if (this.controlledBibTags) {
329             return Promise.resolve(this.controlledBibTags);
330         }
331
332         this.controlledBibTags = [];
333         return this.pcrud.retrieveAll('acsbf', {select: ['tag']})
334         .pipe(
335             map(field => field.tag()),
336             distinct(),
337             map(tag => this.controlledBibTags.push(tag))
338         ).toPromise().then(_ => this.controlledBibTags);
339     }
340 }
341
342
343