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