]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js
Kludge to fix ISO timestamp parsing in AutoFieldWidget
[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  null :
141                         case 'unset' : return openils.widget.AutoFieldWidget.localeStrings.UNSET;
142                         case true : return openils.widget.AutoFieldWidget.localeStrings.TRUE; 
143                         default: return openils.widget.AutoFieldWidget.localeStrings.FALSE;
144                     }
145                 case 'timestamp':
146                     if (!value) return '';
147                     dojo.require('dojo.date.locale');
148                     dojo.require('dojo.date.stamp');
149                     var date = dojo.date.stamp.fromISOString(value);
150                     return dojo.date.locale.format(date, {formatLength:'short'});
151                 case 'org_unit':
152                     if(value === null || value === undefined) return '';
153                     return fieldmapper.aou.findOrgUnit(value).shortname();
154                 case 'int':
155                 case 'float':
156                     if(isNaN(value)) value = 0;
157                 default:
158                     if(value === undefined || value === null)
159                         value = '';
160                     return value+'';
161             }
162         },
163
164         build : function(onload) {
165
166             if(this.widgetValue == null)
167                 this.widgetValue = (this.fmObject) ? this.fmObject[this.idlField.name]() : null;
168
169             if(this.widget) {
170                 // core widget provided for us, attach and move on
171                 if(this.parentNode) // may already be in the "right" place
172                     this.parentNode.appendChild(this.widget.domNode);
173                 if(this.widget.attr('value') == null)
174                     this._widgetLoaded();
175                 return;
176             }
177             
178             if(!this.parentNode) // give it somewhere to live so that dojo won't complain
179                 this.parentNode = dojo.create('div');
180
181             this.onload = onload;
182
183             if(this.readOnly) {
184                 dojo.require('dijit.layout.ContentPane');
185                 this.widget = new dijit.layout.ContentPane(this.dijitArgs, this.parentNode);
186                 if(this.widgetValue !== null)
187                     this._tryLinkedDisplayField();
188
189             } else if(this.widgetClass) {
190                 dojo.require(this.widgetClass);
191                 eval('this.widget = new ' + this.widgetClass + '(this.dijitArgs, this.parentNode);');
192
193             } else {
194
195                 switch(this.idlField.datatype) {
196                     
197                     case 'id':
198                         dojo.require('dijit.form.TextBox');
199                         this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
200                         break;
201
202                     case 'org_unit':
203                         this._buildOrgSelector();
204                         break;
205
206                     case 'money':
207                         dojo.require('dijit.form.CurrencyTextBox');
208                         this.widget = new dijit.form.CurrencyTextBox(this.dijitArgs, this.parentNode);
209                         break;
210
211                     case 'int':
212                         dojo.require('dijit.form.NumberTextBox');
213                         this.dijitArgs = dojo.mixin(this.dijitArgs || {}, {constraints:{places:0}});
214                         this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
215                         break;
216
217                     case 'float':
218                         dojo.require('dijit.form.NumberTextBox');
219                         this.widget = new dijit.form.NumberTextBox(this.dijitArgs, this.parentNode);
220                         break;
221
222                     case 'timestamp':
223                         dojo.require('dijit.form.DateTextBox');
224                         dojo.require('dojo.date.stamp');
225                         this.widget = new dijit.form.DateTextBox(this.dijitArgs, this.parentNode);
226                         if(this.widgetValue != null) 
227                             this.widgetValue = dojo.date.stamp.fromISOString(
228                                 // Kludge until the ML returning ISO timestamps with a colon in the timezone offset,
229                                 // which dojo.date.stamp.fromISOString requires
230                                 this.widgetValue.replace( /^(\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d-\d\d)(\d\d)$/, '$1:$2') 
231                             );
232                         break;
233
234                     case 'bool':
235                         if(this.ternary) {
236                             dojo.require('dijit.form.FilteringSelect');
237                             var store = new dojo.data.ItemFileReadStore({
238                                 data:{
239                                     identifier : 'value',
240                                     items:[
241                                         {label : openils.widget.AutoFieldWidget.localeStrings.UNSET, value : 'unset'},
242                                         {label : openils.widget.AutoFieldWidget.localeStrings.TRUE, value : 'true'},
243                                         {label : openils.widget.AutoFieldWidget.localeStrings.FALSE, value : 'false'}
244                                     ]
245                                 }
246                             });
247                             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
248                             this.widget.searchAttr = this.widget.labelAttr = 'label';
249                             this.widget.valueAttr = 'value';
250                             this.widget.store = store;
251                             this.widget.startup();
252                             this.widgetValue = (this.widgetValue === null) ? 'unset' : 
253                                 (openils.Util.isTrue(this.widgetValue)) ? 'true' : 'false';
254                         } else {
255                             dojo.require('dijit.form.CheckBox');
256                             this.widget = new dijit.form.CheckBox(this.dijitArgs, this.parentNode);
257                             this.widgetValue = openils.Util.isTrue(this.widgetValue);
258                         }
259                         break;
260
261                     case 'link':
262                         if(this._buildLinkSelector()) break;
263
264                     default:
265                         if(this.dijitArgs && (this.dijitArgs.required || this.dijitArgs.regExp)) {
266                             dojo.require('dijit.form.ValidationTextBox');
267                             this.widget = new dijit.form.ValidationTextBox(this.dijitArgs, this.parentNode);
268                         } else {
269                             dojo.require('dijit.form.TextBox');
270                             this.widget = new dijit.form.TextBox(this.dijitArgs, this.parentNode);
271                         }
272                 }
273             }
274
275             if(!this.async) this._widgetLoaded();
276             return this.widget;
277         },
278
279         // we want to display the value for our widget.  However, instead of displaying
280         // an ID, for exmaple, display the value for the 'selector' field on the object
281         // the ID points to
282         _tryLinkedDisplayField : function(noAsync) {
283
284             if(this.idlField.datatype == 'org_unit')
285                 return false; // we already handle org_units, no need to re-fetch
286
287             // user opted to bypass fetching this linked data
288             if(this.suppressLinkedFields.indexOf(this.idlField.name) > -1)
289                 return false;
290
291             var linkInfo = this._getLinkSelector();
292             if(!(linkInfo && linkInfo.vfield && linkInfo.vfield.selector)) 
293                 return false;
294             var lclass = linkInfo.linkClass;
295
296             if(lclass == 'aou') 
297                 return false;
298
299             // first try the store cache
300             var self = this;
301             if(this.cache[this.auth].list[lclass]) {
302                 var store = this.cache[this.auth].list[lclass];
303                 var query = {};
304                 query[linkInfo.vfield.name] = ''+this.widgetValue;
305                 var found = false;
306                 store.fetch({query:query, onComplete:
307                     function(list) {
308                         if(list[0]) {
309                             self.widgetValue = store.getValue(list[0], linkInfo.vfield.selector);
310                             found = true;
311                         }
312                     }
313                 });
314
315                 if(found) return;
316             }
317
318             // then try the single object cache
319             if(this.cache[this.auth].single[lclass] && this.cache[this.auth].single[lclass][this.widgetValue]) {
320                 this.widgetValue = this.cache[this.auth].single[lclass][this.widgetValue];
321                 return;
322             }
323
324             console.log("Fetching sync object " + lclass + " : " + this.widgetValue);
325
326             // if those fail, fetch the linked object
327             this.async = true;
328             var self = this;
329             new openils.PermaCrud().retrieve(lclass, this.widgetValue, {   
330                 async : !this.forceSync,
331                 oncomplete : function(r) {
332                     var item = openils.Util.readResponse(r);
333                     var newvalue = item[linkInfo.vfield.selector]();
334
335                     if(!self.cache[self.auth].single[lclass])
336                         self.cache[self.auth].single[lclass] = {};
337                     self.cache[self.auth].single[lclass][self.widgetValue] = newvalue;
338
339                     self.widgetValue = newvalue;
340                     self.widget.startup();
341                     self._widgetLoaded();
342                 }
343             });
344         },
345
346         _getLinkSelector : function() {
347             var linkClass = this.idlField['class'];
348             if(this.idlField.reltype != 'has_a')  return false;
349             if(!fieldmapper.IDL.fmclasses[linkClass].permacrud) return false;
350             if(!fieldmapper.IDL.fmclasses[linkClass].permacrud.retrieve) return false;
351
352             var vfield;
353             var rclassIdl = fieldmapper.IDL.fmclasses[linkClass];
354
355             for(var f in rclassIdl.fields) {
356                 if(this.idlField.key == rclassIdl.fields[f].name) {
357                     vfield = rclassIdl.fields[f];
358                     break;
359                 }
360             }
361
362             if(!vfield) 
363                 throw new Error("'" + linkClass + "' has no '" + this.idlField.key + "' field!");
364
365             return {
366                 linkClass : linkClass,
367                 vfield : vfield
368             };
369         },
370
371         _buildLinkSelector : function() {
372             var self = this;
373             var selectorInfo = this._getLinkSelector();
374             if(!selectorInfo) return false;
375
376             var linkClass = selectorInfo.linkClass;
377             var vfield = selectorInfo.vfield;
378
379             this.async = true;
380
381             if(linkClass == 'pgt')
382                 return this._buildPermGrpSelector();
383             if(linkClass == 'aou')
384                 return this._buildOrgSelector();
385             if(linkClass == 'acpl')
386                 return this._buildCopyLocSelector();
387
388
389             dojo.require('dojo.data.ItemFileReadStore');
390             dojo.require('dijit.form.FilteringSelect');
391
392             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
393             this.widget.searchAttr = this.widget.labelAttr = vfield.selector || vfield.name;
394             this.widget.valueAttr = vfield.name;
395
396             var oncomplete = function(list) {
397
398                 if(self.labelFormat) 
399                     self.widget.labelAttr = '_label';
400
401                 if(self.searchFormat)
402                     self.widget.searchAttr = '_search';
403
404                 function formatString(item, formatList) {
405
406                     try {
407
408                         // formatList[1..*] are names of fields.  Pull the field
409                         // values from each object to determine the values for string substitution
410                         var values = [];
411                         var format = formatList[0];
412                         for(var i = 1; i< formatList.length; i++) 
413                             values.push(item[formatList[i]]);
414
415                         return dojo.string.substitute(format, values);
416
417                     } catch(E) {
418                         throw new Error(
419                             "openils.widget.AutoFieldWidget: Invalid formatList ["+formatList+"] : "+E);
420                     }
421
422                 }
423
424                 if(list) {
425                     var storeData = {data:fieldmapper[linkClass].toStoreData(list)};
426
427                     if(self.labelFormat) {
428                         dojo.forEach(storeData.data.items, 
429                             function(item) {
430                                 item._label = formatString(item, self.labelFormat);
431                             }
432                         );
433                     }
434
435                     if(self.searchFormat) {
436                         dojo.forEach(storeData.data.items, 
437                             function(item) {
438                                 item._search = formatString(item, self.searchFormat);
439                             }
440                         );
441                     }
442
443                     self.widget.store = new dojo.data.ItemFileReadStore(storeData);
444                     self.cache[self.auth].list[linkClass] = self.widget.store;
445
446                 } else {
447                     self.widget.store = self.cache[self.auth].list[linkClass];
448                 }
449
450                 self.widget.startup();
451                 self._widgetLoaded();
452             };
453
454             if(!this.noCache && this.cache[self.auth].list[linkClass]) {
455                 oncomplete();
456
457             } else {
458
459                 if(this.dataLoader) {
460
461                     // caller provided an external function for retrieving the data
462                     this.dataLoader(linkClass, this.searchFilter, oncomplete);
463
464                 } else {
465
466                     var _cb = function(r) {
467                         oncomplete(openils.Util.readResponse(r, false, true));
468                     };
469
470                     if (this.searchFilter) {
471                         new openils.PermaCrud().search(linkClass, this.searchFilter, {
472                             async : !this.forceSync, oncomplete : _cb
473                         });
474                     } else {
475                         new openils.PermaCrud().retrieveAll(linkClass, {
476                             async : !this.forceSync, oncomplete : _cb
477                         });
478                     }
479                 }
480             }
481
482             return true;
483         },
484
485         /**
486          * For widgets that run asynchronously, provide a callback for finishing up
487          */
488         _widgetLoaded : function(value) {
489             
490             if(this.readOnly) {
491
492                 /* -------------------------------------------------------------
493                    when using widgets in a grid, the cell may dissapear, which 
494                    kills the underlying DOM node, which causes this to fail.
495                    For now, back out gracefully and let grid getters use
496                    getDisplayString() instead
497                   -------------------------------------------------------------*/
498                 try { 
499                     this.baseWidgetValue(this.getDisplayString());
500                 } catch (E) {};
501
502             } else {
503
504                 this.baseWidgetValue(this.widgetValue);
505                 if(this.idlField.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence && (!this.selfReference && !this.noDisablePkey))
506                     this.widget.attr('disabled', true); 
507                 if(this.disableWidgetTest && this.disableWidgetTest(this.idlField.name, this.fmObject))
508                     this.widget.attr('disabled', true); 
509             }
510             if(this.onload)
511                 this.onload(this.widget, this);
512
513             if(!this.readOnly && this.dijitArgs && this.dijitArgs.required) {
514                 // a required dijit is not given any styling to indicate the value
515                 // is invalid until the user has focused the widget then left it with
516                 // invalid data.  This change tells dojo to pretend this focusing has 
517                 // already happened so we can style required widgets during page render.
518                 this.widget._hasBeenBlurred = true;
519                 this.widget.validate();
520             }
521         },
522
523         _buildOrgSelector : function() {
524             dojo.require('fieldmapper.OrgUtils');
525             dojo.require('openils.widget.FilteringTreeSelect');
526             this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
527             this.widget.searchAttr = 'shortname';
528             this.widget.labelAttr = 'shortname';
529             this.widget.parentField = 'parent_ou';
530             var user = new openils.User();
531
532             if(this.widgetValue == null && this.orgDefaultsToWs) 
533                 this.widgetValue = user.user.ws_ou();
534             
535             // if we have a limit perm, find the relevent orgs (async)
536             if(this.orgLimitPerms && this.orgLimitPerms.length > 0) {
537                 this.async = true;
538                 var self = this;
539                 user.getPermOrgList(this.orgLimitPerms, 
540                     function(orgList) {
541                         self.widget.tree = orgList;
542                         self.widget.startup();
543                         self._widgetLoaded();
544                     }
545                 );
546
547             } else {
548                 this.widget.tree = fieldmapper.aou.globalOrgTree;
549                 this.widget.startup();
550             }
551
552             return true;
553         },
554
555         _buildPermGrpSelector : function() {
556             dojo.require('openils.widget.FilteringTreeSelect');
557             this.widget = new openils.widget.FilteringTreeSelect(this.dijitArgs, this.parentNode);
558             this.widget.searchAttr = 'name';
559
560             if(this.cache.permGrpTree) {
561                 this.widget.tree = this.cache.permGrpTree;
562                 this.widget.startup();
563                 return true;
564             } 
565
566             var self = this;
567             this.async = true;
568             new openils.PermaCrud().retrieveAll('pgt', {
569                 async : !this.forceSync,
570                 oncomplete : function(r) {
571                     var list = openils.Util.readResponse(r, false, true);
572                     if(!list) return;
573                     var map = {};
574                     var root = null;
575                     for(var l in list)
576                         map[list[l].id()] = list[l];
577                     for(var l in list) {
578                         var node = list[l];
579                         var pnode = map[node.parent()];
580                         if(!pnode) {root = node; continue;}
581                         if(!pnode.children()) pnode.children([]);
582                         pnode.children().push(node);
583                     }
584                     self.widget.tree = self.cache.permGrpTree = root;
585                     self.widget.startup();
586                     self._widgetLoaded();
587                 }
588             });
589
590             return true;
591         },
592
593         _buildCopyLocSelector : function() {
594             dojo.require('dijit.form.FilteringSelect');
595             this.widget = new dijit.form.FilteringSelect(this.dijitArgs, this.parentNode);
596             this.widget.searchAttr = this.widget.labalAttr = 'name';
597             this.widget.valueAttr = 'id';
598
599             if(this.cache.copyLocStore) {
600                 this.widget.store = this.cache.copyLocStore;
601                 this.widget.startup();
602                 this.async = false;
603                 return true;
604             } 
605
606             // my orgs
607             var ws_ou = openils.User.user.ws_ou();
608             var orgs = fieldmapper.aou.findOrgUnit(ws_ou).orgNodeTrail().map(function (i) { return i.id() });
609             orgs = orgs.concat(fieldmapper.aou.descendantNodeList(ws_ou).map(function (i) { return i.id() }));
610
611             var self = this;
612             new openils.PermaCrud().search('acpl', {owning_lib : orgs}, {
613                 async : !this.forceSync,
614                 oncomplete : function(r) {
615                     var list = openils.Util.readResponse(r, false, true);
616                     if(!list) return;
617                     self.widget.store = 
618                         new dojo.data.ItemFileReadStore({data:fieldmapper.acpl.toStoreData(list)});
619                     self.cache.copyLocStore = self.widget.store;
620                     self.widget.startup();
621                     self._widgetLoaded();
622                 }
623             });
624
625             return true;
626         }
627     });
628
629     openils.widget.AutoFieldWidget.localeStrings = dojo.i18n.getLocalization("openils.widget", "AutoFieldWidget");
630     openils.widget.AutoFieldWidget.cache = {};
631 }
632