2a2a59dfd773587826731ccb302b6db48006c677
[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 {IdlObject, IdlService} from './idl.service';
4 import {NetService} from './net.service';
5 import {AuthService} from './auth.service';
6 import {PcrudService} from './pcrud.service';
7
8 type OrgNodeOrId = number | IdlObject;
9
10 interface OrgFilter {
11     canHaveUsers?: boolean;
12     canHaveVolumes?: boolean;
13     opacVisible?: boolean;
14     inList?: number[];
15     notInList?: number[];
16 }
17
18 interface OrgSettingsBatch {
19     [key: string]: any;
20 }
21
22 @Injectable({providedIn: 'root'})
23 export class OrgService {
24
25     private orgList: IdlObject[] = [];
26     private orgTree: IdlObject; // root node + children
27     private orgMap: {[id: number]: IdlObject} = {};
28     private settingsCache: OrgSettingsBatch = {};
29
30     private orgTypeMap: {[id: number]: IdlObject} = {};
31     private orgTypeList: IdlObject[] = [];
32
33     constructor(
34         private net: NetService,
35         private auth: AuthService,
36         private pcrud: PcrudService
37     ) {}
38
39     get(nodeOrId: OrgNodeOrId): IdlObject {
40         if (typeof nodeOrId === 'object') {
41             return nodeOrId;
42         }
43         return this.orgMap[nodeOrId];
44     }
45
46     list(): IdlObject[] {
47         return this.orgList;
48     }
49
50     typeList(): IdlObject[] {
51         return this.orgTypeList;
52     }
53
54     typeMap(): {[id: number]: IdlObject} {
55         return this.orgTypeMap;
56     }
57
58     /**
59      * Returns a list of org units that match the selected criteria.
60      * All filters must match for an org to be included in the result set.
61      * Unset filter options are ignored.
62      */
63     filterList(filter: OrgFilter, asId?: boolean): any[] {
64         const list = [];
65         this.list().forEach(org => {
66
67             const chu = filter.canHaveUsers;
68             if (chu && !this.canHaveUsers(org)) { return; }
69             if (chu === false && this.canHaveUsers(org)) { return; }
70
71             const chv = filter.canHaveVolumes;
72             if (chv && !this.canHaveVolumes(org)) { return; }
73             if (chv === false && this.canHaveVolumes(org)) { return; }
74
75             const ov = filter.opacVisible;
76             if (ov && !this.opacVisible(org)) { return; }
77             if (ov === false && this.opacVisible(org)) { return; }
78
79             if (filter.inList && !filter.inList.includes(org.id())) {
80                 return;
81             }
82
83             if (filter.notInList && filter.notInList.includes(org.id())) {
84                 return;
85             }
86
87             // All filter tests passed.  Add it to the list
88             list.push(asId ? org.id() : org);
89         });
90
91         return list;
92     }
93
94     tree(): IdlObject {
95         return this.orgTree;
96     }
97
98     // get the root OU
99     root(): IdlObject {
100         return this.orgList[0];
101     }
102
103     // list of org_unit objects or IDs for ancestors + me
104     ancestors(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
105         let node = this.get(nodeOrId);
106         if (!node) { return []; }
107         const nodes = [node];
108         while ( (node = this.get(node.parent_ou())) ) {
109             nodes.push(node);
110         }
111         if (asId) {
112             return nodes.map(n => n.id());
113         }
114         return nodes;
115     }
116
117     // tests that a node can have users
118     canHaveUsers(nodeOrId): boolean {
119         return this.get(nodeOrId).ou_type().can_have_users() === 't';
120     }
121
122     // tests that a node can have volumes
123     canHaveVolumes(nodeOrId): boolean {
124         return this
125             .get(nodeOrId)
126             .ou_type()
127             .can_have_vols() === 't';
128     }
129
130     opacVisible(nodeOrId): boolean {
131         return this.get(nodeOrId).opac_visible() === 't';
132     }
133
134     // list of org_unit objects  or IDs for me + descendants
135     descendants(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
136         const node = this.get(nodeOrId);
137         if (!node) { return []; }
138         const nodes = [];
139         const descend = (n) => {
140             nodes.push(n);
141             n.children().forEach(descend);
142         };
143         descend(node);
144         if (asId) {
145             return nodes.map(n => n.id());
146         }
147         return nodes;
148     }
149
150     // list of org_unit objects or IDs for ancestors + me + descendants
151     fullPath(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
152         const list = this.ancestors(nodeOrId, false).concat(
153           this.descendants(nodeOrId, false).slice(1));
154         if (asId) {
155             return list.map(n => n.id());
156         }
157         return list;
158     }
159
160     sortTree(sortField?: string, node?: IdlObject): void {
161         if (!sortField) { sortField = 'shortname'; }
162         if (!node) { node = this.orgTree; }
163         node.children(
164             node.children().sort((a, b) => {
165                 return a[sortField]() < b[sortField]() ? -1 : 1;
166             })
167         );
168         node.children().forEach(n => this.sortTree(sortField, n));
169     }
170
171     absorbTree(node?: IdlObject): void {
172         if (!node) {
173             node = this.orgTree;
174             this.orgMap = {};
175             this.orgList = [];
176             this.orgTypeMap = {};
177         }
178         this.orgMap[node.id()] = node;
179         this.orgList.push(node);
180
181         this.orgTypeMap[node.ou_type().id()] = node.ou_type();
182         if (!this.orgTypeList.filter(t => t.id() === node.ou_type().id())[0]) {
183             this.orgTypeList.push(node.ou_type());
184         }
185
186         node.children().forEach(c => this.absorbTree(c));
187     }
188
189     /**
190      * Grabs all of the org units from the server, chops them up into
191      * various shapes, then returns an "all done" promise.
192      */
193     fetchOrgs(): Promise<void> {
194         return this.pcrud.search('aou', {parent_ou : null},
195             {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}},
196             {anonymous : true}
197         ).toPromise().then(tree => {
198             // ingest tree, etc.
199             this.orgTree = tree;
200             this.absorbTree();
201         });
202     }
203
204     /**
205      * Populate 'target' with settings from cache where available.
206      * Return the list of settings /not/ pulled from cache.
207      */
208     private settingsFromCache(names: string[], target: any) {
209         const cacheKeys = Object.keys(this.settingsCache);
210
211         cacheKeys.forEach(key => {
212             const matchIdx = names.indexOf(key);
213             if (matchIdx > -1) {
214                 target[key] = this.settingsCache[key];
215                 names.splice(matchIdx, 1);
216             }
217         });
218
219         return names;
220     }
221
222     /**
223      * Fetch org settings from the network.
224      * 'auth' is null for anonymous lookup.
225      */
226     private settingsFromNet(orgId: number,
227         names: string[], auth?: string): Promise<any> {
228
229         const settings = {};
230         return new Promise((resolve, reject) => {
231             this.net.request(
232                 'open-ils.actor',
233                 'open-ils.actor.ou_setting.ancestor_default.batch',
234                 orgId, names, auth
235             ).subscribe(
236                 blob => {
237                     Object.keys(blob).forEach(key => {
238                         const val = blob[key]; // null or hash
239                         settings[key] = val ? val.value : null;
240                     });
241                     resolve(settings);
242                 },
243                 err => reject(err)
244             );
245         });
246     }
247
248
249     /**
250      *
251      */
252     settings(name: string | string[],
253         orgId?: number, anonymous?: boolean): Promise<OrgSettingsBatch> {
254
255         let names = [].concat(name);
256         const settings = {};
257         let auth: string = null;
258         let useCache = false;
259
260         if (this.auth.user()) {
261             if (orgId) {
262                 useCache = Number(orgId) === Number(this.auth.user().ws_ou());
263             } else {
264                 orgId = this.auth.user().ws_ou();
265                 useCache = true;
266             }
267
268             // avoid passing auth token when anonymous is requested.
269             if (!anonymous) {
270                 auth = this.auth.token();
271             }
272
273         } else if (!anonymous) {
274             console.warn('Attempt to fetch org setting(s)',
275                 name, 'in non-anonymous mode without an authtoken');
276             return Promise.resolve({});
277         }
278
279         if (useCache) {
280             names = this.settingsFromCache(names, settings);
281         }
282
283         // All requested settings found in cache (or name list is empty)
284         if (names.length === 0) {
285             return Promise.resolve(settings);
286         }
287
288         return this.settingsFromNet(orgId, names, auth)
289         .then(sets => {
290             if (useCache) {
291                 Object.keys(sets).forEach(key => {
292                     this.settingsCache[key] = sets[key];
293                 });
294             }
295             return sets;
296         });
297     }
298 }