Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / data / Model.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @author Ed Spencer
17  *
18  * A Model represents some object that your application manages. For example, one might define a Model for Users,
19  * Products, Cars, or any other real-world object that we want to model in the system. Models are registered via the
20  * {@link Ext.ModelManager model manager}, and are used by {@link Ext.data.Store stores}, which are in turn used by many
21  * of the data-bound components in Ext.
22  *
23  * Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:
24  *
25  *     Ext.define('User', {
26  *         extend: 'Ext.data.Model',
27  *         fields: [
28  *             {name: 'name',  type: 'string'},
29  *             {name: 'age',   type: 'int'},
30  *             {name: 'phone', type: 'string'},
31  *             {name: 'alive', type: 'boolean', defaultValue: true}
32  *         ],
33  *
34  *         changeName: function() {
35  *             var oldName = this.get('name'),
36  *                 newName = oldName + " The Barbarian";
37  *
38  *             this.set('name', newName);
39  *         }
40  *     });
41  *
42  * The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link
43  * Ext.ModelManager ModelManager}, and all other functions and properties are copied to the new Model's prototype.
44  *
45  * Now we can create instances of our User model and call any model logic we defined:
46  *
47  *     var user = Ext.create('User', {
48  *         name : 'Conan',
49  *         age  : 24,
50  *         phone: '555-555-5555'
51  *     });
52  *
53  *     user.changeName();
54  *     user.get('name'); //returns "Conan The Barbarian"
55  *
56  * # Validations
57  *
58  * Models have built-in support for validations, which are executed against the validator functions in {@link
59  * Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to
60  * models:
61  *
62  *     Ext.define('User', {
63  *         extend: 'Ext.data.Model',
64  *         fields: [
65  *             {name: 'name',     type: 'string'},
66  *             {name: 'age',      type: 'int'},
67  *             {name: 'phone',    type: 'string'},
68  *             {name: 'gender',   type: 'string'},
69  *             {name: 'username', type: 'string'},
70  *             {name: 'alive',    type: 'boolean', defaultValue: true}
71  *         ],
72  *
73  *         validations: [
74  *             {type: 'presence',  field: 'age'},
75  *             {type: 'length',    field: 'name',     min: 2},
76  *             {type: 'inclusion', field: 'gender',   list: ['Male', 'Female']},
77  *             {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']},
78  *             {type: 'format',    field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
79  *         ]
80  *     });
81  *
82  * The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors}
83  * object:
84  *
85  *     var instance = Ext.create('User', {
86  *         name: 'Ed',
87  *         gender: 'Male',
88  *         username: 'edspencer'
89  *     });
90  *
91  *     var errors = instance.validate();
92  *
93  * # Associations
94  *
95  * Models can have associations with other Models via {@link Ext.data.BelongsToAssociation belongsTo} and {@link
96  * Ext.data.HasManyAssociation hasMany} associations. For example, let's say we're writing a blog administration
97  * application which deals with Users, Posts and Comments. We can express the relationships between these models like
98  * this:
99  *
100  *     Ext.define('Post', {
101  *         extend: 'Ext.data.Model',
102  *         fields: ['id', 'user_id'],
103  *
104  *         belongsTo: 'User',
105  *         hasMany  : {model: 'Comment', name: 'comments'}
106  *     });
107  *
108  *     Ext.define('Comment', {
109  *         extend: 'Ext.data.Model',
110  *         fields: ['id', 'user_id', 'post_id'],
111  *
112  *         belongsTo: 'Post'
113  *     });
114  *
115  *     Ext.define('User', {
116  *         extend: 'Ext.data.Model',
117  *         fields: ['id'],
118  *
119  *         hasMany: [
120  *             'Post',
121  *             {model: 'Comment', name: 'comments'}
122  *         ]
123  *     });
124  *
125  * See the docs for {@link Ext.data.BelongsToAssociation} and {@link Ext.data.HasManyAssociation} for details on the
126  * usage and configuration of associations. Note that associations can also be specified like this:
127  *
128  *     Ext.define('User', {
129  *         extend: 'Ext.data.Model',
130  *         fields: ['id'],
131  *
132  *         associations: [
133  *             {type: 'hasMany', model: 'Post',    name: 'posts'},
134  *             {type: 'hasMany', model: 'Comment', name: 'comments'}
135  *         ]
136  *     });
137  *
138  * # Using a Proxy
139  *
140  * Models are great for representing types of data and relationships, but sooner or later we're going to want to load or
141  * save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, which
142  * can be set directly on the Model:
143  *
144  *     Ext.define('User', {
145  *         extend: 'Ext.data.Model',
146  *         fields: ['id', 'name', 'email'],
147  *
148  *         proxy: {
149  *             type: 'rest',
150  *             url : '/users'
151  *         }
152  *     });
153  *
154  * Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save data to and from a
155  * RESTful backend. Let's see how this works:
156  *
157  *     var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
158  *
159  *     user.save(); //POST /users
160  *
161  * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this Model's
162  * data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't have an id,
163  * and performs the appropriate action - in this case issuing a POST request to the url we configured (/users). We
164  * configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
165  *
166  * Loading data via the Proxy is equally easy:
167  *
168  *     //get a reference to the User model class
169  *     var User = Ext.ModelManager.getModel('User');
170  *
171  *     //Uses the configured RestProxy to make a GET request to /users/123
172  *     User.load(123, {
173  *         success: function(user) {
174  *             console.log(user.getId()); //logs 123
175  *         }
176  *     });
177  *
178  * Models can also be updated and destroyed easily:
179  *
180  *     //the user Model we loaded in the last snippet:
181  *     user.set('name', 'Edward Spencer');
182  *
183  *     //tells the Proxy to save the Model. In this case it will perform a PUT request to /users/123 as this Model already has an id
184  *     user.save({
185  *         success: function() {
186  *             console.log('The User was updated');
187  *         }
188  *     });
189  *
190  *     //tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
191  *     user.destroy({
192  *         success: function() {
193  *             console.log('The User was destroyed!');
194  *         }
195  *     });
196  *
197  * # Usage in Stores
198  *
199  * It is very common to want to load a set of Model instances to be displayed and manipulated in the UI. We do this by
200  * creating a {@link Ext.data.Store Store}:
201  *
202  *     var store = Ext.create('Ext.data.Store', {
203  *         model: 'User'
204  *     });
205  *
206  *     //uses the Proxy we set up on Model to load the Store data
207  *     store.load();
208  *
209  * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain a
210  * set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the {@link
211  * Ext.data.Store Store docs} for more information on Stores.
212  *
213  * @constructor
214  * Creates new Model instance.
215  * @param {Object} data An object containing keys corresponding to this model's fields, and their associated values
216  * @param {Number} id (optional) Unique ID to assign to this model instance
217  */
218 Ext.define('Ext.data.Model', {
219     alternateClassName: 'Ext.data.Record',
220
221     mixins: {
222         observable: 'Ext.util.Observable'
223     },
224
225     requires: [
226         'Ext.ModelManager',
227         'Ext.data.IdGenerator',
228         'Ext.data.Field',
229         'Ext.data.Errors',
230         'Ext.data.Operation',
231         'Ext.data.validations',
232         'Ext.data.proxy.Ajax',
233         'Ext.util.MixedCollection'
234     ],
235
236     onClassExtended: function(cls, data) {
237         var onBeforeClassCreated = data.onBeforeClassCreated;
238
239         data.onBeforeClassCreated = function(cls, data) {
240             var me = this,
241                 name = Ext.getClassName(cls),
242                 prototype = cls.prototype,
243                 superCls = cls.prototype.superclass,
244
245                 validations = data.validations || [],
246                 fields = data.fields || [],
247                 associations = data.associations || [],
248                 belongsTo = data.belongsTo,
249                 hasMany = data.hasMany,
250                 idgen = data.idgen,
251
252                 fieldsMixedCollection = new Ext.util.MixedCollection(false, function(field) {
253                     return field.name;
254                 }),
255
256                 associationsMixedCollection = new Ext.util.MixedCollection(false, function(association) {
257                     return association.name;
258                 }),
259
260                 superValidations = superCls.validations,
261                 superFields = superCls.fields,
262                 superAssociations = superCls.associations,
263
264                 association, i, ln,
265                 dependencies = [];
266
267             // Save modelName on class and its prototype
268             cls.modelName = name;
269             prototype.modelName = name;
270
271             // Merge the validations of the superclass and the new subclass
272             if (superValidations) {
273                 validations = superValidations.concat(validations);
274             }
275
276             data.validations = validations;
277
278             // Merge the fields of the superclass and the new subclass
279             if (superFields) {
280                 fields = superFields.items.concat(fields);
281             }
282
283             for (i = 0, ln = fields.length; i < ln; ++i) {
284                 fieldsMixedCollection.add(new Ext.data.Field(fields[i]));
285             }
286
287             data.fields = fieldsMixedCollection;
288
289             if (idgen) {
290                 data.idgen = Ext.data.IdGenerator.get(idgen);
291             }
292
293             //associations can be specified in the more convenient format (e.g. not inside an 'associations' array).
294             //we support that here
295             if (belongsTo) {
296                 belongsTo = Ext.Array.from(belongsTo);
297
298                 for (i = 0, ln = belongsTo.length; i < ln; ++i) {
299                     association = belongsTo[i];
300
301                     if (!Ext.isObject(association)) {
302                         association = {model: association};
303                     }
304
305                     association.type = 'belongsTo';
306                     associations.push(association);
307                 }
308
309                 delete data.belongsTo;
310             }
311
312             if (hasMany) {
313                 hasMany = Ext.Array.from(hasMany);
314                 for (i = 0, ln = hasMany.length; i < ln; ++i) {
315                     association = hasMany[i];
316
317                     if (!Ext.isObject(association)) {
318                         association = {model: association};
319                     }
320
321                     association.type = 'hasMany';
322                     associations.push(association);
323                 }
324
325                 delete data.hasMany;
326             }
327
328             if (superAssociations) {
329                 associations = superAssociations.items.concat(associations);
330             }
331
332             for (i = 0, ln = associations.length; i < ln; ++i) {
333                 dependencies.push('association.' + associations[i].type.toLowerCase());
334             }
335
336             if (data.proxy) {
337                 if (typeof data.proxy === 'string') {
338                     dependencies.push('proxy.' + data.proxy);
339                 }
340                 else if (typeof data.proxy.type === 'string') {
341                     dependencies.push('proxy.' + data.proxy.type);
342                 }
343             }
344
345             Ext.require(dependencies, function() {
346                 Ext.ModelManager.registerType(name, cls);
347
348                 for (i = 0, ln = associations.length; i < ln; ++i) {
349                     association = associations[i];
350
351                     Ext.apply(association, {
352                         ownerModel: name,
353                         associatedModel: association.model
354                     });
355
356                     if (Ext.ModelManager.getModel(association.model) === undefined) {
357                         Ext.ModelManager.registerDeferredAssociation(association);
358                     } else {
359                         associationsMixedCollection.add(Ext.data.Association.create(association));
360                     }
361                 }
362
363                 data.associations = associationsMixedCollection;
364
365                 onBeforeClassCreated.call(me, cls, data);
366
367                 cls.setProxy(cls.prototype.proxy || cls.prototype.defaultProxyType);
368
369                 // Fire the onModelDefined template method on ModelManager
370                 Ext.ModelManager.onModelDefined(cls);
371             });
372         };
373     },
374
375     inheritableStatics: {
376         /**
377          * Sets the Proxy to use for this model. Accepts any options that can be accepted by
378          * {@link Ext#createByAlias Ext.createByAlias}.
379          * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
380          * @return {Ext.data.proxy.Proxy}
381          * @static
382          * @inheritable
383          */
384         setProxy: function(proxy) {
385             //make sure we have an Ext.data.proxy.Proxy object
386             if (!proxy.isProxy) {
387                 if (typeof proxy == "string") {
388                     proxy = {
389                         type: proxy
390                     };
391                 }
392                 proxy = Ext.createByAlias("proxy." + proxy.type, proxy);
393             }
394             proxy.setModel(this);
395             this.proxy = this.prototype.proxy = proxy;
396
397             return proxy;
398         },
399
400         /**
401          * Returns the configured Proxy for this Model
402          * @return {Ext.data.proxy.Proxy} The proxy
403          * @static
404          * @inheritable
405          */
406         getProxy: function() {
407             return this.proxy;
408         },
409
410         /**
411          * Asynchronously loads a model instance by id. Sample usage:
412          *
413          *     MyApp.User = Ext.define('User', {
414          *         extend: 'Ext.data.Model',
415          *         fields: [
416          *             {name: 'id', type: 'int'},
417          *             {name: 'name', type: 'string'}
418          *         ]
419          *     });
420          *
421          *     MyApp.User.load(10, {
422          *         scope: this,
423          *         failure: function(record, operation) {
424          *             //do something if the load failed
425          *         },
426          *         success: function(record, operation) {
427          *             //do something if the load succeeded
428          *         },
429          *         callback: function(record, operation) {
430          *             //do something whether the load succeeded or failed
431          *         }
432          *     });
433          *
434          * @param {Number} id The id of the model to load
435          * @param {Object} config (optional) config object containing success, failure and callback functions, plus
436          * optional scope
437          * @static
438          * @inheritable
439          */
440         load: function(id, config) {
441             config = Ext.apply({}, config);
442             config = Ext.applyIf(config, {
443                 action: 'read',
444                 id    : id
445             });
446
447             var operation  = Ext.create('Ext.data.Operation', config),
448                 scope      = config.scope || this,
449                 record     = null,
450                 callback;
451
452             callback = function(operation) {
453                 if (operation.wasSuccessful()) {
454                     record = operation.getRecords()[0];
455                     Ext.callback(config.success, scope, [record, operation]);
456                 } else {
457                     Ext.callback(config.failure, scope, [record, operation]);
458                 }
459                 Ext.callback(config.callback, scope, [record, operation]);
460             };
461
462             this.proxy.read(operation, callback, this);
463         }
464     },
465
466     statics: {
467         PREFIX : 'ext-record',
468         AUTO_ID: 1,
469         EDIT   : 'edit',
470         REJECT : 'reject',
471         COMMIT : 'commit',
472
473         /**
474          * Generates a sequential id. This method is typically called when a record is {@link Ext#create
475          * create}d and {@link #constructor no id has been specified}. The id will automatically be assigned to the
476          * record. The returned id takes the form: {PREFIX}-{AUTO_ID}.
477          *
478          * - **PREFIX** : String - Ext.data.Model.PREFIX (defaults to 'ext-record')
479          * - **AUTO_ID** : String - Ext.data.Model.AUTO_ID (defaults to 1 initially)
480          *
481          * @param {Ext.data.Model} rec The record being created. The record does not exist, it's a {@link #phantom}.
482          * @return {String} auto-generated string id, `"ext-record-i++"`;
483          * @static
484          */
485         id: function(rec) {
486             var id = [this.PREFIX, '-', this.AUTO_ID++].join('');
487             rec.phantom = true;
488             rec.internalId = id;
489             return id;
490         }
491     },
492
493     /**
494      * @cfg {String/Object} idgen
495      * The id generator to use for this model. The default id generator does not generate
496      * values for the {@link #idProperty}.
497      *
498      * This can be overridden at the model level to provide a custom generator for a model.
499      * The simplest form of this would be:
500      *
501      *      Ext.define('MyApp.data.MyModel', {
502      *          extend: 'Ext.data.Model',
503      *          requires: ['Ext.data.SequentialIdGenerator'],
504      *          idgen: 'sequential',
505      *          ...
506      *      });
507      *
508      * The above would generate {@link Ext.data.SequentialIdGenerator sequential} id's such
509      * as 1, 2, 3 etc..
510      *
511      * Another useful id generator is {@link Ext.data.UuidGenerator}:
512      *
513      *      Ext.define('MyApp.data.MyModel', {
514      *          extend: 'Ext.data.Model',
515      *          requires: ['Ext.data.UuidGenerator'],
516      *          idgen: 'uuid',
517      *          ...
518      *      });
519      *
520      * An id generation can also be further configured:
521      *
522      *      Ext.define('MyApp.data.MyModel', {
523      *          extend: 'Ext.data.Model',
524      *          idgen: {
525      *              type: 'sequential',
526      *              seed: 1000,
527      *              prefix: 'ID_'
528      *          }
529      *      });
530      *
531      * The above would generate id's such as ID_1000, ID_1001, ID_1002 etc..
532      *
533      * If multiple models share an id space, a single generator can be shared:
534      *
535      *      Ext.define('MyApp.data.MyModelX', {
536      *          extend: 'Ext.data.Model',
537      *          idgen: {
538      *              type: 'sequential',
539      *              id: 'xy'
540      *          }
541      *      });
542      *
543      *      Ext.define('MyApp.data.MyModelY', {
544      *          extend: 'Ext.data.Model',
545      *          idgen: {
546      *              type: 'sequential',
547      *              id: 'xy'
548      *          }
549      *      });
550      *
551      * For more complex, shared id generators, a custom generator is the best approach.
552      * See {@link Ext.data.IdGenerator} for details on creating custom id generators.
553      *
554      * @markdown
555      */
556     idgen: {
557         isGenerator: true,
558         type: 'default',
559
560         generate: function () {
561             return null;
562         },
563         getRecId: function (rec) {
564             return rec.modelName + '-' + rec.internalId;
565         }
566     },
567
568     /**
569      * @property {Boolean} editing
570      * Internal flag used to track whether or not the model instance is currently being edited. Read-only.
571      */
572     editing : false,
573
574     /**
575      * @property {Boolean} dirty
576      * True if this Record has been modified. Read-only.
577      */
578     dirty : false,
579
580     /**
581      * @cfg {String} persistenceProperty
582      * The property on this Persistable object that its data is saved to. Defaults to 'data'
583      * (e.g. all persistable data resides in this.data.)
584      */
585     persistenceProperty: 'data',
586
587     evented: false,
588     isModel: true,
589
590     /**
591      * @property {Boolean} phantom
592      * True when the record does not yet exist in a server-side database (see {@link #setDirty}).
593      * Any record which has a real database pk set as its id property is NOT a phantom -- it's real.
594      */
595     phantom : false,
596
597     /**
598      * @cfg {String} idProperty
599      * The name of the field treated as this Model's unique id. Defaults to 'id'.
600      */
601     idProperty: 'id',
602
603     /**
604      * @cfg {String} defaultProxyType
605      * The string type of the default Model Proxy. Defaults to 'ajax'.
606      */
607     defaultProxyType: 'ajax',
608
609     // Fields config and property
610     /**
611      * @cfg {Object[]/String[]} fields
612      * The fields for this model.
613      */
614     /**
615      * @property {Ext.util.MixedCollection} fields
616      * The fields defined on this model.
617      */
618
619     /**
620      * @cfg {Object[]} validations
621      * An array of {@link Ext.data.validations validations} for this model.
622      */
623
624     // Associations configs and properties
625     /**
626      * @cfg {Object[]} associations
627      * An array of {@link Ext.data.Association associations} for this model.
628      */
629     /**
630      * @cfg {String/Object/String[]/Object[]} hasMany
631      * One or more {@link Ext.data.HasManyAssociation HasMany associations} for this model.
632      */
633     /**
634      * @cfg {String/Object/String[]/Object[]} belongsTo
635      * One or more {@link Ext.data.BelongsToAssociation BelongsTo associations} for this model.
636      */
637     /**
638      * @property {Ext.util.MixedCollection} associations
639      * {@link Ext.data.Association Associations} defined on this model.
640      */
641
642     /**
643      * @cfg {String/Object/Ext.data.proxy.Proxy} proxy
644      * The {@link Ext.data.proxy.Proxy proxy} to use for this model.
645      */
646
647     // raw not documented intentionally, meant to be used internally.
648     constructor: function(data, id, raw) {
649         data = data || {};
650
651         var me = this,
652             fields,
653             length,
654             field,
655             name,
656             i,
657             newId,
658             isArray = Ext.isArray(data),
659             newData = isArray ? {} : null; // to hold mapped array data if needed
660
661         /**
662          * An internal unique ID for each Model instance, used to identify Models that don't have an ID yet
663          * @property internalId
664          * @type String
665          * @private
666          */
667         me.internalId = (id || id === 0) ? id : Ext.data.Model.id(me);
668
669         /**
670          * @property {Object} raw The raw data used to create this model if created via a reader.
671          */
672         me.raw = raw;
673
674         Ext.applyIf(me, {
675             data: {}
676         });
677
678         /**
679          * @property {Object} modified Key: value pairs of all fields whose values have changed
680          */
681         me.modified = {};
682
683         // Deal with spelling error in previous releases
684         if (me.persistanceProperty) {
685             //<debug>
686             if (Ext.isDefined(Ext.global.console)) {
687                 Ext.global.console.warn('Ext.data.Model: persistanceProperty has been deprecated. Use persistenceProperty instead.');
688             }
689             //</debug>
690             me.persistenceProperty = me.persistanceProperty;
691         }
692         me[me.persistenceProperty] = {};
693
694         me.mixins.observable.constructor.call(me);
695
696         //add default field values if present
697         fields = me.fields.items;
698         length = fields.length;
699
700         for (i = 0; i < length; i++) {
701             field = fields[i];
702             name  = field.name;
703
704             if (isArray){
705                 // Have to map array data so the values get assigned to the named fields
706                 // rather than getting set as the field names with undefined values.
707                 newData[name] = data[i];
708             }
709             else if (data[name] === undefined) {
710                 data[name] = field.defaultValue;
711             }
712         }
713
714         me.set(newData || data);
715
716         if (me.getId()) {
717             me.phantom = false;
718         } else if (me.phantom) {
719             newId = me.idgen.generate();
720             if (newId !== null) {
721                 me.setId(newId);
722             }
723         }
724
725         // clear any dirty/modified since we're initializing
726         me.dirty = false;
727         me.modified = {};
728
729         if (typeof me.init == 'function') {
730             me.init();
731         }
732
733         me.id = me.idgen.getRecId(me);
734     },
735
736     /**
737      * Returns the value of the given field
738      * @param {String} fieldName The field to fetch the value for
739      * @return {Object} The value
740      */
741     get: function(field) {
742         return this[this.persistenceProperty][field];
743     },
744
745     /**
746      * Sets the given field to the given value, marks the instance as dirty
747      * @param {String/Object} fieldName The field to set, or an object containing key/value pairs
748      * @param {Object} value The value to set
749      */
750     set: function(fieldName, value) {
751         var me = this,
752             fields = me.fields,
753             modified = me.modified,
754             convertFields = [],
755             field, key, i, currentValue, notEditing, count, length;
756
757         /*
758          * If we're passed an object, iterate over that object. NOTE: we pull out fields with a convert function and
759          * set those last so that all other possible data is set before the convert function is called
760          */
761         if (arguments.length == 1 && Ext.isObject(fieldName)) {
762             notEditing = !me.editing;
763             count = 0;
764             for (key in fieldName) {
765                 if (fieldName.hasOwnProperty(key)) {
766
767                     //here we check for the custom convert function. Note that if a field doesn't have a convert function,
768                     //we default it to its type's convert function, so we have to check that here. This feels rather dirty.
769                     field = fields.get(key);
770                     if (field && field.convert !== field.type.convert) {
771                         convertFields.push(key);
772                         continue;
773                     }
774
775                     if (!count && notEditing) {
776                         me.beginEdit();
777                     }
778                     ++count;
779                     me.set(key, fieldName[key]);
780                 }
781             }
782
783             length = convertFields.length;
784             if (length) {
785                 if (!count && notEditing) {
786                     me.beginEdit();
787                 }
788                 count += length;
789                 for (i = 0; i < length; i++) {
790                     field = convertFields[i];
791                     me.set(field, fieldName[field]);
792                 }
793             }
794
795             if (notEditing && count) {
796                 me.endEdit();
797             }
798         } else {
799             if (fields) {
800                 field = fields.get(fieldName);
801
802                 if (field && field.convert) {
803                     value = field.convert(value, me);
804                 }
805             }
806             currentValue = me.get(fieldName);
807             me[me.persistenceProperty][fieldName] = value;
808
809             if (field && field.persist && !me.isEqual(currentValue, value)) {
810                 if (me.isModified(fieldName)) {
811                     if (me.isEqual(modified[fieldName], value)) {
812                         // the original value in me.modified equals the new value, so the
813                         // field is no longer modified
814                         delete modified[fieldName];
815                         // we might have removed the last modified field, so check to see if
816                         // there are any modified fields remaining and correct me.dirty:
817                         me.dirty = false;
818                         for (key in modified) {
819                             if (modified.hasOwnProperty(key)){
820                                 me.dirty = true;
821                                 break;
822                             }
823                         }
824                     }
825                 } else {
826                     me.dirty = true;
827                     modified[fieldName] = currentValue;
828                 }
829             }
830
831             if (!me.editing) {
832                 me.afterEdit();
833             }
834         }
835     },
836
837     /**
838      * Checks if two values are equal, taking into account certain
839      * special factors, for example dates.
840      * @private
841      * @param {Object} a The first value
842      * @param {Object} b The second value
843      * @return {Boolean} True if the values are equal
844      */
845     isEqual: function(a, b){
846         if (Ext.isDate(a) && Ext.isDate(b)) {
847             return a.getTime() === b.getTime();
848         }
849         return a === b;
850     },
851
852     /**
853      * Begins an edit. While in edit mode, no events (e.g.. the `update` event) are relayed to the containing store.
854      * When an edit has begun, it must be followed by either {@link #endEdit} or {@link #cancelEdit}.
855      */
856     beginEdit : function(){
857         var me = this;
858         if (!me.editing) {
859             me.editing = true;
860             me.dirtySave = me.dirty;
861             me.dataSave = Ext.apply({}, me[me.persistenceProperty]);
862             me.modifiedSave = Ext.apply({}, me.modified);
863         }
864     },
865
866     /**
867      * Cancels all changes made in the current edit operation.
868      */
869     cancelEdit : function(){
870         var me = this;
871         if (me.editing) {
872             me.editing = false;
873             // reset the modified state, nothing changed since the edit began
874             me.modified = me.modifiedSave;
875             me[me.persistenceProperty] = me.dataSave;
876             me.dirty = me.dirtySave;
877             delete me.modifiedSave;
878             delete me.dataSave;
879             delete me.dirtySave;
880         }
881     },
882
883     /**
884      * Ends an edit. If any data was modified, the containing store is notified (ie, the store's `update` event will
885      * fire).
886      * @param {Boolean} silent True to not notify the store of the change
887      */
888     endEdit : function(silent){
889         var me = this,
890             didChange;
891             
892         if (me.editing) {
893             me.editing = false;
894             didChange = me.dirty || me.changedWhileEditing();
895             delete me.modifiedSave;
896             delete me.dataSave;
897             delete me.dirtySave;
898             if (silent !== true && didChange) {
899                 me.afterEdit();
900             }
901         }
902     },
903     
904     /**
905      * Checks if the underlying data has changed during an edit. This doesn't necessarily
906      * mean the record is dirty, however we still need to notify the store since it may need
907      * to update any views.
908      * @private
909      * @return {Boolean} True if the underlying data has changed during an edit.
910      */
911     changedWhileEditing: function(){
912         var me = this,
913             saved = me.dataSave,
914             data = me[me.persistenceProperty],
915             key;
916             
917         for (key in data) {
918             if (data.hasOwnProperty(key)) {
919                 if (!me.isEqual(data[key], saved[key])) {
920                     return true;
921                 }
922             }
923         }
924         return false; 
925     },
926
927     /**
928      * Gets a hash of only the fields that have been modified since this Model was created or commited.
929      * @return {Object}
930      */
931     getChanges : function(){
932         var modified = this.modified,
933             changes  = {},
934             field;
935
936         for (field in modified) {
937             if (modified.hasOwnProperty(field)){
938                 changes[field] = this.get(field);
939             }
940         }
941
942         return changes;
943     },
944
945     /**
946      * Returns true if the passed field name has been `{@link #modified}` since the load or last commit.
947      * @param {String} fieldName {@link Ext.data.Field#name}
948      * @return {Boolean}
949      */
950     isModified : function(fieldName) {
951         return this.modified.hasOwnProperty(fieldName);
952     },
953
954     /**
955      * Marks this **Record** as `{@link #dirty}`. This method is used interally when adding `{@link #phantom}` records
956      * to a {@link Ext.data.proxy.Server#writer writer enabled store}.
957      *
958      * Marking a record `{@link #dirty}` causes the phantom to be returned by {@link Ext.data.Store#getUpdatedRecords}
959      * where it will have a create action composed for it during {@link Ext.data.Model#save model save} operations.
960      */
961     setDirty : function() {
962         var me = this,
963             name;
964
965         me.dirty = true;
966
967         me.fields.each(function(field) {
968             if (field.persist) {
969                 name = field.name;
970                 me.modified[name] = me.get(name);
971             }
972         }, me);
973     },
974
975     //<debug>
976     markDirty : function() {
977         if (Ext.isDefined(Ext.global.console)) {
978             Ext.global.console.warn('Ext.data.Model: markDirty has been deprecated. Use setDirty instead.');
979         }
980         return this.setDirty.apply(this, arguments);
981     },
982     //</debug>
983
984     /**
985      * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. Rejects
986      * all changes made to the model instance since either creation, or the last commit operation. Modified fields are
987      * reverted to their original values.
988      *
989      * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of reject
990      * operations.
991      *
992      * @param {Boolean} silent (optional) True to skip notification of the owning store of the change.
993      * Defaults to false.
994      */
995     reject : function(silent) {
996         var me = this,
997             modified = me.modified,
998             field;
999
1000         for (field in modified) {
1001             if (modified.hasOwnProperty(field)) {
1002                 if (typeof modified[field] != "function") {
1003                     me[me.persistenceProperty][field] = modified[field];
1004                 }
1005             }
1006         }
1007
1008         me.dirty = false;
1009         me.editing = false;
1010         me.modified = {};
1011
1012         if (silent !== true) {
1013             me.afterReject();
1014         }
1015     },
1016
1017     /**
1018      * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all changes made to the
1019      * instance since either creation or the last commit operation.
1020      *
1021      * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of commit
1022      * operations.
1023      *
1024      * @param {Boolean} silent (optional) True to skip notification of the owning store of the change.
1025      * Defaults to false.
1026      */
1027     commit : function(silent) {
1028         var me = this;
1029
1030         me.phantom = me.dirty = me.editing = false;
1031         me.modified = {};
1032
1033         if (silent !== true) {
1034             me.afterCommit();
1035         }
1036     },
1037
1038     /**
1039      * Creates a copy (clone) of this Model instance.
1040      *
1041      * @param {String} [id] A new id, defaults to the id of the instance being copied.
1042      * See `{@link Ext.data.Model#id id}`. To generate a phantom instance with a new id use:
1043      *
1044      *     var rec = record.copy(); // clone the record
1045      *     Ext.data.Model.id(rec); // automatically generate a unique sequential id
1046      *
1047      * @return {Ext.data.Model}
1048      */
1049     copy : function(newId) {
1050         var me = this;
1051
1052         return new me.self(Ext.apply({}, me[me.persistenceProperty]), newId || me.internalId);
1053     },
1054
1055     /**
1056      * Sets the Proxy to use for this model. Accepts any options that can be accepted by
1057      * {@link Ext#createByAlias Ext.createByAlias}.
1058      *
1059      * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
1060      * @return {Ext.data.proxy.Proxy}
1061      */
1062     setProxy: function(proxy) {
1063         //make sure we have an Ext.data.proxy.Proxy object
1064         if (!proxy.isProxy) {
1065             if (typeof proxy === "string") {
1066                 proxy = {
1067                     type: proxy
1068                 };
1069             }
1070             proxy = Ext.createByAlias("proxy." + proxy.type, proxy);
1071         }
1072         proxy.setModel(this.self);
1073         this.proxy = proxy;
1074
1075         return proxy;
1076     },
1077
1078     /**
1079      * Returns the configured Proxy for this Model.
1080      * @return {Ext.data.proxy.Proxy} The proxy
1081      */
1082     getProxy: function() {
1083         return this.proxy;
1084     },
1085
1086     /**
1087      * Validates the current data against all of its configured {@link #validations}.
1088      * @return {Ext.data.Errors} The errors object
1089      */
1090     validate: function() {
1091         var errors      = Ext.create('Ext.data.Errors'),
1092             validations = this.validations,
1093             validators  = Ext.data.validations,
1094             length, validation, field, valid, type, i;
1095
1096         if (validations) {
1097             length = validations.length;
1098
1099             for (i = 0; i < length; i++) {
1100                 validation = validations[i];
1101                 field = validation.field || validation.name;
1102                 type  = validation.type;
1103                 valid = validators[type](validation, this.get(field));
1104
1105                 if (!valid) {
1106                     errors.add({
1107                         field  : field,
1108                         message: validation.message || validators[type + 'Message']
1109                     });
1110                 }
1111             }
1112         }
1113
1114         return errors;
1115     },
1116
1117     /**
1118      * Checks if the model is valid. See {@link #validate}.
1119      * @return {Boolean} True if the model is valid.
1120      */
1121     isValid: function(){
1122         return this.validate().isValid();
1123     },
1124
1125     /**
1126      * Saves the model instance using the configured proxy.
1127      * @param {Object} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
1128      * @return {Ext.data.Model} The Model instance
1129      */
1130     save: function(options) {
1131         options = Ext.apply({}, options);
1132
1133         var me     = this,
1134             action = me.phantom ? 'create' : 'update',
1135             record = null,
1136             scope  = options.scope || me,
1137             operation,
1138             callback;
1139
1140         Ext.apply(options, {
1141             records: [me],
1142             action : action
1143         });
1144
1145         operation = Ext.create('Ext.data.Operation', options);
1146
1147         callback = function(operation) {
1148             if (operation.wasSuccessful()) {
1149                 record = operation.getRecords()[0];
1150                 //we need to make sure we've set the updated data here. Ideally this will be redundant once the
1151                 //ModelCache is in place
1152                 me.set(record.data);
1153                 record.dirty = false;
1154
1155                 Ext.callback(options.success, scope, [record, operation]);
1156             } else {
1157                 Ext.callback(options.failure, scope, [record, operation]);
1158             }
1159
1160             Ext.callback(options.callback, scope, [record, operation]);
1161         };
1162
1163         me.getProxy()[action](operation, callback, me);
1164
1165         return me;
1166     },
1167
1168     /**
1169      * Destroys the model using the configured proxy.
1170      * @param {Object} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
1171      * @return {Ext.data.Model} The Model instance
1172      */
1173     destroy: function(options){
1174         options = Ext.apply({}, options);
1175
1176         var me     = this,
1177             record = null,
1178             scope  = options.scope || me,
1179             operation,
1180             callback;
1181
1182         Ext.apply(options, {
1183             records: [me],
1184             action : 'destroy'
1185         });
1186
1187         operation = Ext.create('Ext.data.Operation', options);
1188         callback = function(operation) {
1189             if (operation.wasSuccessful()) {
1190                 Ext.callback(options.success, scope, [record, operation]);
1191             } else {
1192                 Ext.callback(options.failure, scope, [record, operation]);
1193             }
1194             Ext.callback(options.callback, scope, [record, operation]);
1195         };
1196
1197         me.getProxy().destroy(operation, callback, me);
1198         return me;
1199     },
1200
1201     /**
1202      * Returns the unique ID allocated to this model instance as defined by {@link #idProperty}.
1203      * @return {Number} The id
1204      */
1205     getId: function() {
1206         return this.get(this.idProperty);
1207     },
1208
1209     /**
1210      * Sets the model instance's id field to the given id.
1211      * @param {Number} id The new id
1212      */
1213     setId: function(id) {
1214         this.set(this.idProperty, id);
1215     },
1216
1217     /**
1218      * Tells this model instance that it has been added to a store.
1219      * @param {Ext.data.Store} store The store to which this model has been added.
1220      */
1221     join : function(store) {
1222         /**
1223          * @property {Ext.data.Store} store
1224          * The {@link Ext.data.Store Store} to which this Record belongs.
1225          */
1226         this.store = store;
1227     },
1228
1229     /**
1230      * Tells this model instance that it has been removed from the store.
1231      * @param {Ext.data.Store} store The store from which this model has been removed.
1232      */
1233     unjoin: function(store) {
1234         delete this.store;
1235     },
1236
1237     /**
1238      * @private
1239      * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
1240      * afterEdit method is called
1241      */
1242     afterEdit : function() {
1243         this.callStore('afterEdit');
1244     },
1245
1246     /**
1247      * @private
1248      * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
1249      * afterReject method is called
1250      */
1251     afterReject : function() {
1252         this.callStore("afterReject");
1253     },
1254
1255     /**
1256      * @private
1257      * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
1258      * afterCommit method is called
1259      */
1260     afterCommit: function() {
1261         this.callStore('afterCommit');
1262     },
1263
1264     /**
1265      * @private
1266      * Helper function used by afterEdit, afterReject and afterCommit. Calls the given method on the
1267      * {@link Ext.data.Store store} that this instance has {@link #join joined}, if any. The store function
1268      * will always be called with the model instance as its single argument.
1269      * @param {String} fn The function to call on the store
1270      */
1271     callStore: function(fn) {
1272         var store = this.store;
1273
1274         if (store !== undefined && typeof store[fn] == "function") {
1275             store[fn](this);
1276         }
1277     },
1278
1279     /**
1280      * Gets all of the data from this Models *loaded* associations. It does this recursively - for example if we have a
1281      * User which hasMany Orders, and each Order hasMany OrderItems, it will return an object like this:
1282      *
1283      *     {
1284      *         orders: [
1285      *             {
1286      *                 id: 123,
1287      *                 status: 'shipped',
1288      *                 orderItems: [
1289      *                     ...
1290      *                 ]
1291      *             }
1292      *         ]
1293      *     }
1294      *
1295      * @return {Object} The nested data set for the Model's loaded associations
1296      */
1297     getAssociatedData: function(){
1298         return this.prepareAssociatedData(this, [], null);
1299     },
1300
1301     /**
1302      * @private
1303      * This complex-looking method takes a given Model instance and returns an object containing all data from
1304      * all of that Model's *loaded* associations. See (@link #getAssociatedData}
1305      * @param {Ext.data.Model} record The Model instance
1306      * @param {String[]} ids PRIVATE. The set of Model instance internalIds that have already been loaded
1307      * @param {String} associationType (optional) The name of the type of association to limit to.
1308      * @return {Object} The nested data set for the Model's loaded associations
1309      */
1310     prepareAssociatedData: function(record, ids, associationType) {
1311         //we keep track of all of the internalIds of the models that we have loaded so far in here
1312         var associations     = record.associations.items,
1313             associationCount = associations.length,
1314             associationData  = {},
1315             associatedStore, associatedName, associatedRecords, associatedRecord,
1316             associatedRecordCount, association, id, i, j, type, allow;
1317
1318         for (i = 0; i < associationCount; i++) {
1319             association = associations[i];
1320             type = association.type;
1321             allow = true;
1322             if (associationType) {
1323                 allow = type == associationType;
1324             }
1325             if (allow && type == 'hasMany') {
1326
1327                 //this is the hasMany store filled with the associated data
1328                 associatedStore = record[association.storeName];
1329
1330                 //we will use this to contain each associated record's data
1331                 associationData[association.name] = [];
1332
1333                 //if it's loaded, put it into the association data
1334                 if (associatedStore && associatedStore.data.length > 0) {
1335                     associatedRecords = associatedStore.data.items;
1336                     associatedRecordCount = associatedRecords.length;
1337
1338                     //now we're finally iterating over the records in the association. We do this recursively
1339                     for (j = 0; j < associatedRecordCount; j++) {
1340                         associatedRecord = associatedRecords[j];
1341                         // Use the id, since it is prefixed with the model name, guaranteed to be unique
1342                         id = associatedRecord.id;
1343
1344                         //when we load the associations for a specific model instance we add it to the set of loaded ids so that
1345                         //we don't load it twice. If we don't do this, we can fall into endless recursive loading failures.
1346                         if (Ext.Array.indexOf(ids, id) == -1) {
1347                             ids.push(id);
1348
1349                             associationData[association.name][j] = associatedRecord.data;
1350                             Ext.apply(associationData[association.name][j], this.prepareAssociatedData(associatedRecord, ids, type));
1351                         }
1352                     }
1353                 }
1354             } else if (allow && type == 'belongsTo') {
1355                 associatedRecord = record[association.instanceName];
1356                 if (associatedRecord !== undefined) {
1357                     id = associatedRecord.id;
1358                     if (Ext.Array.indexOf(ids, id) == -1) {
1359                         ids.push(id);
1360                         associationData[association.name] = associatedRecord.data;
1361                         Ext.apply(associationData[association.name], this.prepareAssociatedData(associatedRecord, ids, type));
1362                     }
1363                 }
1364             }
1365         }
1366
1367         return associationData;
1368     }
1369 });
1370