]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/FlattenerStore.js
Link checker: user interface and supporting fixes (part 2)
[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,        /* timestamp. used internally */
20         "_last_fetch_sort": null,   /* dijit sort object. used internally */
21         "_flattener_url": "/opac/extras/flattener",
22
23         /* Everything between here and the constructor can be specified in
24          * the constructor's args object. */
25
26         "fmClass": null,
27         "mapClause": null,
28         "sloClause": null,
29         "limit": 25,
30         "offset": 0,
31         "baseSort": null,
32         "defaultSort": null,
33         "sortFieldReMap": null,
34
35         "constructor": function(/* object */ args) {
36             dojo.mixin(this, args);
37             this._current_items = {};
38         },
39
40         /* turn dojo-style sort into flattener-style sort */
41         "_prepare_sort": function(dsort) {
42             if (!dsort || !dsort.length)
43                 return this.baseSort || this.defaultSort || [];
44
45             return (this.baseSort || []).concat(
46                 dsort.map(
47                     function(d) {
48                         var o = {};
49                         o[d.attribute] = d.descending ? "desc" : "asc";
50                         return o;
51                     }
52                 )
53             );
54         },
55
56         "_remap_sort": function(prepared_sort) {
57             if (this.sortFieldReMap) {
58                 return prepared_sort.map(
59                     dojo.hitch(
60                         this, function(exp) {
61                             if (typeof exp == "object") {
62                                 var key;
63                                 for (key in exp)
64                                     break;
65                                 var newkey = (key in this.sortFieldReMap) ?
66                                     this.sortFieldReMap[key] : key;
67                                 var o = {};
68                                 o[newkey] = exp[key];
69                                 return o;
70                             } else {
71                                 return (exp in this.sortFieldReMap) ?
72                                     this.sortFieldReMap[exp] : exp;
73                             }
74                         }
75                     )
76                 );
77             } else {
78                 return prepared_sort;
79             }
80         },
81
82         "_build_flattener_params": function(req) {
83             var params = {
84                 "hint": this.fmClass,
85                 "ses": openils.User.authtoken
86             };
87
88             /* If we're asked for a specific identity, we don't use
89              * any query or sort/count/start (sort/limit/offset).  */
90             if ("identity" in req) {
91                 var where = {};
92                 where[this.fmIdentifier] = req.identity;
93
94                 params.where = dojo.toJson(where);
95             } else {
96                 params.where =  dojo.toJson(req.query);
97
98                 var slo = {
99                     "sort": this._remap_sort(this._prepare_sort(req.sort))
100                 };
101
102                 if (!req.queryOptions.all) {
103                     slo.limit =
104                         (!isNaN(req.count) && req.count != Infinity) ?
105                             req.count : this.limit;
106
107                     slo.offset =
108                         (!isNaN(req.start) && req.start != Infinity) ?
109                             req.start : this.offset;
110                 }
111
112                 if (req.queryOptions.columns)
113                     params.columns = req.queryOptions.columns;
114                 if (req.queryOptions.labels)
115                     params.labels = req.queryOptions.labels;
116
117                 params.slo = dojo.toJson(slo);
118             }
119
120             if (this.mapKey) {
121                 params.key = this.mapKey;
122             } else {
123                 params.map = dojo.toJson(this.mapClause);
124             }
125
126 //            for (var key in params)
127 //                console.debug("flattener param " + key + " -> " + params[key]);
128
129             return params;
130         },
131
132         "_display_attributes": function() {
133             var self = this;
134
135             return openils.Util.objectProperties(this.mapClause).filter(
136                 function(key) { return self.mapClause[key].display; }
137             );
138         },
139
140         "_get_map_key": function() {
141             //console.debug("mapClause: " + dojo.toJson(this.mapClause));
142             this.mapKey = fieldmapper.standardRequest(
143                 ["open-ils.fielder",
144                     "open-ils.fielder.flattened_search.prepare"], {
145                     "params": [openils.User.authtoken, this.fmClass,
146                         this.mapClause],
147                     "async": false
148                 }
149             );
150         },
151
152         "_on_http_error": function(response, ioArgs, req, retry_method) {
153             if (response.status == 402) {   /* 'Payment Required' stands
154                                                in for cache miss */
155                 if (this._retried_map_key_already) {
156                     var e = new FlattenerStoreError(
157                         "Server won't cache flattener map?"
158                     );
159                     if (typeof req.onError == "function")
160                         req.onError.call(callback_scope, e);
161                     else
162                         throw e;
163                 } else {
164                     this._retried_map_key_already = true;
165                     delete this.mapKey;
166                     if (retry_method)
167                         return this[retry_method](req);
168                 }
169             }
170         },
171
172         "_fetch_prepare": function(req) {
173             req.queryOptions = req.queryOptions || {};
174             req.abort = function() { console.warn("[unimplemented] abort()"); };
175
176             /* If we were asked to fetch without any sort order specified (as
177              * will happen when coming from fetchToPrint(), try to use the
178              * last cached sort order, if any. */
179             req.sort = req.sort || this._last_fetch_sort;
180             this._last_fetch_sort = req.sort;
181
182             if (!this.mapKey)
183                 this._get_map_key();
184
185             var p = this._build_flattener_params(req);
186             console.debug("_fetch_prepare() returning " + dojo.toJson(p));
187             return p;
188         },
189
190         "_fetch_execute": function(params,handle_as,mime_type,onload,onerror) {
191             dojo.xhrPost({
192                 "url": this._flattener_url,
193                 "content": params,
194                 "handleAs": handle_as,
195                 "sync": false,
196                 "preventCache": true,
197                 "headers": {"Accept": mime_type},
198                 "load": onload,
199                 "error": onerror
200             });
201         },
202
203         /* *** Nonstandard but public API - Please think hard about doing
204          * things the Dojo Way whenever possible before extending the API
205          * here. *** */
206
207         /* fetchToPrint() acts like a lot like fetch(), but doesn't call
208          * onBegin or onComplete.  */
209         "fetchToPrint": function(req) {
210             var callback_scope = req.scope || dojo.global;
211             var post_params;
212
213             try {
214                 post_params = this._fetch_prepare(req);
215             } catch (E) {
216                 if (typeof req.onError == "function")
217                     req.onError.call(callback_scope, E);
218                 else
219                     throw E;
220             }
221
222             var process_fetch_all = dojo.hitch(
223                 this, function(text) {
224                     this._retried_map_key_already = false;
225
226                     if (typeof req.onComplete == "function")
227                         req.onComplete.call(callback_scope, text, req);
228                 }
229             );
230
231             var process_error = dojo.hitch(
232                 this, function(response, ioArgs) {
233                     this._on_http_error(response, ioArgs, req, "fetchToPrint");
234                 }
235             );
236
237             this._fetch_execute(
238                 post_params,
239                 "text",
240                 "text/html",
241                 process_fetch_all,
242                 process_error
243             );
244
245             return req;
246         },
247
248         /* *** Begin dojo.data.api.Read methods *** */
249
250         "getValue": function(
251             /* object */ item,
252             /* string */ attribute,
253             /* anything */ defaultValue) {
254             //console.log("getValue(" + lazy(item) + ", " + attribute + ", " + defaultValue + ")")
255             if (!this.isItem(item))
256                 throw new FlattenerStoreError("getValue(): bad item " + item);
257             else if (typeof attribute != "string")
258                 throw new FlattenerStoreError("getValue(): bad attribute");
259
260             var value = item[attribute];
261             return (typeof value == "undefined") ? defaultValue : value;
262         },
263
264         "getValues": function(/* object */ item, /* string */ attribute) {
265             //console.log("getValues(" + item + ", " + attribute + ")");
266             if (!this.isItem(item) || typeof attribute != "string")
267                 throw new FlattenerStoreError("bad arguments");
268
269             var result = this.getValue(item, attribute, []);
270             return dojo.isArray(result) ? result : [result];
271         },
272
273         "getAttributes": function(/* object */ item) {
274             //console.log("getAttributes(" + item + ")");
275             if (!this.isItem(item))
276                 throw new FlattenerStoreError("getAttributes(): bad args");
277             else
278                 return this._display_attributes();
279         },
280
281         "hasAttribute": function(/* object */ item, /* string */ attribute) {
282             //console.log("hasAttribute(" + item + ", " + attribute + ")");
283             if (!this.isItem(item) || typeof attribute != "string") {
284                 throw new FlattenerStoreError("hasAttribute(): bad args");
285             } else {
286                 return dojo.indexOf(this._display_attributes(), attribute) > -1;
287             }
288         },
289
290         "containsValue": function(
291             /* object */ item,
292             /* string */ attribute,
293             /* anything */ value) {
294             //console.log("containsValue(" + item + ", " + attribute + ", " + value + ")");
295             if (!this.isItem(item) || typeof attribute != "string")
296                 throw new FlattenerStoreError("bad data");
297             else
298                 return (
299                     dojo.indexOf(this.getValues(item, attribute), value) >= -1
300                 );
301         },
302
303         "isItem": function(/* anything */ something) {
304             //console.log("isItem(" + lazy(something) + ")");
305             if (typeof something != "object" || something === null)
306                 return false;
307
308             var fields = this._display_attributes();
309
310             for (var i = 0; i < fields.length; i++) {
311                 var cur = fields[i];
312                 if (!(cur in something))
313                     return false;
314             }
315             return true;
316         },
317
318         "isItemLoaded": function(/* anything */ something) {
319             /* XXX if 'something' is not an item at all, are we just supposed
320              * to return false or throw an exception? */
321             return this.isItem(something) && (
322                 something[this.fmIdentifier] in this._current_items
323             );
324         },
325
326         "close": function(/* object */ request) { /* no-op */ return; },
327
328         "getLabel": function(/* object */ item) {
329             console.warn("[unimplemented] getLabel()");
330         },
331
332         "getLabelAttributes": function(/* object */ item) {
333             console.warn("[unimplemented] getLabelAttributes()");
334         },
335
336         "loadItem": function(/* object */ keywordArgs) {
337             if (!keywordArgs.force && this.isItemLoaded(keywordArgs.item))
338                 return;
339
340             keywordArgs.identity = this.getIdentity(keywordArgs.item);
341             return this.fetchItemByIdentity(keywordArgs);
342         },
343
344         "fetch": function(/* request-object */ req) {
345             //  Respect the following properties of the *req* object:
346             //
347             //      query    a dojo-style query, which will need modest
348             //                  translation for our server-side service
349             //      count    an int
350             //      onBegin  a callback that takes the number of items
351             //                  that this call to fetch() *could* have
352             //                  returned, with a higher limit. We do
353             //                  tricks with this.
354             //      onItem   a callback that takes each item as we get it
355             //      onComplete  a callback that takes the list of items
356             //                      after they're all fetched
357
358             var self = this;
359             var callback_scope = req.scope || dojo.global;
360             var post_params;
361
362             try {
363                 post_params = this._fetch_prepare(req);
364             } catch (E) {
365                 if (typeof req.onError == "function")
366                     req.onError.call(callback_scope, E);
367                 else
368                     throw E;
369             }
370
371             var process_fetch = function(obj, when) {
372                 if (when < self._last_fetch) /* Stale response. Discard. */
373                     return;
374
375                 self._retried_map_key_already = false;
376
377                 /* The following is apparently the "right" way to call onBegin,
378                  * and is very necessary (at least in Dojo 1.3.3) to get
379                  * the Grid's fetch-more-when-I-need-it logic to work
380                  * correctly. *grumble* crummy documentation *snarl!*
381                  */
382                 if (typeof req.onBegin == "function") {
383                     /* We lie to onBegin like this because we don't know how
384                      * many more rows we might be able to fetch if the
385                      * user keeps scrolling.  Once we get a number of
386                      * results that is less than the limit we asked for,
387                      * we stop exaggerating, and the grid is smart enough to
388                      * know we're at the end and it does the right thing. */
389                     var might_be_a_lie = req.start;
390                     if (obj.length >= req.count)
391                         might_be_a_lie += obj.length + req.count;
392                     else
393                         might_be_a_lie += obj.length;
394
395                     console.debug(
396                         "process_fetch() calling onBegin with " +
397                         might_be_a_lie + ", " + dojo.toJson(req)
398                     );
399                     req.onBegin.call(callback_scope, might_be_a_lie, req);
400                 }
401
402                 console.debug(
403                     "about to call onItem for " + obj.length +
404                     " elements in the obj array"
405                 );
406                 dojo.forEach(
407                     obj,
408                     function(item) {
409                         /* Cache items internally. */
410                         self._current_items[item[self.fmIdentifier]] = item;
411
412                         if (typeof req.onItem == "function")
413                             req.onItem.call(callback_scope, item, req);
414                     }
415                 );
416
417                 if (typeof req.onComplete == "function")
418                     req.onComplete.call(callback_scope, obj, req);
419             };
420
421             var process_error = dojo.hitch(
422                 this, function(response, ioArgs) {
423                     this._on_http_error(response, ioArgs, req, "fetch");
424                 }
425             );
426
427             var fetch_time = this._last_fetch = (new Date().getTime());
428
429             this._fetch_execute(
430                 post_params,
431                 "json",
432                 "application/json",
433                 function(obj) { process_fetch(obj, fetch_time); },
434                 process_error
435             );
436
437             return req;
438         },
439
440         /* *** Begin dojo.data.api.Identity methods *** */
441
442         "getIdentity": function(/* object */ item) {
443             if (!this.isItem(item))
444                 throw new FlattenerStoreError("not an item");
445
446             return item[this.fmIdentifier];
447         },
448
449         "getIdentityAttributes": function(/* object */ item) {
450             // console.log("getIdentityAttributes(" + item + ")");
451             return [this.fmIdentifier];
452         },
453
454         "fetchItemByIdentity": function(/* object */ keywordArgs) {
455             var callback_scope = keywordArgs.scope || dojo.global;
456             var identity = keywordArgs.identity;
457
458             if (typeof identity == "undefined")
459                 throw new FlattenerStoreError(
460                     "fetchItemByIdentity() needs identity in keywordArgs"
461                 );
462
463             /* First of force's two implications:
464              * fetch even if already loaded. */
465             if (this._current_items[identity] && !keywordArgs.force) {
466                 keywordArgs.onItem.call(
467                     callback_scope, this._current_items[identity]
468                 );
469
470                 return;
471             }
472
473             var post_params;
474             try {
475                 post_params = this._fetch_prepare(keywordArgs);
476             } catch (E) {
477                 if (typeof keywordArgs.onError == "function")
478                     keywordArgs.onError.call(callback_scope, E);
479                 else
480                     throw E;
481             }
482
483             var process_fetch_one = dojo.hitch(
484                 this, function(obj, when) {
485                     if (when < this._last_fetch) /* Stale response. Discard. */
486                         return;
487
488                     if (dojo.isArray(obj)) {
489                         if (obj.length <= 1) {
490                             obj = obj.pop() || null;    /* safe enough */
491                             /* Second of force's two implications: call setValue
492                              * ourselves.  Makes a DataGrid update. */
493                             if (keywordArgs.force && obj &&
494                                 (origitem = this._current_items[identity])) {
495                                 for (var prop in origitem)
496                                     this.setValue(origitem, prop, obj[prop]);
497                             }
498                             if (keywordArgs.onItem)
499                                 keywordArgs.onItem.call(callback_scope, obj);
500                         } else {
501                             var e = new FlattenerStoreError("Too many results");
502                             if (keywordArgs.onError)
503                                 keywordArgs.onError.call(callback_scope, e);
504                             else
505                                 throw e;
506                         }
507                     } else {
508                         var e = new FlattenerStoreError("Bad response");
509                         if (keywordArgs.onError)
510                             keywordArgs.onError.call(callback_scope, e);
511                         else
512                             throw e;
513                     }
514                 }
515             );
516
517             var process_error = dojo.hitch(
518                 this, function(response, ioArgs) {
519                     this._on_http_error(
520                         response, ioArgs, keywordArgs, "fetchItemByIdentity"
521                     );
522                 }
523             );
524
525             var fetch_time = this._last_fetch = (new Date().getTime());
526
527             this._fetch_execute(
528                 post_params,
529                 "json",
530                 "application/json",
531                 function(obj) { process_fetch_one(obj, fetch_time); },
532                 process_error
533             );
534         },
535
536         /* dojo.data.api.Write - only very partially implemented, because
537          * for FlattenerGrid, the intended client of this store, we don't
538          * need most of the methods. */
539
540         "deleteItem": function(item) {
541             //console.log("deleteItem()");
542
543             var identity = this.getIdentity(item);
544             delete this._current_items[identity];   /* safe even if missing */
545
546             this.onDelete(item);
547         },
548
549         "setValue": function(item, attribute, value) {
550             /* Silently do nothing when setValue()'s caller wants to change
551              * the identifier.  They must be confused anyway. */
552             if (attribute == this.fmIdentifier)
553                 return;
554
555             var old_value = dojo.clone(item[attribute]);
556
557             item[attribute] = dojo.clone(value);
558             this.onSet(item, attribute, old_value, value);
559         },
560
561         "setValues": function(item, attribute, values) {
562             console.warn("[unimplemented] setValues()");    /* unneeded */
563         },
564
565         "newItem": function(keywordArgs, parentInfo) {
566             console.warn("[unimplemented] newItem()");    /* unneeded */
567         },
568
569         "unsetAttribute": function() {
570             console.warn("[unimplemented] unsetAttribute()");   /* unneeded */
571         },
572
573         "save": function() {
574             console.warn("[unimplemented] save()"); /* unneeded */
575         },
576
577         "revert": function() {
578             console.warn("[unimplemented] revert()");   /* unneeded */
579         },
580
581         "isDirty": function() { /* I /think/ this will be ok for our purposes */
582             console.info("[stub] isDirty() will always return false");
583
584             return false;
585         },
586
587         /* dojo.data.api.Notification - Keep these no-op methods because
588          * clients will dojo.connect() to them.  */
589
590         "onNew" : function(item) { /* no-op */ },
591         "onDelete" : function(item) { /* no-op */ },
592         "onSet": function(item, attr, oldval, newval) { /* no-op */ },
593
594         /* *** Classes implementing any Dojo APIs do this to list which
595          *     APIs they're implementing. *** */
596
597         "getFeatures": function() {
598             return {
599                 "dojo.data.api.Read": true,
600                 "dojo.data.api.Identity": true,
601                 "dojo.data.api.Notification": true,
602                 "dojo.data.api.Write": true     /* well, only partly */
603             };
604         }
605     });
606 }