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