84c0a8b6d13dea044ab7f9e0223771f5f6bee8a7
[tine20] / tine20 / Tinebase / js / data / Record.js
1 /*
2  * Tine 2.0
3  * 
4  * @package     Tine
5  * @subpackage  Tinebase
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @author      Cornelius Weiss <c.weiss@metaways.de>
8  * @copyright   Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de)
9  */
10
11 Ext.ns('Tine.Tinebase', 'Tine.Tinebase.data');
12
13 Tine.Tinebase.data.Record = function(data, id) {
14     if (id || id === 0) {
15         this.id = id;
16     } else if (data[this.idProperty]) {
17         this.id = data[this.idProperty];
18     } else {
19         this.id = ++Ext.data.Record.AUTO_ID;
20     }
21     this.data = data;
22     this.ctime = new Date().getTime();
23 };
24
25 /**
26  * @namespace Tine.Tinebase.data
27  * @class     Tine.Tinebase.data.Record
28  * @extends   Ext.data.Record
29  * 
30  * Baseclass of Tine 2.0 models
31  */
32 Ext.extend(Tine.Tinebase.data.Record, Ext.data.Record, {
33     /**
34      * @cfg {String} appName
35      * internal/untranslated app name (required)
36      */
37     appName: null,
38     /**
39      * @cfg {String} modelName
40      * name of the model/record  (required)
41      */
42     modelName: null,
43     /**
44      * @cfg {String} idProperty
45      * property of the id of the record
46      */
47     idProperty: 'id',
48     /**
49      * @cfg {String} titleProperty
50      * property of the title attibute, used in generic getTitle function  (required)
51      */
52     titleProperty: null,
53     /**
54      * @cfg {String} recordName
55      * untranslated record/item name
56      */
57     recordName: 'record',
58     /**
59      * @cfg {String} recordName
60      * untranslated records/items (plural) name
61      */
62     recordsName: 'records',
63     /**
64      * @cfg {String} grantsPath
65      * path (see _.get() to find grants
66      */
67     grantsPath: null,
68     /**
69      * @cfg {String} containerProperty
70      * name of the container property
71      */
72     containerProperty: null,
73     /**
74      * @cfg {String} containerName
75      * untranslated container name
76      */
77     containerName: 'container',
78     /**
79      * @cfg {string} containerName
80      * untranslated name of container (plural)
81      */
82     containersName: 'containers',
83     /**
84      * default filter
85      * @type {string}
86      */
87     defaultFilter: null,
88     
89     cfExp: /^#(.+)/,
90     
91     /**
92      * Get the value of the {@link Ext.data.Field#name named field}.
93      * @param {String} name The {@link Ext.data.Field#name name of the field} to get the value of.
94      * @return {Object} The value of the field.
95      */
96     get: function(name) {
97         var cfName = String(name).match(this.cfExp);
98         
99         if (cfName) {
100             return this.data.customfields ? this.data.customfields[cfName[1]] : null;
101         }
102         
103         return this.data[name];
104     },
105     
106     /**
107      * Set the value of the {@link Ext.data.Field#name named field}.
108      * @param {String} name The {@link Ext.data.Field#name name of the field} to get the value of.
109      * @return {Object} The value of the field.
110      */
111     set : function(name, value) {
112         var encode = Ext.isPrimitive(value) ? String : Ext.encode,
113             current = this.get(name),
114             cfName;
115             
116         if (encode(current) == encode(value)) {
117             return;
118         }
119         this.dirty = true;
120         if (!this.modified) {
121             this.modified = {};
122         }
123         if (this.modified[name] === undefined) {
124             this.modified[name] = current;
125         }
126         if (encode(value) == encode(this.modified[name])) {
127             delete this.modified[name];
128         }
129         if (Object.keys(this.modified).length === 0) {
130             this.dirty = false;
131         }
132         if (cfName = String(name).match(this.cfExp)) {
133             var oldValueJSON = JSON.stringify(this.get('customfields') || {}),
134                 valueObject = JSON.parse(oldValueJSON);
135
136             Tine.Tinebase.common.assertComparable(valueObject);
137             valueObject[cfName[1]] = value;
138
139             if (JSON.stringify(valueObject) != oldValueJSON) {
140                 this.set('customfields', valueObject);
141             }
142         } else {
143             this.data[name] = value;
144         }
145         
146         if (!this.editing) {
147             this.afterEdit();
148         }
149     },
150     
151     /**
152      * returns title of this record
153      * 
154      * @return {String}
155      */
156     getTitle: function() {
157         if (Tine.Tinebase.data.TitleRendererManager.has(this.appName, this.modelName)) {
158             return Tine.Tinebase.data.TitleRendererManager.get(this.appName, this.modelName)(this);
159         } else {
160             var s = this.titleProperty ? this.titleProperty.split('.') : [null];
161             return (s.length > 0 && this.get(s[0]) && this.get(s[0])[s[1]]) ? this.get(s[0])[s[1]] : s[0] ? this.get(this.titleProperty) : '';
162         }
163     },
164     /**
165      * returns the id of the record
166      */
167     getId: function() {
168         return this.get(this.idProperty ? this.idProperty : 'id');
169     },
170
171     /**
172      * sets the id of the record
173      *
174      * @param {String} id
175      */
176     setId: function(id) {
177         return this.set(this.idProperty ? this.idProperty : 'id', id);
178     },
179
180     /**
181      * converts data to String
182      * 
183      * @return {String}
184      */
185     toString: function() {
186         return Ext.encode(this.data);
187     },
188     
189     /**
190      * returns true if given record obsoletes this one
191      * 
192      * - returns false if record has no modlog properties to make 
193      *   handling of updated records work in the grid panel
194      * @see 0009464: user grid does not refresh after ctx menu action
195      * 
196      * @param {Tine.Tinebase.data.Record} record
197      * @return {Boolean}
198      */
199     isObsoletedBy: function(record) {
200         if (record.modelName !== this.modelName || record.getId() !== this.getId()) {
201             throw new Ext.Error('Records could not be compared');
202         }
203         
204         if (this.constructor.hasField('seq') && record.get('seq') != this.get('seq')) {
205             return record.get('seq') > this.get('seq');
206         }
207         
208         return (this.constructor.hasField('last_modified_time')) ? record.get('last_modified_time') > this.get('last_modified_time') : false;
209     }
210 });
211
212 /**
213  * Generate a constructor for a specific Record layout.
214  * 
215  * @param {Array} def see {@link Ext.data.Record#create}
216  * @param {Object} meta information see {@link Tine.Tinebase.data.Record}
217  * 
218  * <br>usage:<br>
219 <b>IMPORTANT: the ngettext comments are required for the translation system!</b>
220 <pre><code>
221 var TopicRecord = Tine.Tinebase.data.Record.create([
222     {name: 'summary', mapping: 'topic_title'},
223     {name: 'details', mapping: 'username'}
224 ], {
225     appName: 'Tasks',
226     modelName: 'Task',
227     idProperty: 'id',
228     titleProperty: 'summary',
229     // ngettext('Task', 'Tasks', n);
230     recordName: 'Task',
231     recordsName: 'Tasks',
232     containerProperty: 'container_id',
233     // ngettext('to do list', 'to do lists', n);
234     containerName: 'to do list',
235     containesrName: 'to do lists'
236 });
237 </code></pre>
238  * @static
239  */
240 Tine.Tinebase.data.Record.create = function(o, meta) {
241     var f = Ext.extend(Tine.Tinebase.data.Record, {});
242     var p = f.prototype;
243     Ext.apply(p, meta);
244     p.fields = new Ext.util.MixedCollection(false, function(field) {
245         return field.name;
246     });
247     for(var i = 0, len = o.length; i < len; i++) {
248         if (o[i]['name'] == meta.containerProperty && meta.allowBlankContainer === false) {
249             o[i]['allowBlank'] = false;
250         }
251         p.fields.add(new Ext.data.Field(o[i]));
252     }
253     f.getField = function(name) {
254         return p.fields.get(name);
255     };
256     f.getMeta = function(name) {
257         var value = null;
258         switch(name) {
259             case ('phpClassName'):
260                 value = p.appName + '_Model_' + p.modelName;
261                 break;
262             default:
263                 value = p[name];
264         }
265         return value;
266     };
267     f.getDefaultData = function() {
268         return {};
269     };
270     f.getFieldDefinitions = function() {
271         return p.fields.items;
272     };
273     f.getFieldNames = function() {
274         if (! p.fieldsarray) {
275             var arr = p.fieldsarray = [];
276             Ext.each(p.fields.items, function(item) {arr.push(item.name);});
277         }
278         return p.fieldsarray;
279     };
280     f.hasField = function(n) {
281         return p.fields.indexOfKey(n) >= 0;
282     };
283     f.getRecordName = function() {
284         var app = Tine.Tinebase.appMgr.get(p.appName),
285             i18n = app && app.i18n ? app.i18n :i18n;
286             
287         return i18n.n_(p.recordName, p.recordsName, 1);
288     };
289     f.getRecordsName = function() {
290         var app = Tine.Tinebase.appMgr.get(p.appName),
291             i18n = app && app.i18n ? app.i18n :i18n;
292             
293         return i18n.n_(p.recordName, p.recordsName, 50);
294     };
295     f.getContainerName = function() {
296         var app = Tine.Tinebase.appMgr.get(p.appName),
297             i18n = app && app.i18n ? app.i18n :i18n;
298             
299         return i18n.n_(p.containerName, p.containersName, 1);
300     };
301     f.getContainersName = function() {
302         var app = Tine.Tinebase.appMgr.get(p.appName),
303             i18n = app && app.i18n ? app.i18n :i18n;
304             
305         return i18n.n_(p.containerName, p.containersName, 50);
306     };
307     f.getAppName = function() {
308         return Tine.Tinebase.appMgr.get(p.appName).i18n._(p.appName);
309     };
310     /**
311      * returns the php class name of the record itself or by the application(name) and model(name)
312      * @param {mixed} app       the application instance or the application name or the record class
313      * @param {mixed} model     the model name
314      * @return {String} php class name
315      */
316     f.getPhpClassName = function(app, model) {
317         // without arguments the php class name of the this is returned
318         if (!app && !model) {
319             return f.getMeta('phpClassName');
320         }
321         // if var app is a record class, the getMeta method is called
322         if (Ext.isFunction(app.getMeta)) {
323             return app.getMeta('phpClassName');
324         }
325
326         var appName = (Ext.isObject(app) && app.hasOwnProperty('name')) ? app.name : app;
327         return appName + '_Model_' + model;
328     };
329     f.getModelConfiguration = function() {
330         return p.modelConfiguration;
331     };
332     
333     // sanitize containerProperty label
334     var containerProperty = f.getMeta('containerProperty');
335     if (containerProperty) {
336         var field = p.fields.get(containerProperty);
337         if (field) {
338             field.label = p.containerName;
339         }
340     }
341     if (!p.grantsPath) {
342         p.grantsPath = 'data' + (containerProperty ? ('.' + containerProperty) : '') + '.account_grants';
343     }
344     Tine.Tinebase.data.RecordMgr.add(f);
345     return f;
346 };
347
348 Tine.Tinebase.data.Record.generateUID = function(length) {
349     length = length || 40;
350         
351     var s = '0123456789abcdef',
352         uuid = new Array(length);
353     for(var i=0; i<length; i++) {
354         uuid[i] = s.charAt(Math.ceil(Math.random() *15));
355     }
356     return uuid.join('');
357 };
358
359 Tine.Tinebase.data.RecordManager = Ext.extend(Ext.util.MixedCollection, {
360     add: function(record) {
361         if (! Ext.isFunction(record.getMeta)) {
362             throw new Ext.Error('only records of type Tinebase.data.Record could be added');
363         }
364         var appName = record.getMeta('appName'),
365             modelName = record.getMeta('modelName');
366             
367         if (! appName && modelName) {
368             throw new Ext.Error('appName and modelName must be in the metadatas');
369         }
370         
371 //        console.log('register model "' + appName + '.' + modelName + '"');
372         Tine.Tinebase.data.RecordManager.superclass.add.call(this, appName + '.' + modelName, record);
373     },
374     
375     get: function(appName, modelName) {
376         if (Ext.isFunction(appName.getMeta)) {
377             return appName;
378         }
379         if (! modelName && appName.modelName) {
380             modelName = appName.modelName;
381         }
382         if (appName.appName) {
383             appName = appName.appName;
384         }
385             
386         if (! Ext.isString(appName)) {
387             throw new Ext.Error('appName must be a string');
388         }
389         
390         Ext.each([appName, modelName], function(what) {
391             if (! Ext.isString(what)) return;
392             var parts = what.split(/(?:_Model_)|(?:\.)/);
393             if (parts.length > 1) {
394                 appName = parts[0];
395                 modelName = parts[1];
396             }
397         });
398         
399         return Tine.Tinebase.data.RecordManager.superclass.get.call(this, appName + '.' + modelName);
400     }
401 });
402 Tine.Tinebase.data.RecordMgr = new Tine.Tinebase.data.RecordManager(true);
403
404 /**
405  * create record from json string
406  *
407  * @param {String} json
408  * @param {Tine.Tinebase.data.Record} recordClass
409  * @returns {Tine.Tinebase.data.Record}
410  */
411 Tine.Tinebase.data.Record.setFromJson = function(json, recordClass) {
412     if (! Ext.isString(json)) {
413         throw new Ext.Error('not a string');
414     }
415
416     var jsonReader = new Ext.data.JsonReader({
417         id: recordClass.idProperty,
418         root: 'results',
419         totalProperty: 'totalcount'
420     }, recordClass);
421
422     var recordData = Ext.util.JSON.decode('{"results": [' + json + ']}'),
423         data = jsonReader.readRecords(recordData),
424         record = data.records[0],
425         recordId = record.get(record.idProperty);
426
427     record.id = recordId ? recordId : 0;
428
429     return record;
430 };