]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js
don't assume an autofieldwidget cached linked object list contains the linked object...
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / AutoFieldWidget.js
1 if(!dojo._hasResource['openils.widget.AutoFieldWidget']) {
2     dojo.provide('openils.widget.AutoFieldWidget');
3     dojo.require('openils.Util');
4     dojo.require('openils.User');
5     dojo.require('fieldmapper.IDL');
6     dojo.require('openils.PermaCrud');
7         dojo.requireLocalization("openils.widget", "AutoFieldWidget");
8
9     dojo.declare('openils.widget.AutoFieldWidget', null, {
10
11         async : false,
12
13         /**
14          * args:
15          *  idlField -- Field description object from fieldmapper.IDL.fmclasses
16          *  fmObject -- If available, the object being edited.  This will be used 
17          *      to set the value of the widget.
18          *  fmClass -- Class name (not required if idlField or fmObject is set)
19          *  fmField -- Field name (not required if idlField)
20          *  parentNode -- If defined, the widget will be appended to this DOM node
21          *  dijitArgs -- Optional parameters object, passed directly to the dojo widget
22          *  orgLimitPerms -- If this field defines a set of org units and an orgLimitPerms 
23          *      is defined, the code will limit the org units in the set to those
24          *      allowed by the permission
25          *  orgDefaultsToWs -- If this is an org unit field and the widget has no value,
26          *      set the value equal to the users's workstation org unit.  Othwerwise, leave it null
27          *  selfReference -- The primary purpose of an AutoFieldWidget is to render the value
28          *      or widget for a field on an object (that may or may not link to another object).
29          *      selfReference allows you to sidestep the indirection and create a selector widget
30          *      based purely on an fmClass.  To get a dropdown of all of the 'abc'
31          *      objects, pass in {selfReference : true, fmClass : 'abc'}.  
32          *  labelFormat -- For widgets that are displayed as remote object filtering selects,
33          *      this provides a mechanism for overriding the label format in the filtering select.
34          *      It must be an array, whose first value is a format string, compliant with
35          *      dojo.string.substitute.  The remaining array items are the arguments to the format
36          *      represented as field names on the remote linked object.
37          *      E.g.
38          *      labelFormat : [ '${0} (${1})', 'obj_field_1', 'obj_field_2' ]
39          *      Note: this does not control the final display value.  Only values in the drop-down.
40          *      See searchFormat for controlling the display value
41          *  searchFormat -- This format controls the structure of the search attribute which
42          *      controls the text used during type-ahead searching and the displayed value in 
43          *      the filtering select.  See labelFormat for the structure.  
44          *  dataLoader : Bypass the default PermaCrud linked data fetcher and use this function instead.
45          *      Function arguments are (link class name, search filter, callback)
46          *      The fetched objects should be passed to the callback as an array
47          */
48         constructor : function(args) {
49             for(var k in args)
50                 this[k] = args[k];
51
52             // find the field description in the IDL if not provided
53             if(this.fmObject) 
54                 this.fmClass = this.fmObject.classname;
55             this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
56             this.suppressLinkedFields = args.suppressLinkedFields || [];
57
58             if(this.selfReference) {
59                 this.fmField = fieldmapper.IDL.fmclasses[this.fmClass].pkey;
60                 
61                 // create a mock-up of the idlField object.  
62                 this.idlField = {
63                     datatype : 'link',
64                     'class' : this.fmClass,
65                     reltype : 'has_a',
66                     key : this.fmField,
67                     name : this.fmField
68                 };
69
70             } else {
71
72                 if(!this.idlField) {
73                     this.fmIDL = fieldmapper.IDL.fmclasses[this.fmClass];
74                     var fields = this.fmIDL.fields;
75                     for(var f in fields) 
76                         if(fields[f].name == this.fmField)
77                             this.idlField = fields[f];
78                 }
79             }
80
81             if(!this.idlField) 
82                 throw new Error("AutoFieldWidget could not determine which " +
83                     "field to render.  We need more information. fmClass=" + 
84                     this.fmClass + ' fmField=' + this.fmField + ' fmObject=' + js2JSON(this.fmObject));
85
86             this.auth = openils.User.authtoken;
87             this.cache = openils.widget.AutoFieldWidget.cache;
88             this.cache[this.auth] = this.cache[this.auth] || {};
89             this.cache[this.auth].single = this.cache[this.auth].single || {};
90             this.cache[this.auth].list = this.cache[this.auth].list || {};
91         },
92
93         /**
94          * Turn the widget-stored value into a value oils understands
95          */
96         getFormattedValue : function() {
97             var value = this.baseWidgetValue();
98             switch(this.idlField.datatype) {
99                 case 'bool':
100                     switch(value) {
101                         case 'true': return 't';
102                         case 'on': return 't';
103                         case 'false' : return 'f';
104                         case 'unset' : return null;
105                         case true : return 't';
106                         default: return 'f';
107                     }
108                 case 'timestamp':
109                     if(!value) return null;
110                     return dojo.date.stamp.toISOString(value);
111                 case 'int':
112                 case 'float':
113                 case 'money':
114                     if(isNaN(value)) value = null;
115                 default:
116                     return (value === '') ? null : value;
117             }
118         },
119
120         baseWidgetValue : function(value) {
121             var attr = (this.readOnly) ? 'content' : 'value';
122             if(arguments.length) this.widget.attr(attr, value);
123             return this.widget.attr(attr);
124         },
125         
126         /**
127          * Turn the widget-stored value into something visually suitable
128          */
129         getDisplayString : function() {
130             var value = this.widgetValue;
131             switch(this.idlField.datatype) {
132                 case 'bool':
133                     switch(value) {
134                         case 't': 
135                         case 'true': 
136                             return openils.widget.AutoFieldWidget.localeStrings.TRUE; 
137                         case 'f' : 
138                         case 'false' : 
139                             return openils.widget.AutoFieldWidget.localeStrings.FALSE;
140                         case 'unset' : return openils.widget.AutoFieldWidget.localeStrings.UNSET;
141                         case true : return openils.widget.AutoFieldWidget.localeStrings.TRUE; 
142                         default: return openils.widget.AutoFieldWidget.localeStrings.FALSE;
143                     }
144                 case 'timestamp':
145                     if (!value) return '';
146                     dojo.require('dojo.date.locale');
147                     dojo.require('dojo.date.stamp');
148                     var date = dojo.date.stamp.fromISOString(value);
149                     return dojo.date.locale.format(date, {formatLength:'short'});
150                 case 'org_unit':
151                     if(value === null || value === undefined) return '';
152                     return fieldmapper.aou.findOrgUnit(value).shortname();
153                 case 'int':
154                 case 'float':
155                     if(isNaN(value)) value = 0;
156                 default:
157                     if(value === undefined || value === null)
158                         value = '';
159                     return value+'';
160             }
161         },
162
163         build : function(onload) {
164
165             if(this.widgetValue == null)
166                 this.widgetValue = (this.fmObject) ? this.fmObject[this.idlField.name]() : null;
167
168             if(this.widget) {
169                 // core widget provided for us, attach and move on
170                 if(this.parentNode) // may already be in the "right" place
171                     this.parentNode.appendChild(this.widget.domNode);
172                 if(this.widget.attr('value') == null)
173                     this._widgetLoaded();
174                 return;
175             }
176             
177             if(!this.parentNode) // give it somewhere to live so that dojo won't complain
178                 this.parentNode = dojo.create('div');
179
180             this.onload = onload;
181
182             if(this.readOnly) {
183                 dojo.require('dijit.layout.ContentPane');
184                 this.widget = new dijit.layout.ContentPane(this.dijitArgs, this.parentNode);
185                 if(this.widgetValue !== null)
186                     this._tryLinkedDisplayField();
187
188             } else if(this.widgetClass) {
189                 dojo.require(this.widgetClass);
190                 eval('this.widget = new ' + this.widgetClass + '(this.dijitArgs, this.parentNode);');
191
192             } else {
193
194                 switch(this.idlField.datatype) {
195                     
196                     case 'id':
197                         dojo.require('dijit.form.TextBox');
198                         this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
199                         break;
200
201                     case 'org_unit':
202                         this._buildOrgSelector();
203                         break;
204
205                     case 'money':
206                         dojo.require('dijit.form.CurrencyTextBox');
207                         this.widget = new dijit.form.CurrencyTextBox(this.dijitArgs, this.parentNode);
208                         break;
209
210                     case 'int':
211                         dojo.require('dijit.form.NumberTextBox');
212                         this.dijitArgs = dojo.mixin(this.dijitArgs || {}, {constraints:{places:0}});
213                         this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
214                         break;
215
216                     case 'float':
217                         dojo.require('dijit.form.NumberTextBox');
218                         this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
219                         break;
220
221                     case 'timestamp':
222                         dojo.require('dijit.form.DateTextBox');
223                         dojo.require('dojo.date.stamp');
224                         this.widget = new dijit.form.DateTextBox(this.dijitArgs, this.parentNode);
225                         if(this.widgetValue != null) 
226                             this.widgetValue = dojo.date.stamp.fromISOString(this.widgetValue);
227                         break;
228
229                     case 'bool':
230                         if(this.ternary) {
231                             dojo.require('dijit.form.FilteringSelect');
232                             var store = new dojo.data.ItemFileReadStore({
233                                 data:{
234                                     identifier : 'value',
235                                     items:[
236                                         {label : openils.widget.AutoFieldWidget.localeStrings.UNSET, value : 'unset'},
237                                         {label : openils.widget.AutoFieldWidget.localeStrings.TRUE, value : 'true'},
238                                         {label : openils.widget.AutoFieldWidget.localeStrings.FALSE, value : 'false'}
239                                     ]
240                                 }
241                             });
242                             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
243                             this.widget.searchAttr = this.widget.labelAttr = 'label';
244                             this.widget.valueAttr = 'value';
245                             this.widget.store = store;
246                             this.widget.startup();
247                             this.widgetValue = (this.widgetValue === null) ? 'unset' : 
248                                 (openils.Util.isTrue(this.widgetValue)) ? 'true' : 'false';
249                         } else {
250                             dojo.require('dijit.form.CheckBox');
251                             this.widget = new dijit.form.CheckBox(this.dijitArgs, this.parentNode);
252                             this.widgetValue = openils.Util.isTrue(this.widgetValue);
253                         }
254                         break;
255
256                     case 'link':
257                         if(this._buildLinkSelector()) break;
258
259                     default:
260                         if(this.dijitArgs && (this.dijitArgs.required || this.dijitArgs.regExp)) {
261                             dojo.require('dijit.form.ValidationTextBox');
262                             this.widget = new dijit.form.ValidationTextBox(this.dijitArgs, this.parentNode);
263                         } else {
264                             dojo.require('dijit.form.TextBox');
265                             this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
266                         }
267                 }
268             }
269
270             if(!this.async) this._widgetLoaded();
271             return this.widget;
272         },
273
274         // we want to display the value for our widget.  However, instead of displaying
275         // an ID, for exmaple, display the value for the 'selector' field on the object
276         // the ID points to
277         _tryLinkedDisplayField : function(noAsync) {
278
279             if(this.idlField.datatype == 'org_unit')
280                 return false; // we already handle org_units, no need to re-fetch
281
282             // user opted to bypass fetching this linked data
283             if(this.suppressLinkedFields.indexOf(this.idlField.name) > -1)
284                 return false;
285
286             var linkInfo = this._getLinkSelector();
287             if(!(linkInfo && linkInfo.vfield && linkInfo.vfield.selector)) 
288                 return false;
289             var lclass = linkInfo.linkClass;
290
291             if(lclass == 'aou') 
292                 return false;
293
294             // first try the store cache
295             var self = this;
296             if(this.cache[this.auth].list[lclass]) {
297                 var store = this.cache[this.auth].list[lclass];
298                 var query = {};
299                 query[linkInfo.vfield.name] = ''+this.widgetValue;
300                 var found = false;
301                 store.fetch({query:query, onComplete:
302                     function(list) {
303                         if(list[0]) {
304                             self.widgetValue = store.getValue(list[0], linkInfo.vfield.selector);
305                             found = true;
306                         }
307                     }
308                 });
309
310                 if(found) return;
311             }
312
313             // then try the single object cache
314             if(this.cache[this.auth].single[lclass] && this.cache[this.auth].single[lclass][this.widgetValue]) {
315                 this.widgetValue = this.cache[this.auth].single[lclass][this.widgetValue];
316                 return;
317             }
318
319             console.log("Fetching sync object " + lclass + " : " + this.widgetValue);
320
321             // if those fail, fetch the linked object
322             this.async = true;
323             var self = this;
324             new openils.PermaCrud().retrieve(lclass, this.widgetValue, {   
325                 async : !this.forceSync,
326                 oncomplete : function(r) {
327                     var item = openils.Util.readResponse(r);
328                     var newvalue = item[linkInfo.vfield.selector]();
329
330                     if(!self.cache[self.auth].single[lclass])
331                         self.cache[self.auth].single[lclass] = {};
332                     self.cache[self.auth].single[lclass][self.widgetValue] = newvalue;
333
334                     self.widgetValue = newvalue;
335                     self.widget.startup();
336                     self._widgetLoaded();
337                 }
338             });
339         },
340
341         _getLinkSelector : function() {
342             var linkClass = this.idlField['class'];
343             if(this.idlField.reltype != 'has_a')  return false;
344             if(!fieldmapper.IDL.fmclasses[linkClass].permacrud) return false;
345             if(!fieldmapper.IDL.fmclasses[linkClass].permacrud.retrieve) return false;
346
347             var vfield;
348             var rclassIdl = fieldmapper.IDL.fmclasses[linkClass];
349
350             for(var f in rclassIdl.fields) {
351                 if(this.idlField.key == rclassIdl.fields[f].name) {
352                     vfield = rclassIdl.fields[f];
353                     break;
354                 }
355             }
356
357             if(!vfield) 
358                 throw new Error("'" + linkClass + "' has no '" + this.idlField.key + "' field!");
359
360             return {
361                 linkClass : linkClass,
362                 vfield : vfield
363             };
364         },
365
366         _buildLinkSelector : function() {
367             var self = this;
368             var selectorInfo = this._getLinkSelector();
369             if(!selectorInfo) return false;
370
371             var linkClass = selectorInfo.linkClass;
372             var vfield = selectorInfo.vfield;
373
374             this.async = true;
375
376             if(linkClass == 'pgt')
377                 return this._buildPermGrpSelector();
378             if(linkClass == 'aou')
379                 return this._buildOrgSelector();
380             if(linkClass == 'acpl')
381                 return this._buildCopyLocSelector();
382
383
384             dojo.require('dojo.data.ItemFileReadStore');
385             dojo.require('dijit.form.FilteringSelect');
386
387             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
388             this.widget.searchAttr = this.widget.labelAttr = vfield.selector || vfield.name;
389             this.widget.valueAttr = vfield.name;
390
391             var oncomplete = function(list) {
392
393                 if(self.labelFormat) 
394                     self.widget.labelAttr = '_label';
395
396                 if(self.searchFormat)
397                     self.widget.searchAttr = '_search';
398
399                 function formatString(item, formatList) {
400
401                     try {
402
403                         // formatList[1..*] are names of fields.  Pull the field
404                         // values from each object to determine the values for string substitution
405                         var values = [];
406                         var format = formatList[0];
407                         for(var i = 1; i< formatList.length; i++) 
408                             values.push(item[formatList[i]]);
409
410                         return dojo.string.substitute(format, values);
411
412                     } catch(E) {
413                         throw new Error(
414                             "openils.widget.AutoFieldWidget: Invalid formatList ["+formatList+"] : "+E);
415                     }
416
417                 }
418
419                 if(list) {
420                     var storeData = {data:fieldmapper[linkClass].toStoreData(list)};
421
422                     if(self.labelFormat) {
423                         dojo.forEach(storeData.data.items, 
424                             function(item) {
425                                 item._label = formatString(item, self.labelFormat);
426                             }
427                         );
428                     }
429
430                     if(self.searchFormat) {
431                         dojo.forEach(storeData.data.items, 
432                             function(item) {
433                                 item._search = formatString(item, self.searchFormat);
434                             }
435                         );
436                     }
437
438                     self.widget.store = new dojo.data.ItemFileReadStore(storeData);
439                     self.cache[self.auth].list[linkClass] = self.widget.store;
440
441                 } else {
442                     self.widget.store = self.cache[self.auth].list[linkClass];
443                 }
444
445                 self.widget.startup();
446                 self._widgetLoaded();
447             };
448
449             if(!this.noCache && this.cache[self.auth].list[linkClass]) {
450                 oncomplete();
451
452             } else {
453
454                 if(this.dataLoader) {
455
456                     // caller provided an external function for retrieving the data
457                     this.dataLoader(linkClass, this.searchFilter, oncomplete);
458
459                 } else {
460
461                     var _cb = function(r) {
462                         oncomplete(openils.Util.readResponse(r, false, true));
463                     };
464
465                     if (this.searchFilter) {
466                         new openils.PermaCrud().search(linkClass, this.searchFilter, {
467                             async : !this.forceSync, oncomplete : _cb
468                         });
469                     } else {
470                         new openils.PermaCrud().retrieveAll(linkClass, {
471                             async : !this.forceSync, oncomplete : _cb
472                         });
473                     }
474                 }
475             }
476
477             return true;
478         },
479
480         /**
481          * For widgets that run asynchronously, provide a callback for finishing up
482          */
483         _widgetLoaded : function(value) {
484             
485             if(this.readOnly) {
486
487                 /* -------------------------------------------------------------
488                    when using widgets in a grid, the cell may dissapear, which 
489                    kills the underlying DOM node, which causes this to fail.
490                    For now, back out gracefully and let grid getters use
491                    getDisplayString() instead
492                   -------------------------------------------------------------*/
493                 try { 
494                     this.baseWidgetValue(this.getDisplayString());
495                 } catch (E) {};
496
497             } else {
498
499                 this.baseWidgetValue(this.widgetValue);
500                 if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && (!this.selfReference && !this.noDisablePkey))
501                     this.widget.attr('disabled', true); 
502                 if(this.disableWidgetTest && this.disableWidgetTest(this.idlField.name, this.fmObject))
503                     this.widget.attr('disabled', true); 
504             }
505             if(this.onload)
506                 this.onload(this.widget, this);
507
508             if(!this.readOnly && this.dijitArgs && this.dijitArgs.required) {
509                 // a required dijit is not given any styling to indicate the value
510                 // is invalid until the user has focused the widget then left it with
511                 // invalid data.  This change tells dojo to pretend this focusing has 
512                 // already happened so we can style required widgets during page render.
513                 this.widget._hasBeenBlurred = true;
514                 this.widget.validate();
515             }
516         },
517
518         _buildOrgSelector : function() {
519             dojo.require('fieldmapper.OrgUtils');
520             dojo.require('openils.widget.FilteringTreeSelect');
521             this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
522             this.widget.searchAttr = 'shortname';
523             this.widget.labelAttr = 'shortname';
524             this.widget.parentField = 'parent_ou';
525             var user = new openils.User();
526
527             if(this.widgetValue == null && this.orgDefaultsToWs) 
528                 this.widgetValue = user.user.ws_ou();
529             
530             // if we have a limit perm, find the relevent orgs (async)
531             if(this.orgLimitPerms && this.orgLimitPerms.length > 0) {
532                 this.async = true;
533                 var self = this;
534                 user.getPermOrgList(this.orgLimitPerms, 
535                     function(orgList) {
536                         self.widget.tree = orgList;
537                         self.widget.startup();
538                         self._widgetLoaded();
539                     }
540                 );
541
542             } else {
543                 this.widget.tree = fieldmapper.aou.globalOrgTree;
544                 this.widget.startup();
545             }
546
547             return true;
548         },
549
550         _buildPermGrpSelector : function() {
551             dojo.require('openils.widget.FilteringTreeSelect');
552             this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
553             this.widget.searchAttr = 'name';
554
555             if(this.cache.permGrpTree) {
556                 this.widget.tree = this.cache.permGrpTree;
557                 this.widget.startup();
558                 return true;
559             } 
560
561             var self = this;
562             this.async = true;
563             new openils.PermaCrud().retrieveAll('pgt', {
564                 async : !this.forceSync,
565                 oncomplete : function(r) {
566                     var list = openils.Util.readResponse(r, false, true);
567                     if(!list) return;
568                     var map = {};
569                     var root = null;
570                     for(var l in list)
571                         map[list[l].id()] = list[l];
572                     for(var l in list) {
573                         var node = list[l];
574                         var pnode = map[node.parent()];
575                         if(!pnode) {root = node; continue;}
576                         if(!pnode.children()) pnode.children([]);
577                         pnode.children().push(node);
578                     }
579                     self.widget.tree = self.cache.permGrpTree = root;
580                     self.widget.startup();
581                     self._widgetLoaded();
582                 }
583             });
584
585             return true;
586         },
587
588         _buildCopyLocSelector : function() {
589             dojo.require('dijit.form.FilteringSelect');
590             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
591             this.widget.searchAttr = this.widget.labalAttr = 'name';
592             this.widget.valueAttr = 'id';
593
594             if(this.cache.copyLocStore) {
595                 this.widget.store = this.cache.copyLocStore;
596                 this.widget.startup();
597                 this.async = false;
598                 return true;
599             } 
600
601             // my orgs
602             var ws_ou = openils.User.user.ws_ou();
603             var orgs = fieldmapper.aou.findOrgUnit(ws_ou).orgNodeTrail().map(function (i) { return i.id() });
604             orgs = orgs.concat(fieldmapper.aou.descendantNodeList(ws_ou).map(function (i) { return i.id() }));
605
606             var self = this;
607             new openils.PermaCrud().search('acpl', {owning_lib : orgs}, {
608                 async : !this.forceSync,
609                 oncomplete : function(r) {
610                     var list = openils.Util.readResponse(r, false, true);
611                     if(!list) return;
612                     self.widget.store = 
613                         new dojo.data.ItemFileReadStore({data:fieldmapper.acpl.toStoreData(list)});
614                     self.cache.copyLocStore = self.widget.store;
615                     self.widget.startup();
616                     self._widgetLoaded();
617                 }
618             });
619
620             return true;
621         }
622     });
623
624     openils.widget.AutoFieldWidget.localeStrings = dojo.i18n.getLocalization("openils.widget", "AutoFieldWidget");
625     openils.widget.AutoFieldWidget.cache = {};
626 }
627