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, /* used internally */
20 "_flattener_url": "/opac/extras/flattener",
22 /* Everything between here and the constructor can be specified in
23 * the constructor's args object. */
33 "constructor": function(/* object */ args) {
34 dojo.mixin(this, args);
35 this._current_items = {};
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 || [];
43 return (this.baseSort || []).concat(
47 o[d.attribute] = d.descending ? "desc" : "asc";
54 "_prepare_flattener_params": function(req) {
57 "ses": openils.User.authtoken
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) {
64 where[this.fmIdentifier] = req.identity;
66 params.where = dojo.toJson(where);
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;
75 "where": dojo.toJson(req.query),
77 "sort": this._prepare_sort(req.sort),
85 if (this.mapKey) { /* XXX TODO, get a map key */
86 params.key = this.mapKey;
88 params.map = dojo.toJson(this.mapClause);
91 for (var key in params)
92 console.debug("flattener param " + key + " -> " + params[key]);
97 "_display_attributes": function() {
100 return openils.Util.objectProperties(this.mapClause).filter(
101 function(key) { return self.mapClause[key].display; }
105 "_get_map_key": function() {
106 //console.debug("mapClause: " + dojo.toJson(this.mapClause));
107 this.mapKey = fieldmapper.standardRequest(
109 "open-ils.fielder.flattened_search.prepare"], {
110 "params": [openils.User.authtoken, this.fmClass,
117 /* *** Begin dojo.data.api.Read methods *** */
119 "getValue": function(
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");
129 var value = item[attribute];
130 return (typeof value == "undefined") ? defaultValue : value;
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");
138 var result = this.getValue(item, attribute, []);
139 return dojo.isArray(result) ? result : [result];
142 "getAttributes": function(/* object */ item) {
143 //console.log("getAttributes(" + item + ")");
144 if (!this.isItem(item))
145 throw new FlattenerStoreError("getAttributes(): bad args");
147 return this._display_attributes();
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");
155 return dojo.indexOf(this._display_attributes(), attribute) > -1;
159 "containsValue": function(
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");
168 dojo.indexOf(this.getValues(item, attribute), value) >= -1
172 "isItem": function(/* anything */ something) {
173 //console.log("isItem(" + lazy(something) + ")");
174 if (typeof something != "object" || something === null)
177 var fields = this._display_attributes();
179 for (var i = 0; i < fields.length; i++) {
181 if (!(cur in something))
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
195 "close": function(/* object */ request) { /* no-op */ return; },
197 "getLabel": function(/* object */ item) {
198 console.warn("[unimplemented] getLabel()");
201 "getLabelAttributes": function(/* object */ item) {
202 console.warn("[unimplemented] getLabelAttributes()");
205 "loadItem": function(/* object */ keywordArgs) {
206 if (!keywordArgs.force && this.isItemLoaded(keywordArgs.item))
209 keywordArgs.identity = this.getIdentity(keywordArgs.item);
210 return this.fetchItemByIdentity(keywordArgs);
213 "fetch": function(/* request-object */ req) {
214 // Respect the following properties of the *req* object:
216 // query a dojo-style query, which will need modest
217 // translation for our server-side service
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
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
227 // The onError callback is ignored for now (haven't thought
228 // of anything useful to do with it yet).
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().
234 //console.log("fetch(" + dojo.toJson(req) + ")");
236 var callback_scope = req.scope || dojo.global;
243 req.onError.call(callback_scope, E);
249 var post_params = this._prepare_flattener_params(req);
252 if (typeof req.onComplete == "function")
253 req.onComplete.call(callback_scope, [], req);
257 var process_fetch = function(obj, when) {
258 if (when < self._last_fetch) /* Stale response. Discard. */
261 self._retried_map_key_already = false;
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!*
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;
279 might_be_a_lie += obj.length;
281 req.onBegin.call(callback_scope, might_be_a_lie, req);
287 /* Cache items internally. */
288 self._current_items[item[self.fmIdentifier]] = item;
290 if (typeof req.onItem == "function")
291 req.onItem.call(callback_scope, item, req);
295 if (typeof req.onComplete == "function")
296 req.onComplete.call(callback_scope, obj, req);
299 req.abort = function() {
300 throw new FlattenerStoreError(
301 "The 'abort' operation is not supported"
305 var fetch_time = this._last_fetch = (new Date().getTime());
308 "url": this._flattener_url,
309 "content": post_params,
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
318 if (self._retried_map_key_already) {
319 var e = new FlattenerStoreError(
320 "Server won't cache flattener map?"
322 if (typeof req.onError == "function")
323 req.onError.call(callback_scope, e);
327 self._retried_map_key_already = true;
329 return self.fetch(req);
338 /* *** Begin dojo.data.api.Identity methods *** */
340 "getIdentity": function(/* object */ item) {
341 if (!this.isItem(item))
342 throw new FlattenerStoreError("not an item");
344 return item[this.fmIdentifier];
347 "getIdentityAttributes": function(/* object */ item) {
348 // console.log("getIdentityAttributes(" + item + ")");
349 return [this.fmIdentifier];
352 "fetchItemByIdentity": function(/* object */ keywordArgs) {
353 var callback_scope = keywordArgs.scope || dojo.global;
354 var identity = keywordArgs.identity;
356 if (typeof identity == "undefined")
357 throw new FlattenerStoreError(
358 "fetchItemByIdentity() needs identity in keywordArgs"
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]
371 var post_params = this._prepare_flattener_params(keywordArgs);
373 var process_fetch_one = dojo.hitch(
374 this, function(obj, when) {
375 if (when < this._last_fetch) /* Stale response. Discard. */
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]);
388 if (keywordArgs.onItem)
389 keywordArgs.onItem.call(callback_scope, obj);
391 var e = new FlattenerStoreError("Too many results");
392 if (keywordArgs.onError)
393 keywordArgs.onError.call(callback_scope, e);
398 var e = new FlattenerStoreError("Bad response");
399 if (keywordArgs.onError)
400 keywordArgs.onError.call(callback_scope, e);
407 var fetch_time = this._last_fetch = (new Date().getTime());
410 "url": this._flattener_url,
411 "content": post_params,
414 "preventCache": true,
415 "headers": {"Accept": "application/json"},
416 "load": function(obj){ process_fetch_one(obj, fetch_time); }
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. */
424 "deleteItem": function(item) {
425 //console.log("deleteItem()");
427 var identity = this.getIdentity(item);
428 delete this._current_items[identity]; /* safe even if missing */
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)
439 var old_value = dojo.clone(item[attribute]);
441 item[attribute] = dojo.clone(value);
442 this.onSet(item, attribute, old_value, value);
445 "setValues": function(item, attribute, values) {
446 console.warn("[unimplemented] setValues()"); /* unneeded */
449 "newItem": function(keywordArgs, parentInfo) {
450 console.warn("[unimplemented] newItem()"); /* unneeded */
453 "unsetAttribute": function() {
454 console.warn("[unimplemented] unsetAttribute()"); /* unneeded */
458 console.warn("[unimplemented] save()"); /* unneeded */
461 "revert": function() {
462 console.warn("[unimplemented] revert()"); /* unneeded */
465 "isDirty": function() { /* I /think/ this will be ok for our purposes */
466 console.info("[stub] isDirty() will always return false");
471 /* dojo.data.api.Notification - Keep these no-op methods because
472 * clients will dojo.connect() to them. */
474 "onNew" : function(item) { /* no-op */ },
475 "onDelete" : function(item) { /* no-op */ },
476 "onSet": function(item, attr, oldval, newval) { /* no-op */ },
478 /* *** Classes implementing any Dojo APIs do this to list which
479 * APIs they're implementing. *** */
481 "getFeatures": function() {
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 */