e5cbdd7764b83d7922876701d9833de9b3ecb2ac
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / FlattenerStore.js
1 if (!dojo._hasResource["openils.FlattenerStore"]) {
2     dojo._hasResource["openils.FlattenerStore"] = true;
3
4     dojo.provide("openils.FlattenerStore");
5
6     dojo.require("DojoSRF");
7     dojo.require("openils.User");
8     dojo.require("openils.Util");
9
10     /* An exception class specific to openils.FlattenerStore */
11     function FlattenerStoreError(message) { this.message = message; }
12     FlattenerStoreError.prototype.toString = function() {
13         return "openils.FlattenerStore: " + this.message;
14     };
15
16     dojo.declare(
17         "openils.FlattenerStore", null, {
18
19         "_last_fetch": null,        /* used internally */
20         "_flattener_url": "/opac/extras/flattener",
21
22         /* Everything between here and the constructor can be specified in
23          * the constructor's args object. */
24
25         "fmClass": null,
26         "mapClause": null,
27         "sloClause": null,
28         "limit": 25,
29         "offset": 0,
30         "baseSort": null,
31         "defaultSort": null,
32
33         "constructor": function(/* object */ args) {
34             dojo.mixin(this, args);
35             this._current_items = {};
36         },
37
38         /* turn dojo-style sort into flattener-style sort */
39         "_prepare_sort": function(dsort) {
40             if (!dsort || !dsort.length)
41                 return this.baseSort || this.defaultSort || [];
42
43             return (this.baseSort || []).concat(
44                 dsort.map(
45                     function(d) {
46                         var o = {};
47                         o[d.attribute] = d.descending ? "desc" : "asc";
48                         return o;
49                     }
50                 )
51             );
52         },
53
54         "_prepare_flattener_params": function(req) {
55             var params = {
56                 "hint": this.fmClass,
57                 "ses": openils.User.authtoken
58             };
59
60             /* If we're asked for a specific identity, we don't use
61              * any query or sort/count/start (sort/limit/offset).  */
62             if ("identity" in req) {
63                 var where = {};
64                 where[this.fmIdentifier] = req.identity;
65
66                 params.where = dojo.toJson(where);
67             } else {
68                 var limit = (!isNaN(req.count) && req.count != Infinity) ?
69                     req.count : this.limit;
70                 var offset = (!isNaN(req.start) && req.start != Infinity) ?
71                     req.start : this.offset;
72
73                 dojo.mixin(
74                     params, {
75                         "where": dojo.toJson(req.query),
76                         "slo": dojo.toJson({
77                             "sort": this._prepare_sort(req.sort),
78                             "limit": limit,
79                             "offset": offset
80                         })
81                     }
82                 );
83             }
84
85             if (this.mapKey) { /* XXX TODO, get a map key */
86                 params.key = this.mapKey;
87             } else {
88                 params.map = dojo.toJson(this.mapClause);
89             }
90
91             for (var key in params)
92                 console.debug("flattener param " + key + " -> " + params[key]);
93
94             return params;
95         },
96
97         "_display_attributes": function() {
98             var self = this;
99
100             return openils.Util.objectProperties(this.mapClause).filter(
101                 function(key) { return self.mapClause[key].display; }
102             );
103         },
104
105         "_get_map_key": function() {
106             //console.debug("mapClause: " + dojo.toJson(this.mapClause));
107             this.mapKey = fieldmapper.standardRequest(
108                 ["open-ils.fielder",
109                     "open-ils.fielder.flattened_search.prepare"], {
110                     "params": [openils.User.authtoken, this.fmClass,
111                         this.mapClause],
112                     "async": false
113                 }
114             );
115         },
116
117         /* *** Begin dojo.data.api.Read methods *** */
118
119         "getValue": function(
120             /* object */ item,
121             /* string */ attribute,
122             /* anything */ defaultValue) {
123             //console.log("getValue(" + lazy(item) + ", " + attribute + ", " + defaultValue + ")")
124             if (!this.isItem(item))
125                 throw new FlattenerStoreError("getValue(): bad item " + item);
126             else if (typeof attribute != "string")
127                 throw new FlattenerStoreError("getValue(): bad attribute");
128
129             var value = item[attribute];
130             return (typeof value == "undefined") ? defaultValue : value;
131         },
132
133         "getValues": function(/* object */ item, /* string */ attribute) {
134             //console.log("getValues(" + item + ", " + attribute + ")");
135             if (!this.isItem(item) || typeof attribute != "string")
136                 throw new FlattenerStoreError("bad arguments");
137
138             var result = this.getValue(item, attribute, []);
139             return dojo.isArray(result) ? result : [result];
140         },
141
142         "getAttributes": function(/* object */ item) {
143             //console.log("getAttributes(" + item + ")");
144             if (!this.isItem(item))
145                 throw new FlattenerStoreError("getAttributes(): bad args");
146             else
147                 return this._display_attributes();
148         },
149
150         "hasAttribute": function(/* object */ item, /* string */ attribute) {
151             //console.log("hasAttribute(" + item + ", " + attribute + ")");
152             if (!this.isItem(item) || typeof attribute != "string") {
153                 throw new FlattenerStoreError("hasAttribute(): bad args");
154             } else {
155                 return dojo.indexOf(this._display_attributes(), attribute) > -1;
156             }
157         },
158
159         "containsValue": function(
160             /* object */ item,
161             /* string */ attribute,
162             /* anything */ value) {
163             //console.log("containsValue(" + item + ", " + attribute + ", " + value + ")");
164             if (!this.isItem(item) || typeof attribute != "string")
165                 throw new FlattenerStoreError("bad data");
166             else
167                 return (
168                     dojo.indexOf(this.getValues(item, attribute), value) >= -1
169                 );
170         },
171
172         "isItem": function(/* anything */ something) {
173             //console.log("isItem(" + lazy(something) + ")");
174             if (typeof something != "object" || something === null)
175                 return false;
176
177             var fields = this._display_attributes();
178
179             for (var i = 0; i < fields.length; i++) {
180                 var cur = fields[i];
181                 if (!(cur in something))
182                     return false;
183             }
184             return true;
185         },
186
187         "isItemLoaded": function(/* anything */ something) {
188             /* XXX if 'something' is not an item at all, are we just supposed
189              * to return false or throw an exception? */
190             return this.isItem(something) && (
191                 something[this.fmIdentifier] in this._current_items
192             );
193         },
194
195         "close": function(/* object */ request) { /* no-op */ return; },
196
197         "getLabel": function(/* object */ item) {
198             console.warn("[unimplemented] getLabel()");
199         },
200
201         "getLabelAttributes": function(/* object */ item) {
202             console.warn("[unimplemented] getLabelAttributes()");
203         },
204
205         "loadItem": function(/* object */ keywordArgs) {
206             if (!keywordArgs.force && this.isItemLoaded(keywordArgs.item))
207                 return;
208
209             keywordArgs.identity = this.getIdentity(keywordArgs.item);
210             return this.fetchItemByIdentity(keywordArgs);
211         },
212
213         "fetch": function(/* request-object */ req) {
214             //  Respect the following properties of the *req* object:
215             //
216             //      query    a dojo-style query, which will need modest
217             //                  translation for our server-side service
218             //      count    an int
219             //      onBegin  a callback that takes the number of items
220             //                  that this call to fetch() *could* have
221             //                  returned, with a higher limit. We do
222             //                  tricks with this.
223             //      onItem   a callback that takes each item as we get it
224             //      onComplete  a callback that takes the list of items
225             //                      after they're all fetched
226             //
227             //  The onError callback is ignored for now (haven't thought
228             //  of anything useful to do with it yet).
229             //
230             //  The Read API also charges this method with adding an abort
231             //  callback to the *req* object for the caller's use, but
232             //  the one we provide does nothing but issue an alert().
233
234             //console.log("fetch(" + dojo.toJson(req) + ")");
235             var self = this;
236             var callback_scope = req.scope || dojo.global;
237
238             if (!this.mapKey) {
239                 try {
240                     this._get_map_key();
241                 } catch (E) {
242                     if (req.onError)
243                         req.onError.call(callback_scope, E);
244                     else
245                         throw E;
246                 }
247             }
248
249             var post_params = this._prepare_flattener_params(req);
250
251             if (!post_params) {
252                 if (typeof req.onComplete == "function")
253                     req.onComplete.call(callback_scope, [], req);
254                 return;
255             }
256
257             var process_fetch = function(obj, when) {
258                 if (when < self._last_fetch) /* Stale response. Discard. */
259                     return;
260
261                 self._retried_map_key_already = false;
262
263                 /* The following is apparently the "right" way to call onBegin,
264                  * and is very necessary (at least in Dojo 1.3.3) to get
265                  * the Grid's fetch-more-when-I-need-it logic to work
266                  * correctly. *grumble* crummy documentation *snarl!*
267                  */
268                 if (typeof req.onBegin == "function") {
269                     /* We lie to onBegin like this because we don't know how
270                      * many more rows we might be able to fetch if the
271                      * user keeps scrolling.  Once we get a number of
272                      * results that is less than the limit we asked for,
273                      * we stop exaggerating, and the grid is smart enough to
274                      * know we're at the end and it does the right thing. */
275                     var might_be_a_lie = req.start;
276                     if (obj.length >= req.count)
277                         might_be_a_lie += obj.length + req.count;
278                     else
279                         might_be_a_lie += obj.length;
280
281                     req.onBegin.call(callback_scope, might_be_a_lie, req);
282                 }
283
284                 dojo.forEach(
285                     obj,
286                     function(item) {
287                         /* Cache items internally. */
288                         self._current_items[item[self.fmIdentifier]] = item;
289
290                         if (typeof req.onItem == "function")
291                             req.onItem.call(callback_scope, item, req);
292                     }
293                 );
294
295                 if (typeof req.onComplete == "function")
296                     req.onComplete.call(callback_scope, obj, req);
297             };
298
299             req.abort = function() {
300                 throw new FlattenerStoreError(
301                     "The 'abort' operation is not supported"
302                 );
303             };
304
305             var fetch_time = this._last_fetch = (new Date().getTime());
306
307             dojo.xhrPost({
308                 "url": this._flattener_url,
309                 "content": post_params,
310                 "handleAs": "json",
311                 "sync": false,
312                 "preventCache": true,
313                 "headers": {"Accept": "application/json"},
314                 "load": function(obj) { process_fetch(obj, fetch_time); },
315                 "error": function(response, ioArgs) {
316                     if (response.status == 402) {   /* 'Payment Required' stands
317                                                        in for cache miss */
318                         if (self._retried_map_key_already) {
319                             var e = new FlattenerStoreError(
320                                 "Server won't cache flattener map?"
321                             );
322                             if (typeof req.onError == "function")
323                                 req.onError.call(callback_scope, e);
324                             else
325                                 throw e;
326                         } else {
327                             self._retried_map_key_already = true;
328                             delete self.mapKey;
329                             return self.fetch(req);
330                         }
331                     }
332                 }
333             });
334
335             return req;
336         },
337
338         /* *** Begin dojo.data.api.Identity methods *** */
339
340         "getIdentity": function(/* object */ item) {
341             if (!this.isItem(item))
342                 throw new FlattenerStoreError("not an item");
343
344             return item[this.fmIdentifier];
345         },
346
347         "getIdentityAttributes": function(/* object */ item) {
348             // console.log("getIdentityAttributes(" + item + ")");
349             return [this.fmIdentifier];
350         },
351
352         "fetchItemByIdentity": function(/* object */ keywordArgs) {
353             var callback_scope = keywordArgs.scope || dojo.global;
354             var identity = keywordArgs.identity;
355
356             if (typeof identity == "undefined")
357                 throw new FlattenerStoreError(
358                     "fetchItemByIdentity() needs identity in keywordArgs"
359                 );
360
361             /* First of force's two implications:
362              * fetch even if already loaded. */
363             if (this._current_items[identity] && !keywordArgs.force) {
364                 keywordArgs.onItem.call(
365                     callback_scope, this._current_items[identity]
366                 );
367
368                 return;
369             }
370
371             var post_params = this._prepare_flattener_params(keywordArgs);
372
373             var process_fetch_one = dojo.hitch(
374                 this, function(obj, when) {
375                     if (when < this._last_fetch) /* Stale response. Discard. */
376                         return;
377
378                     if (dojo.isArray(obj)) {
379                         if (obj.length <= 1) {
380                             obj = obj.pop() || null;    /* safe enough */
381                             /* Second of force's two implications: call setValue
382                              * ourselves.  Makes a DataGrid update. */
383                             if (keywordArgs.force && obj &&
384                                 (origitem = this._current_items[identity])) {
385                                 for (var prop in origitem)
386                                     this.setValue(origitem, prop, obj[prop]);
387                             }
388                             if (keywordArgs.onItem)
389                                 keywordArgs.onItem.call(callback_scope, obj);
390                         } else {
391                             var e = new FlattenerStoreError("Too many results");
392                             if (keywordArgs.onError)
393                                 keywordArgs.onError.call(callback_scope, e);
394                             else
395                                 throw e;
396                         }
397                     } else {
398                         var e = new FlattenerStoreError("Bad response");
399                         if (keywordArgs.onError)
400                             keywordArgs.onError.call(callback_scope, e);
401                         else
402                             throw e;
403                     }
404                 }
405             );
406
407             var fetch_time = this._last_fetch = (new Date().getTime());
408
409             dojo.xhrPost({
410                 "url": this._flattener_url,
411                 "content": post_params,
412                 "handleAs": "json",
413                 "sync": false,
414                 "preventCache": true,
415                 "headers": {"Accept": "application/json"},
416                 "load": function(obj){ process_fetch_one(obj, fetch_time); }
417             });
418         },
419
420         /* dojo.data.api.Write - only very partially implemented, because
421          * for FlattenerGrid, the intended client of this store, we don't
422          * need most of the methods. */
423
424         "deleteItem": function(item) {
425             //console.log("deleteItem()");
426
427             var identity = this.getIdentity(item);
428             delete this._current_items[identity];   /* safe even if missing */
429
430             this.onDelete(item);
431         },
432
433         "setValue": function(item, attribute, value) {
434             /* Silently do nothing when setValue()'s caller wants to change
435              * the identifier.  They must be confused anyway. */
436             if (attribute == this.fmIdentifier)
437                 return;
438
439             var old_value = dojo.clone(item[attribute]);
440
441             item[attribute] = dojo.clone(value);
442             this.onSet(item, attribute, old_value, value);
443         },
444
445         "setValues": function(item, attribute, values) {
446             console.warn("[unimplemented] setValues()");    /* unneeded */
447         },
448
449         "newItem": function(keywordArgs, parentInfo) {
450             console.warn("[unimplemented] newItem()");    /* unneeded */
451         },
452
453         "unsetAttribute": function() {
454             console.warn("[unimplemented] unsetAttribute()");   /* unneeded */
455         },
456
457         "save": function() {
458             console.warn("[unimplemented] save()"); /* unneeded */
459         },
460
461         "revert": function() {
462             console.warn("[unimplemented] revert()");   /* unneeded */
463         },
464
465         "isDirty": function() { /* I /think/ this will be ok for our purposes */
466             console.info("[stub] isDirty() will always return false");
467
468             return false;
469         },
470
471         /* dojo.data.api.Notification - Keep these no-op methods because
472          * clients will dojo.connect() to them.  */
473
474         "onNew" : function(item) { /* no-op */ },
475         "onDelete" : function(item) { /* no-op */ },
476         "onSet": function(item, attr, oldval, newval) { /* no-op */ },
477
478         /* *** Classes implementing any Dojo APIs do this to list which
479          *     APIs they're implementing. *** */
480
481         "getFeatures": function() {
482             return {
483                 "dojo.data.api.Read": true,
484                 "dojo.data.api.Identity": true,
485                 "dojo.data.api.Notification": true,
486                 "dojo.data.api.Write": true     /* well, only partly */
487             };
488         }
489     });
490 }