1 if (!dojo._hasResource["openils.FlattenerStore"]) {
2 dojo._hasResource["openils.FlattenerStore"] = true;
4 dojo.provide("openils.FlattenerStore");
6 dojo.require("DojoSRF");
7 dojo.require("openils.User");
8 dojo.require("openils.Util");
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;
17 "openils.FlattenerStore", null, {
19 "_last_fetch": null, /* timestamp. used internally */
20 "_last_fetch_sort": null, /* dijit sort object. used internally */
21 "_flattener_url": "/opac/extras/flattener",
23 /* Everything between here and the constructor can be specified in
24 * the constructor's args object. */
33 "sortFieldReMap": null,
35 "constructor": function(/* object */ args) {
36 dojo.mixin(this, args);
37 this._current_items = {};
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 || [];
45 return (this.baseSort || []).concat(
49 o[d.attribute] = d.descending ? "desc" : "asc";
56 "_remap_sort": function(prepared_sort) {
57 if (this.sortFieldReMap) {
58 return prepared_sort.map(
61 if (typeof exp == "object") {
65 var newkey = (key in this.sortFieldReMap) ?
66 this.sortFieldReMap[key] : key;
71 return (exp in this.sortFieldReMap) ?
72 this.sortFieldReMap[exp] : exp;
82 "_build_flattener_params": function(req) {
85 "ses": openils.User.authtoken
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) {
92 where[this.fmIdentifier] = req.identity;
94 params.where = dojo.toJson(where);
96 params.where = dojo.toJson(req.query);
99 "sort": this._remap_sort(this._prepare_sort(req.sort))
102 if (!req.queryOptions.all) {
104 (!isNaN(req.count) && req.count != Infinity) ?
105 req.count : this.limit;
108 (!isNaN(req.start) && req.start != Infinity) ?
109 req.start : this.offset;
112 if (req.queryOptions.columns)
113 params.columns = req.queryOptions.columns;
114 if (req.queryOptions.labels)
115 params.labels = req.queryOptions.labels;
117 params.slo = dojo.toJson(slo);
121 params.key = this.mapKey;
123 params.map = dojo.toJson(this.mapClause);
126 // for (var key in params)
127 // console.debug("flattener param " + key + " -> " + params[key]);
132 "_display_attributes": function() {
135 return openils.Util.objectProperties(this.mapClause).filter(
136 function(key) { return self.mapClause[key].display; }
140 "_get_map_key": function() {
141 //console.debug("mapClause: " + dojo.toJson(this.mapClause));
142 this.mapKey = fieldmapper.standardRequest(
144 "open-ils.fielder.flattened_search.prepare"], {
145 "params": [openils.User.authtoken, this.fmClass,
152 "_on_http_error": function(response, ioArgs, req, retry_method) {
153 if (response.status == 402) { /* 'Payment Required' stands
155 if (this._retried_map_key_already) {
156 var e = new FlattenerStoreError(
157 "Server won't cache flattener map?"
159 if (typeof req.onError == "function")
160 req.onError.call(callback_scope, e);
164 this._retried_map_key_already = true;
167 return this[retry_method](req);
172 "_fetch_prepare": function(req) {
173 req.queryOptions = req.queryOptions || {};
174 req.abort = function() { console.warn("[unimplemented] abort()"); };
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;
185 var p = this._build_flattener_params(req);
186 console.debug("_fetch_prepare() returning " + dojo.toJson(p));
190 "_fetch_execute": function(params,handle_as,mime_type,onload,onerror) {
192 "url": this._flattener_url,
194 "handleAs": handle_as,
196 "preventCache": true,
197 "headers": {"Accept": mime_type},
203 /* *** Nonstandard but public API - Please think hard about doing
204 * things the Dojo Way whenever possible before extending the API
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;
214 post_params = this._fetch_prepare(req);
216 if (typeof req.onError == "function")
217 req.onError.call(callback_scope, E);
222 var process_fetch_all = dojo.hitch(
223 this, function(text) {
224 this._retried_map_key_already = false;
226 if (typeof req.onComplete == "function")
227 req.onComplete.call(callback_scope, text, req);
231 var process_error = dojo.hitch(
232 this, function(response, ioArgs) {
233 this._on_http_error(response, ioArgs, req, "fetchToPrint");
248 /* *** Begin dojo.data.api.Read methods *** */
250 "getValue": function(
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");
260 var value = item[attribute];
261 return (typeof value == "undefined") ? defaultValue : value;
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");
269 var result = this.getValue(item, attribute, []);
270 return dojo.isArray(result) ? result : [result];
273 "getAttributes": function(/* object */ item) {
274 //console.log("getAttributes(" + item + ")");
275 if (!this.isItem(item))
276 throw new FlattenerStoreError("getAttributes(): bad args");
278 return this._display_attributes();
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");
286 return dojo.indexOf(this._display_attributes(), attribute) > -1;
290 "containsValue": function(
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");
299 dojo.indexOf(this.getValues(item, attribute), value) >= -1
303 "isItem": function(/* anything */ something) {
304 //console.log("isItem(" + lazy(something) + ")");
305 if (typeof something != "object" || something === null)
308 var fields = this._display_attributes();
310 for (var i = 0; i < fields.length; i++) {
312 if (!(cur in something))
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
326 "close": function(/* object */ request) { /* no-op */ return; },
328 "getLabel": function(/* object */ item) {
329 console.warn("[unimplemented] getLabel()");
332 "getLabelAttributes": function(/* object */ item) {
333 console.warn("[unimplemented] getLabelAttributes()");
336 "loadItem": function(/* object */ keywordArgs) {
337 if (!keywordArgs.force && this.isItemLoaded(keywordArgs.item))
340 keywordArgs.identity = this.getIdentity(keywordArgs.item);
341 return this.fetchItemByIdentity(keywordArgs);
344 "fetch": function(/* request-object */ req) {
345 // Respect the following properties of the *req* object:
347 // query a dojo-style query, which will need modest
348 // translation for our server-side service
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
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
359 var callback_scope = req.scope || dojo.global;
363 post_params = this._fetch_prepare(req);
365 if (typeof req.onError == "function")
366 req.onError.call(callback_scope, E);
371 var process_fetch = function(obj, when) {
372 if (when < self._last_fetch) /* Stale response. Discard. */
375 self._retried_map_key_already = false;
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!*
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;
393 might_be_a_lie += obj.length;
396 "process_fetch() calling onBegin with " +
397 might_be_a_lie + ", " + dojo.toJson(req)
399 req.onBegin.call(callback_scope, might_be_a_lie, req);
403 "about to call onItem for " + obj.length +
404 " elements in the obj array"
409 /* Cache items internally. */
410 self._current_items[item[self.fmIdentifier]] = item;
412 if (typeof req.onItem == "function")
413 req.onItem.call(callback_scope, item, req);
417 if (typeof req.onComplete == "function")
418 req.onComplete.call(callback_scope, obj, req);
421 var process_error = dojo.hitch(
422 this, function(response, ioArgs) {
423 this._on_http_error(response, ioArgs, req, "fetch");
427 var fetch_time = this._last_fetch = (new Date().getTime());
433 function(obj) { process_fetch(obj, fetch_time); },
440 /* *** Begin dojo.data.api.Identity methods *** */
442 "getIdentity": function(/* object */ item) {
443 if (!this.isItem(item))
444 throw new FlattenerStoreError("not an item");
446 return item[this.fmIdentifier];
449 "getIdentityAttributes": function(/* object */ item) {
450 // console.log("getIdentityAttributes(" + item + ")");
451 return [this.fmIdentifier];
454 "fetchItemByIdentity": function(/* object */ keywordArgs) {
455 var callback_scope = keywordArgs.scope || dojo.global;
456 var identity = keywordArgs.identity;
458 if (typeof identity == "undefined")
459 throw new FlattenerStoreError(
460 "fetchItemByIdentity() needs identity in keywordArgs"
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]
475 post_params = this._fetch_prepare(keywordArgs);
477 if (typeof keywordArgs.onError == "function")
478 keywordArgs.onError.call(callback_scope, E);
483 var process_fetch_one = dojo.hitch(
484 this, function(obj, when) {
485 if (when < this._last_fetch) /* Stale response. Discard. */
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]);
498 if (keywordArgs.onItem)
499 keywordArgs.onItem.call(callback_scope, obj);
501 var e = new FlattenerStoreError("Too many results");
502 if (keywordArgs.onError)
503 keywordArgs.onError.call(callback_scope, e);
508 var e = new FlattenerStoreError("Bad response");
509 if (keywordArgs.onError)
510 keywordArgs.onError.call(callback_scope, e);
517 var process_error = dojo.hitch(
518 this, function(response, ioArgs) {
520 response, ioArgs, keywordArgs, "fetchItemByIdentity"
525 var fetch_time = this._last_fetch = (new Date().getTime());
531 function(obj) { process_fetch_one(obj, fetch_time); },
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. */
540 "deleteItem": function(item) {
541 //console.log("deleteItem()");
543 var identity = this.getIdentity(item);
544 delete this._current_items[identity]; /* safe even if missing */
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)
555 var old_value = dojo.clone(item[attribute]);
557 item[attribute] = dojo.clone(value);
558 this.onSet(item, attribute, old_value, value);
561 "setValues": function(item, attribute, values) {
562 console.warn("[unimplemented] setValues()"); /* unneeded */
565 "newItem": function(keywordArgs, parentInfo) {
566 console.warn("[unimplemented] newItem()"); /* unneeded */
569 "unsetAttribute": function() {
570 console.warn("[unimplemented] unsetAttribute()"); /* unneeded */
574 console.warn("[unimplemented] save()"); /* unneeded */
577 "revert": function() {
578 console.warn("[unimplemented] revert()"); /* unneeded */
581 "isDirty": function() { /* I /think/ this will be ok for our purposes */
582 console.info("[stub] isDirty() will always return false");
587 /* dojo.data.api.Notification - Keep these no-op methods because
588 * clients will dojo.connect() to them. */
590 "onNew" : function(item) { /* no-op */ },
591 "onDelete" : function(item) { /* no-op */ },
592 "onSet": function(item, attr, oldval, newval) { /* no-op */ },
594 /* *** Classes implementing any Dojo APIs do this to list which
595 * APIs they're implementing. *** */
597 "getFeatures": function() {
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 */