]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/core/org.service.ts
cc8e4cd7b2996ddb0d9c15e2e337d233e5464a28
[Evergreen.git] / Open-ILS / src / eg2 / src / app / core / org.service.ts
1 import {Injectable} from '@angular/core';
2 import {Observable} from 'rxjs';
3 import {tap} from 'rxjs/operators';
4 import {IdlObject, IdlService} from './idl.service';
5 import {NetService} from './net.service';
6 import {AuthService} from './auth.service';
7 import {PcrudService} from './pcrud.service';
8 import {DbStoreService} from './db-store.service';
9
10 type OrgNodeOrId = number | IdlObject;
11
12 interface OrgFilter {
13     canHaveUsers?: boolean;
14     canHaveVolumes?: boolean;
15     opacVisible?: boolean;
16     inList?: number[];
17     notInList?: number[];
18 }
19
20 interface OrgSettingsBatch {
21     [key: string]: any;
22 }
23
24 @Injectable({providedIn: 'root'})
25 export class OrgService {
26
27     private orgList: IdlObject[] = [];
28     private orgTree: IdlObject; // root node + children
29     private orgMap: {[id: number]: IdlObject} = {};
30     private settingsCache: OrgSettingsBatch = {};
31
32     private orgTypeMap: {[id: number]: IdlObject} = {};
33     private orgTypeList: IdlObject[] = [];
34
35     constructor(
36         private db: DbStoreService,
37         private net: NetService,
38         private auth: AuthService,
39         private pcrud: PcrudService
40     ) {}
41
42     get(nodeOrId: OrgNodeOrId): IdlObject {
43         if (typeof nodeOrId === 'object') {
44             return nodeOrId;
45         }
46         return this.orgMap[nodeOrId];
47     }
48
49     list(): IdlObject[] {
50         return this.orgList;
51     }
52
53     typeList(): IdlObject[] {
54         return this.orgTypeList;
55     }
56
57     typeMap(): {[id: number]: IdlObject} {
58         return this.orgTypeMap;
59     }
60
61     /**
62      * Returns a list of org units that match the selected criteria.
63      * All filters must match for an org to be included in the result set.
64      * Unset filter options are ignored.
65      */
66     filterList(filter: OrgFilter, asId?: boolean): any[] {
67         const list = [];
68         this.list().forEach(org => {
69
70             const chu = filter.canHaveUsers;
71             if (chu && !this.canHaveUsers(org)) { return; }
72             if (chu === false && this.canHaveUsers(org)) { return; }
73
74             const chv = filter.canHaveVolumes;
75             if (chv && !this.canHaveVolumes(org)) { return; }
76             if (chv === false && this.canHaveVolumes(org)) { return; }
77
78             const ov = filter.opacVisible;
79             if (ov && !this.opacVisible(org)) { return; }
80             if (ov === false && this.opacVisible(org)) { return; }
81
82             if (filter.inList && !filter.inList.includes(org.id())) {
83                 return;
84             }
85
86             if (filter.notInList && filter.notInList.includes(org.id())) {
87                 return;
88             }
89
90             // All filter tests passed.  Add it to the list
91             list.push(asId ? org.id() : org);
92         });
93
94         return list;
95     }
96
97     tree(): IdlObject {
98         return this.orgTree;
99     }
100
101     // get the root OU
102     root(): IdlObject {
103         return this.orgList[0];
104     }
105
106     // list of org_unit objects or IDs for ancestors + me
107     ancestors(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
108         let node = this.get(nodeOrId);
109         if (!node) { return []; }
110         const nodes = [node];
111         while ( (node = this.get(node.parent_ou())) ) {
112             nodes.push(node);
113         }
114         if (asId) {
115             return nodes.map(n => n.id());
116         }
117         return nodes;
118     }
119
120     // tests that a node can have users
121     canHaveUsers(nodeOrId): boolean {
122         return this.get(nodeOrId).ou_type().can_have_users() === 't';
123     }
124
125     // tests that a node can have volumes
126     canHaveVolumes(nodeOrId): boolean {
127         return this
128             .get(nodeOrId)
129             .ou_type()
130             .can_have_vols() === 't';
131     }
132
133     opacVisible(nodeOrId): boolean {
134         return this.get(nodeOrId).opac_visible() === 't';
135     }
136
137     // list of org_unit objects  or IDs for me + descendants
138     descendants(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
139         const node = this.get(nodeOrId);
140         if (!node) { return []; }
141         const nodes = [];
142         const descend = (n) => {
143             nodes.push(n);
144             n.children().forEach(descend);
145         };
146         descend(node);
147         if (asId) {
148             return nodes.map(n => n.id());
149         }
150         return nodes;
151     }
152
153     // list of org_unit objects or IDs for ancestors + me + descendants
154     fullPath(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
155         const list = this.ancestors(nodeOrId, false).concat(
156           this.descendants(nodeOrId, false).slice(1));
157         if (asId) {
158             return list.map(n => n.id());
159         }
160         return list;
161     }
162
163     sortTree(sortField?: string, node?: IdlObject): void {
164         if (!sortField) { sortField = 'shortname'; }
165         if (!node) { node = this.orgTree; }
166         node.children(
167             node.children().sort((a, b) => {
168                 return a[sortField]() < b[sortField]() ? -1 : 1;
169             })
170         );
171         node.children().forEach(n => this.sortTree(sortField, n));
172     }
173
174     absorbTree(node?: IdlObject): void {
175         if (!node) {
176             node = this.orgTree;
177             this.orgMap = {};
178         }
179         this.orgMap[node.id()] = node;
180         this.orgList.push(node);
181
182         node.children().forEach(c => this.absorbTree(c));
183     }
184
185     /**
186      * Grabs all of the org units from the server, chops them up into
187      * various shapes, then returns an "all done" promise.
188      */
189     fetchOrgs(): Promise<void> {
190
191         // Grab org types separately so we are guaranteed to fetch types
192         // that are not yet in use by an org unit.
193         return this.pcrud.retrieveAll(
194             'aout', {}, {anonymous: true, atomic: true}).toPromise()
195         .then(types => {
196             this.orgTypeList = types;
197             types.forEach(t => this.orgTypeMap[Number(t.id())] = t);
198
199             return this.pcrud.search('aou', {parent_ou : null},
200                 {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}},
201                 {anonymous : true}
202             ).toPromise()
203         })
204
205         .then(tree => {
206             // ingest tree, etc.
207             this.orgTree = tree;
208             this.absorbTree();
209         });
210     }
211
212     private appendSettingsFromCache(names: string[], batch: OrgSettingsBatch) {
213         names.forEach(name => {
214             if (name in this.settingsCache) {
215                 batch[name] = this.settingsCache[name];
216             }
217         });
218     }
219
220     // Pulls setting values from IndexedDB.
221     // Update local cache with any values found.
222     private appendSettingsFromDb(names: string[],
223         batch: OrgSettingsBatch): Promise<OrgSettingsBatch> {
224
225         if (names.length === 0) { return Promise.resolve(batch); }
226
227         return this.db.request({
228             schema: 'cache',
229             table: 'Setting',
230             action: 'selectWhereIn',
231             field: 'name',
232             value: names
233         }).then(settings => {
234
235             // array of key => JSON-string objects
236             settings.forEach(setting => {
237                 const value = JSON.parse(setting.value);
238                 // propagate to local cache as well
239                 batch[setting.name] = this.settingsCache[setting.name] = value;
240             });
241
242             return batch;
243         }).catch(_ => batch);
244     }
245
246     // Add values for the list of named settings from the 'batch' to
247     // IndexedDB, copying the local cache as well.
248     private addSettingsToDb(names: string[],
249         batch: OrgSettingsBatch): Promise<OrgSettingsBatch> {
250
251         const rows = [];
252         names.forEach(name => {
253             // Anything added to the db should also be cached locally.
254             this.settingsCache[name] = batch[name];
255             rows.push({name: name, value: JSON.stringify(batch[name])});
256         });
257
258         if (rows.length === 0) { return Promise.resolve(batch); }
259
260         return this.db.request({
261             schema: 'cache',
262             table: 'Setting',
263             action: 'insertOrReplace',
264             rows: rows
265         }).then(_ => batch).catch(_ => batch);
266     }
267
268     /**
269      * Append the named settings from the network to the in-progress
270      * batch of settings.  'auth' is null for anonymous lookup.
271      */
272     private appendSettingsFromNet(orgId: number, names: string[],
273         batch: OrgSettingsBatch, auth?: string): Promise<OrgSettingsBatch> {
274
275         if (names.length === 0) { return Promise.resolve(batch); }
276
277         return this.net.request(
278             'open-ils.actor',
279             'open-ils.actor.ou_setting.ancestor_default.batch',
280             orgId, names, auth
281
282         ).pipe(tap(settings => {
283             Object.keys(settings).forEach(key => {
284                 const val = settings[key]; // null or hash
285                 batch[key] = val ? val.value : null;
286             });
287         })).toPromise().then(_ => batch);
288     }
289
290     // Given a set of setting names and an in-progress settings batch,
291     // return the list of names which are not yet represented in the ,
292     // indicating their data needs to be fetched from the next layer up
293     // (cache, network, etc.).
294     settingNamesRemaining(names: string[], settings: OrgSettingsBatch): string[] {
295         return names.filter(name => !(name in settings));
296     }
297
298     // Returns a key/value batch of org unit settings.
299     // Cacheable settings (orgId === here) are pulled from local cache,
300     // then IndexedDB, then the network.  Non-cacheable settings are
301     // fetched from the network each time.
302     settings(name: string | string[],
303         orgId?: number, anonymous?: boolean): Promise<OrgSettingsBatch> {
304
305         let names = [].concat(name);
306         let auth: string = null;
307         let useCache = false;
308         const batch: OrgSettingsBatch = {};
309
310         if (names.length === 0) { return Promise.resolve(batch); }
311
312         if (this.auth.user()) {
313             if (orgId) {
314                 useCache = Number(orgId) === Number(this.auth.user().ws_ou());
315             } else {
316                 orgId = this.auth.user().ws_ou();
317                 useCache = true;
318             }
319
320             // avoid passing auth token when anonymous is requested.
321             if (!anonymous) {
322                 auth = this.auth.token();
323             }
324
325         } else if (!anonymous) {
326             console.warn('Attempt to fetch org setting(s)',
327                 name, 'in non-anonymous mode without an authtoken');
328             return Promise.resolve({});
329         }
330
331         if (!useCache) {
332             return this.appendSettingsFromNet(orgId, names, batch, auth);
333         }
334
335         this.appendSettingsFromCache(names, batch);
336         names = this.settingNamesRemaining(names, batch);
337
338         return this.appendSettingsFromDb(names, batch)
339         .then(_ => {
340
341             names = this.settingNamesRemaining(names, batch);
342
343             return this.appendSettingsFromNet(orgId, names, batch, auth)
344             .then(__ => this.addSettingsToDb(names, batch));
345         });
346     }
347
348     // remove setting values cached in the indexeddb settings table.
349     clearCachedSettings(): Promise<any> {
350         return this.db.request({
351             schema: 'cache',
352             table: 'Setting',
353             action: 'deleteAll'
354         }).catch(_ => null);
355     }
356 }