]> git.proxmox.com Git - extjs.git/blob - extjs/packages/core/src/data/Model.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / data / Model.js
1 /**
2 * A Model or Entity represents some object that your application manages. For example, one
3 * might define a Model for Users, Products, Cars, or other real-world object that we want
4 * to model in the system. Models are used by {@link Ext.data.Store stores}, which are in
5 * turn used by many of the data-bound components in Ext.
6 *
7 * # Fields
8 *
9 * Models are defined as a set of fields and any arbitrary methods and properties relevant
10 * to the model. For example:
11 *
12 * Ext.define('User', {
13 * extend: 'Ext.data.Model',
14 * fields: [
15 * {name: 'name', type: 'string'},
16 * {name: 'age', type: 'int', convert: null},
17 * {name: 'phone', type: 'string'},
18 * {name: 'alive', type: 'boolean', defaultValue: true, convert: null}
19 * ],
20 *
21 * changeName: function() {
22 * var oldName = this.get('name'),
23 * newName = oldName + " The Barbarian";
24 *
25 * this.set('name', newName);
26 * }
27 * });
28 *
29 * Now we can create instances of our User model and call any model logic we defined:
30 *
31 * var user = Ext.create('User', {
32 * id : 'ABCD12345',
33 * name : 'Conan',
34 * age : 24,
35 * phone: '555-555-5555'
36 * });
37 *
38 * user.changeName();
39 * user.get('name'); //returns "Conan The Barbarian"
40 *
41 * By default, the built in field types such as number and boolean coerce string values
42 * in the raw data by virtue of their {@link Ext.data.field.Field#method-convert} method.
43 * When the server can be relied upon to send data in a format that does not need to be
44 * converted, disabling this can improve performance. The {@link Ext.data.reader.Json Json}
45 * and {@link Ext.data.reader.Array Array} readers are likely candidates for this
46 * optimization. To disable field conversions you simply specify `null` for the field's
47 * {@link Ext.data.field.Field#cfg-convert convert config}.
48 *
49 * ## The "id" Field and `idProperty`
50 *
51 * A Model definition always has an *identifying field* which should yield a unique key
52 * for each instance. By default, a field named "id" will be created with a
53 * {@link Ext.data.Field#mapping mapping} of "id". This happens because of the default
54 * {@link #idProperty} provided in Model definitions.
55 *
56 * To alter which field is the identifying field, use the {@link #idProperty} config.
57 *
58 * # Validators
59 *
60 * Models have built-in support for field validators. Validators are added to models as in
61 * the follow example:
62 *
63 * Ext.define('User', {
64 * extend: 'Ext.data.Model',
65 * fields: [
66 * { name: 'name', type: 'string' },
67 * { name: 'age', type: 'int' },
68 * { name: 'phone', type: 'string' },
69 * { name: 'gender', type: 'string' },
70 * { name: 'username', type: 'string' },
71 * { name: 'alive', type: 'boolean', defaultValue: true }
72 * ],
73 *
74 * validators: {
75 * age: 'presence',
76 * name: { type: 'length', min: 2 },
77 * gender: { type: 'inclusion', list: ['Male', 'Female'] },
78 * username: [
79 * { type: 'exclusion', list: ['Admin', 'Operator'] },
80 * { type: 'format', matcher: /([a-z]+)[0-9]{2,3}/i }
81 * ]
82 * }
83 * });
84 *
85 * The derived type of `Ext.data.field.Field` can also provide validation. If `validators`
86 * need to be duplicated on multiple fields, instead consider creating a custom field type.
87 *
88 * ## Validation
89 *
90 * The results of the validators can be retrieved via the "associated" validation record:
91 *
92 * var instance = Ext.create('User', {
93 * name: 'Ed',
94 * gender: 'Male',
95 * username: 'edspencer'
96 * });
97 *
98 * var validation = instance.getValidation();
99 *
100 * The returned object is an instance of `Ext.data.Validation` and has as its fields the
101 * result of the field `validators`. The validation object is "dirty" if there are one or
102 * more validation errors present.
103 *
104 * This record is also available when using data binding as a "pseudo-association" called
105 * "validation". This pseudo-association can be hidden by an explicitly declared
106 * association by the same name (for compatibility reasons), but doing so is not
107 * recommended.
108 *
109 * The `{@link Ext.Component#modelValidation}` config can be used to enable automatic
110 * binding from the "validation" of a record to the form fields that may be bound to its
111 * values.
112 *
113 * # Associations
114 *
115 * Models often have associations with other Models. These associations can be defined by
116 * fields (often called "foreign keys") or by other data such as a many-to-many (or "matrix").
117 *
118 * ## Foreign-Key Associations - One-to-Many
119 *
120 * The simplest way to define an association from one Model to another is to add a
121 * {@link Ext.data.field.Field#cfg-reference reference config} to the appropriate field.
122 *
123 * Ext.define('Post', {
124 * extend: 'Ext.data.Model',
125 *
126 * fields: [
127 * { name: 'user_id', reference: 'User' }
128 * ]
129 * });
130 *
131 * Ext.define('Comment', {
132 * extend: 'Ext.data.Model',
133 *
134 * fields: [
135 * { name: 'user_id', reference: 'User' },
136 * { name: 'post_id', reference: 'Post' }
137 * ]
138 * });
139 *
140 * Ext.define('User', {
141 * extend: 'Ext.data.Model',
142 *
143 * fields: [
144 * 'name'
145 * ]
146 * });
147 *
148 * The placement of `reference` on the appropriate fields tells the Model which field has
149 * the foreign-key and the type of Model it identifies. That is, the value of these fields
150 * is set to value of the `idProperty` field of the target Model.
151 *
152 * ### One-to-Many Without Foreign-Keys
153 *
154 * To define an association without a foreign-key field, you will need to use either the
155 * `{@link #cfg-hasMany}` or `{@link #cfg-belongsTo}`.
156 *
157 * Ext.define('Post', {
158 * extend: 'Ext.data.Model',
159 *
160 * belongsTo: 'User'
161 * });
162 *
163 * Ext.define('Comment', {
164 * extend: 'Ext.data.Model',
165 *
166 * belongsTo: [ 'Post', 'User' ]
167 * });
168 *
169 * // User is as above
170 *
171 * These declarations have changed slightly from previous releases. In previous releases
172 * both "sides" of an association had to declare their particular roles. This is now only
173 * required if the defaults assumed for names are not satisfactory.
174 *
175 * ## Foreign-Key Associations - One-to-One
176 *
177 * A special case of one-to-many associations is the one-to-one case. This is defined as
178 * a `{@link Ext.data.field.Field#reference unique reference}`.
179 *
180 * Ext.define('Address', {
181 * extend: 'Ext.data.Model',
182 *
183 * fields: [
184 * 'address',
185 * 'city',
186 * 'state'
187 * ]
188 * });
189 *
190 * Ext.define('User', {
191 * extend: 'Ext.data.Model',
192 *
193 * fields: [{
194 * name: 'addressId',
195 * reference: 'Address',
196 * unique: true
197 * }]
198 * });
199 *
200 * ## Many-to-Many
201 *
202 * The classic use case for many-to-many is a User and Group. Users can belong to many
203 * Groups and Groups can contain many Users. This association is declared using the
204 * `{@link #cfg-manyToMany}` config like so:
205 *
206 *
207 * Ext.define('User', {
208 * extend: 'Ext.data.Model',
209 *
210 * fields: [
211 * 'name'
212 * ],
213 *
214 * manyToMany: 'Group'
215 * });
216 *
217 * Ext.define('Group', {
218 * extend: 'Ext.data.Model',
219 *
220 * fields: [
221 * 'name'
222 * ],
223 *
224 * manyToMany: 'User'
225 * });
226 *
227 * As with other associations, only one "side" needs to be declared.
228 *
229 * To manage the relationship between a `manyToMany` relationship, a {@link Ext.data.Session}
230 * must be used.
231 *
232 * # Using a Proxy
233 *
234 * Models are great for representing types of data and relationships, but sooner or later we're going to want to load or
235 * save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, which
236 * can be set directly on the Model:
237 *
238 * Ext.define('User', {
239 * extend: 'Ext.data.Model',
240 * fields: ['id', 'name', 'email'],
241 *
242 * proxy: {
243 * type: 'rest',
244 * url : '/users'
245 * }
246 * });
247 *
248 * 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
249 * RESTful backend. Let's see how this works:
250 *
251 * var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
252 *
253 * user.save(); //POST /users
254 *
255 * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this Model's
256 * data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't have an id,
257 * and performs the appropriate action - in this case issuing a POST request to the url we configured (/users). We
258 * configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
259 *
260 * Loading data via the Proxy is accomplished with the static `load` method:
261 *
262 * //Uses the configured RestProxy to make a GET request to /users/123
263 * User.load(123, {
264 * success: function(user) {
265 * console.log(user.getId()); //logs 123
266 * }
267 * });
268 *
269 * Models can also be updated and destroyed easily:
270 *
271 * //the user Model we loaded in the last snippet:
272 * user.set('name', 'Edward Spencer');
273 *
274 * //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
275 * user.save({
276 * success: function() {
277 * console.log('The User was updated');
278 * }
279 * });
280 *
281 * //tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
282 * user.erase({
283 * success: function() {
284 * console.log('The User was destroyed!');
285 * }
286 * });
287 *
288 * # HTTP Parameter names when using a {@link Ext.data.proxy.Ajax Ajax proxy}
289 *
290 * By default, the model ID is specified in an HTTP parameter named `id`. To change the
291 * name of this parameter use the Proxy's {@link Ext.data.proxy.Ajax#idParam idParam}
292 * configuration.
293 *
294 * Parameters for other commonly passed values such as
295 * {@link Ext.data.proxy.Ajax#pageParam page number} or
296 * {@link Ext.data.proxy.Ajax#startParam start row} may also be configured.
297 *
298 * # Usage in Stores
299 *
300 * 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
301 * creating a {@link Ext.data.Store Store}:
302 *
303 * var store = Ext.create('Ext.data.Store', {
304 * model: 'User'
305 * });
306 *
307 * //uses the Proxy we set up on Model to load the Store data
308 * store.load();
309 *
310 * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain a
311 * set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the {@link
312 * Ext.data.Store Store docs} for more information on Stores.
313 */
314 Ext.define('Ext.data.Model', {
315 alternateClassName: 'Ext.data.Record',
316
317 requires: [
318 'Ext.data.ErrorCollection',
319 'Ext.data.operation.*',
320 'Ext.data.field.*',
321 'Ext.data.validator.Validator',
322 'Ext.data.schema.Schema',
323 'Ext.data.identifier.Generator',
324 'Ext.data.identifier.Sequential'
325 ],
326 uses: [
327 'Ext.data.Validation'
328 ],
329
330 /**
331 * @property {Boolean} isEntity
332 * The value `true` to identify this class and its subclasses.
333 * @readonly
334 */
335 isEntity: true,
336
337 /**
338 * @property {Boolean} isModel
339 * The value `true` to identify this class and its subclasses.
340 * @readonly
341 */
342 isModel: true,
343
344 // Record ids are more flexible.
345 validIdRe: null,
346
347 erasing: false,
348
349 observableType: 'record',
350
351 constructor: function (data, session) {
352 var me = this,
353 cls = me.self,
354 identifier = cls.identifier,
355 Model = Ext.data.Model,
356 modelIdentifier = Model.identifier,
357 idProperty = me.idField.name,
358 array, id, initializeFn, internalId, len, i, fields;
359
360 // Yes, this is here on purpose. See EXTJS-16494. The second
361 // assignment seems to work around a strange JIT issue that prevents
362 // this.data being assigned in random scenarios, even though the data
363 // is passed into the constructor. The issue occurs on 4th gen iPads and
364 // lower, possibly other older iOS devices.
365 me.data = me.data = data || (data = {});
366 me.session = session || null;
367 me.internalId = internalId = modelIdentifier.generate();
368
369 //<debug>
370 var dataId = data[idProperty];
371 if (session && !session.isSession) {
372 Ext.raise('Bad Model constructor argument 2 - "session" is not a Session');
373 }
374 //</debug>
375
376 if ((array = data) instanceof Array) {
377 me.data = data = {};
378 fields = me.getFields();
379 len = Math.min(fields.length, array.length);
380 for (i = 0; i < len; ++i) {
381 data[fields[i].name] = array[i];
382 }
383 }
384
385 if (!(initializeFn = cls.initializeFn)) {
386 cls.initializeFn = initializeFn = Model.makeInitializeFn(cls);
387 }
388 if (!initializeFn.$nullFn) {
389 cls.initializeFn(me);
390 }
391
392 // Must do this after running the initializeFn due to converters on idField
393 if (!(me.id = id = data[idProperty]) && id !== 0) {
394 //<debug>
395 if (dataId) {
396 Ext.raise('The model ID configured in data ("' + dataId + '") has been rejected by the ' + me.fieldsMap[idProperty].type + ' field converter for the ' + idProperty + ' field');
397 }
398 //</debug>
399 if (session) {
400 identifier = session.getIdentifier(cls);
401 id = identifier.generate();
402 } else if (modelIdentifier === identifier) {
403 id = internalId;
404 } else {
405 id = identifier.generate();
406 }
407
408 data[idProperty] = me.id = id;
409 me.phantom = true;
410 }
411
412 if (session) {
413 session.add(me);
414 }
415
416 if (me.init && Ext.isFunction(me.init)) {
417 me.init();
418 }
419 },
420
421 /**
422 * @property {String} entityName
423 * The short name of this entity class. This name is derived from the `namespace` of
424 * the associated `schema` and this class name. By default, a class is not given a
425 * shortened name.
426 *
427 * All entities in a given `schema` must have a unique `entityName`.
428 *
429 * For more details see "Relative Naming" in {@link Ext.data.schema.Schema}.
430 */
431
432 /**
433 * @property {Boolean} editing
434 * Internal flag used to track whether or not the model instance is currently being edited.
435 * @readonly
436 */
437 editing: false,
438
439 /**
440 * @property {Boolean} dirty
441 * True if this record has been modified.
442 * @readonly
443 */
444 dirty: false,
445
446 /**
447 * @property {Ext.data.Session} session
448 * The {@link Ext.data.Session} for this record.
449 * @readonly
450 */
451 session: null,
452
453 /**
454 * @property {Boolean} dropped
455 * True if this record is pending delete on the server. This is set by the `drop`
456 * method and transmitted to the server by the `save` method.
457 * @readonly
458 */
459 dropped: false,
460
461 /**
462 * @property {Boolean} erased
463 * True if this record has been erased on the server. This flag is set of the `erase`
464 * method.
465 * @readonly
466 */
467 erased: false,
468
469 /**
470 * @cfg {String} [clientIdProperty]
471 * The name of the property a server will use to send back a client-generated id in a
472 * `create` or `update` `{@link Ext.data.operation.Operation operation}`.
473 *
474 * If specified, this property cannot have the same name as any other field.
475 *
476 * For example:
477 *
478 * Ext.define('Person', {
479 * idProperty: 'id', // this is the default value (for clarity)
480 *
481 * clientIdProperty: 'clientId',
482 *
483 * identifier: 'negative', // to generate -1, -2 etc on the client
484 *
485 * fields: [ 'name' ]
486 * });
487 *
488 * var person = new Person({
489 * // no id provided, so -1 is generated
490 * name: 'Clark Kent'
491 * });
492 *
493 * The server is given this data during the `create`:
494 *
495 * {
496 * id: -1,
497 * name: 'Clark Kent'
498 * }
499 *
500 * The server allocates a real id and responds like so:
501 *
502 * {
503 * id: 427,
504 * clientId: -1
505 * }
506 *
507 * This property is most useful when creating multiple entities in a single call to
508 * the server in a `{@link Ext.data.operation.Create create operation}`. Alternatively,
509 * the server could respond with records that correspond one-to-one to those sent in
510 * the `operation`.
511 *
512 * For example the client could send a `create` with this data:
513 *
514 * [ { id: -1, name: 'Clark Kent' },
515 * { id: -2, name: 'Peter Parker' },
516 * { id: -3, name: 'Bruce Banner' } ]
517 *
518 * And the server could respond in the same order:
519 *
520 * [ { id: 427 }, // updates id = -1
521 * { id: 428 }, // updates id = -2
522 * { id: 429 } ] // updates id = -3
523 *
524 * Or using `clientIdProperty` the server could respond in arbitrary order:
525 *
526 * [ { id: 427, clientId: -3 },
527 * { id: 428, clientId: -1 },
528 * { id: 429, clientId: -2 } ]
529 *
530 * **IMPORTANT:** When upgrading from previous versions be aware that this property
531 * used to perform the role of `{@link Ext.data.writer.Writer#clientIdProperty}` as
532 * well as that described above. To continue send a client-generated id as other than
533 * the `idProperty`, set `clientIdProperty` on the `writer`. A better solution, however,
534 * is most likely a properly configured `identifier` as that would work better with
535 * associations.
536 */
537 clientIdProperty: null,
538
539 evented: false,
540
541 /**
542 * @property {Boolean} phantom
543 * True when the record does not yet exist in a server-side database. Any record which
544 * has a real database identity set as its `idProperty` is NOT a phantom -- it's real.
545 */
546 phantom: false,
547
548 /**
549 * @cfg {String} [idProperty='id']
550 * The name of the field treated as this Model's unique id.
551 *
552 * If changing the idProperty in a subclass, the generated id field will replace the one
553 * generated by the superclass, for example;
554 *
555 * Ext.define('Super', {
556 * extend: 'Ext.data.Model',
557 * fields: ['name']
558 * });
559 *
560 * Ext.define('Sub', {
561 * extend: 'Super',
562 * idProperty: 'customId'
563 * });
564 *
565 * var fields = Super.getFields();
566 * // Has 2 fields, "name" & "id"
567 * console.log(fields[0].name, fields[1].name, fields.length);
568 *
569 * fields = Sub.getFields();
570 * // Has 2 fields, "name" & "customId", "id" is replaced
571 * console.log(fields[0].name, fields[1].name, fields.length);
572 *
573 * The data values for this field must be unique or there will be id value collisions
574 * in the {@link Ext.data.Store Store}.
575 */
576 idProperty: 'id',
577
578 /**
579 * @cfg {Object} manyToMany
580 * A config object for a {@link Ext.data.schema.ManyToMany ManyToMany} association.
581 * See the class description for {@link Ext.data.schema.ManyToMany ManyToMany} for
582 * configuration examples.
583 */
584 manyToMany: null,
585
586 /**
587 * @cfg {String/Object} identifier
588 * The id generator to use for this model. The `identifier` generates values for the
589 * {@link #idProperty} when no value is given. Records with client-side generated
590 * values for {@link #idProperty} are called {@link #phantom} records since they are
591 * not yet known to the server.
592 *
593 * This can be overridden at the model level to provide a custom generator for a model.
594 * The simplest form of this would be:
595 *
596 * Ext.define('MyApp.data.MyModel', {
597 * extend: 'Ext.data.Model',
598 * requires: ['Ext.data.identifier.Sequential'],
599 * identifier: 'sequential',
600 * ...
601 * });
602 *
603 * The above would generate {@link Ext.data.identifier.Sequential sequential} id's such
604 * as 1, 2, 3 etc..
605 *
606 * Another useful id generator is {@link Ext.data.identifier.Uuid}:
607 *
608 * Ext.define('MyApp.data.MyModel', {
609 * extend: 'Ext.data.Model',
610 * requires: ['Ext.data.identifier.Uuid'],
611 * identifier: 'uuid',
612 * ...
613 * });
614 *
615 * An id generator can also be further configured:
616 *
617 * Ext.define('MyApp.data.MyModel', {
618 * extend: 'Ext.data.Model',
619 * identifier: {
620 * type: 'sequential',
621 * seed: 1000,
622 * prefix: 'ID_'
623 * }
624 * });
625 *
626 * The above would generate id's such as ID_1000, ID_1001, ID_1002 etc..
627 *
628 * If multiple models share an id space, a single generator can be shared:
629 *
630 * Ext.define('MyApp.data.MyModelX', {
631 * extend: 'Ext.data.Model',
632 * identifier: {
633 * type: 'sequential',
634 * id: 'xy'
635 * }
636 * });
637 *
638 * Ext.define('MyApp.data.MyModelY', {
639 * extend: 'Ext.data.Model',
640 * identifier: {
641 * type: 'sequential',
642 * id: 'xy'
643 * }
644 * });
645 *
646 * For more complex, shared id generators, a custom generator is the best approach.
647 * See {@link Ext.data.identifier.Generator} for details on creating custom id generators.
648 */
649 identifier: null,
650
651 // Fields config and property
652 // @cmd-auto-dependency {aliasPrefix: "data.field."}
653 /**
654 * @cfg {Object[]/String[]} fields
655 * An Array of `Ext.data.field.Field` config objects, simply the field
656 * {@link Ext.data.field.Field#name name}, or a mix of config objects and strings.
657 * If just a name is given, the field type defaults to `auto`.
658 *
659 * In a {@link Ext.data.field.Field Field} config object you may pass the alias of
660 * the `Ext.data.field.*` type using the `type` config option.
661 *
662 * // two fields are set:
663 * // - an 'auto' field with a name of 'firstName'
664 * // - and an Ext.data.field.Integer field with a name of 'age'
665 * fields: ['firstName', {
666 * type: 'int',
667 * name: 'age'
668 * }]
669 *
670 * Fields will automatically be created at read time for any for any keys in the
671 * data passed to the Model's {@link #proxy proxy's}
672 * {@link Ext.data.reader.Reader reader} whose name is not explicitly configured in
673 * the `fields` config.
674 *
675 * Extending a Model class will inherit all the `fields` from the superclass /
676 * ancestor classes.
677 */
678 /**
679 * @property {Ext.data.field.Field[]} fields
680 * An array fields defined for this Model (including fields defined in superclasses)
681 * in ordinal order; that is in declaration order.
682 * @private
683 * @readonly
684 */
685
686 /**
687 * @property {Object} fieldOrdinals
688 * This property is indexed by field name and contains the ordinal of that field. The
689 * ordinal often has meaning to servers and is derived based on the position in the
690 * `fields` array.
691 *
692 * This can be used like so:
693 *
694 * Ext.define('MyApp.models.User', {
695 * extend: 'Ext.data.Model',
696 *
697 * fields: [
698 * { name: 'name' }
699 * ]
700 * });
701 *
702 * var nameOrdinal = MyApp.models.User.fieldOrdinals.name;
703 *
704 * // or, if you have an instance:
705 *
706 * var user = new MyApp.models.User();
707 * var nameOrdinal = user.fieldOrdinals.name;
708 *
709 * @private
710 * @readonly
711 */
712
713 /**
714 * @property {Object} modified
715 * A hash of field values which holds the initial values of fields before a set of edits
716 * are {@link #commit committed}.
717 */
718
719 /**
720 * @property {Object} previousValues
721 * This object is similar to the `modified` object except it holds the data values as
722 * they were prior to the most recent change.
723 * @readonly
724 * @private
725 */
726 previousValues: undefined, // Not "null" so getPrevious returns undefined first time
727
728 // @cmd-auto-dependency { aliasPrefix : "proxy.", defaultPropertyName : "defaultProxyType"}
729 /**
730 * @cfg {String/Object/Ext.data.proxy.Proxy} proxy
731 * The {@link Ext.data.proxy.Proxy proxy} to use for this class.
732 */
733 proxy: undefined,
734
735 /**
736 * @cfg {String/Object} [schema='default']
737 * The name of the {@link Ext.data.schema.Schema schema} to which this entity and its
738 * associations belong. For details on custom schemas see `Ext.data.schema.Schema`.
739 */
740 /**
741 * @property {Ext.data.schema.Schema} schema
742 * The `Ext.data.schema.Schema` to which this entity and its associations belong.
743 * @readonly
744 */
745 schema: 'default',
746
747 /**
748 * @cfg {String} [versionProperty]
749 * If specified, this is the name of the property that contains the entity "version".
750 * The version property is used to manage a long-running transaction and allows the
751 * detection of simultaneous modification.
752 *
753 * The way a version property is used is that the client receives the version as it
754 * would any other entity property. When saving an entity, this property is always
755 * included in the request and the server uses the value in a "conditional update".
756 * If the current version of the entity on the server matches the version property
757 * sent by the client, the update is allowed. Otherwise, the update fails.
758 *
759 * On successful update, both the client and server increment the version. This is
760 * done on the server in the conditional update and on the client when it receives a
761 * success on its update request.
762 */
763 versionProperty: null,
764
765 /**
766 * @property {Number} generation
767 * This property is incremented on each modification of a record.
768 * @readonly
769 * @since 5.0.0
770 */
771 generation: 1,
772
773 /**
774 * @cfg {Object[]} validators
775 * An array of {@link Ext.data.validator.Validator validators} for this model.
776 */
777
778 /**
779 * @cfg {String} [validationSeparator=null]
780 * If specified this property is used to concatenate multiple errors for each field
781 * as reported by the `validators`.
782 */
783 validationSeparator: null,
784
785 /**
786 * @cfg {Boolean} [convertOnSet=true]
787 * Set to `false` to prevent any converters from being called on fields specified in
788 * a {@link Ext.data.Model#set set} operation.
789 *
790 * **Note:** Setting the config to `false` will only prevent the convert / calculate
791 * call when the set `fieldName` param matches the field's `{@link #name}`. In the
792 * following example the calls to set `salary` will not execute the convert method
793 * on `set` while the calls to set `vested` will execute the convert method on the
794 * initial read as well as on `set`.
795 *
796 * Example model definition:
797 *
798 * Ext.define('MyApp.model.Employee', {
799 * extend: 'Ext.data.Model',
800 * fields: ['yearsOfService', {
801 * name: 'salary',
802 * convert: function (val) {
803 * var startingBonus = val * .1;
804 * return val + startingBonus;
805 * }
806 * }, {
807 * name: 'vested',
808 * convert: function (val, record) {
809 * return record.get('yearsOfService') >= 4;
810 * },
811 * depends: 'yearsOfService'
812 * }],
813 * convertOnSet: false
814 * });
815 *
816 * var tina = Ext.create('MyApp.model.Employee', {
817 * salary: 50000,
818 * yearsOfService: 3
819 * });
820 *
821 * console.log(tina.get('salary')); // logs 55000
822 * console.log(tina.get('vested')); // logs false
823 *
824 * tina.set({
825 * salary: 60000,
826 * yearsOfService: 4
827 * });
828 * console.log(tina.get('salary')); // logs 60000
829 * console.log(tina.get('vested')); // logs true
830 */
831 convertOnSet: true,
832
833 // Associations configs and properties
834 /**
835 * @cfg {Object[]} associations
836 * An array of {@link Ext.data.schema.Association associations} for this model.
837 */
838 /**
839 * @cfg {String/Object/String[]/Object[]} hasMany
840 * One or more {@link #hasMany HasMany associations} for this model.
841 */
842 /**
843 * @cfg {String/Object/String[]/Object[]} belongsTo
844 * One or more {@link #belongsTo BelongsTo associations} for this model.
845 */
846
847 /**
848 * Begins an edit. While in edit mode, no events (e.g.. the `update` event) are
849 * relayed to the containing store. When an edit has begun, it must be followed by
850 * either `endEdit` or `cancelEdit`.
851 */
852 beginEdit: function () {
853 var me = this,
854 modified = me.modified,
855 previousValues = me.previousValues;
856
857 if (!me.editing) {
858 me.editing = true;
859
860 me.editMemento = {
861 dirty: me.dirty,
862 data: Ext.apply({}, me.data),
863 generation: me.generation,
864 modified: modified && Ext.apply({}, modified),
865 previousValues: previousValues && Ext.apply({}, previousValues)
866 };
867 }
868 },
869
870 /**
871 * Cancels all changes made in the current edit operation.
872 */
873 cancelEdit: function () {
874 var me = this,
875 editMemento = me.editMemento;
876
877 if (editMemento) {
878 me.editing = false;
879
880 // reset the modified state, nothing changed since the edit began
881 Ext.apply(me, editMemento);
882 me.editMemento = null;
883 }
884 },
885
886 /**
887 * Ends an edit. If any data was modified, the containing store is notified
888 * (ie, the store's `update` event will fire).
889 * @param {Boolean} [silent] True to not notify any stores of the change.
890 * @param {String[]} [modifiedFieldNames] Array of field names changed during edit.
891 */
892 endEdit: function (silent, modifiedFieldNames) {
893 var me = this,
894 editMemento = me.editMemento;
895
896 if (editMemento) {
897 me.editing = false;
898 me.editMemento = null;
899
900 // Since these reflect changes we never notified others about, the real set
901 // of "previousValues" is what we captured in the memento:
902 me.previousValues = editMemento.previousValues;
903
904 if (!silent) {
905 if (!modifiedFieldNames) {
906 modifiedFieldNames = me.getModifiedFieldNames(editMemento.data);
907 }
908
909 if (me.dirty || (modifiedFieldNames && modifiedFieldNames.length)) {
910 me.callJoined('afterEdit', [modifiedFieldNames]);
911 }
912 }
913 }
914 },
915
916 getField: function (name) {
917 return this.self.getField(name);
918 },
919
920 /**
921 * Get the fields array for this model.
922 * @return {Ext.data.field.Field[]} The fields array
923 */
924 getFields: function () {
925 return this.self.getFields();
926 },
927
928 getFieldsMap: function () {
929 return this.fieldsMap;
930 },
931
932 /**
933 * Get the idProperty for this model.
934 * @return {String} The idProperty
935 */
936 getIdProperty: function () {
937 return this.idProperty;
938 },
939
940 /**
941 * Returns the unique ID allocated to this model instance as defined by `idProperty`.
942 * @return {Number/String} The id
943 */
944 getId: function () {
945 return this.id;
946 },
947
948 /**
949 * Return a unique observable ID. Model is not observable but tree nodes (`Ext.data.NodeInterface`) are, so
950 * they must be globally unique within the {@link #observableType}.
951 * @protected
952 */
953 getObservableId: function() {
954 return this.internalId;
955 },
956
957 /**
958 * Sets the model instance's id field to the given id.
959 * @param {Number/String} id The new id.
960 * @param {Object} [options] See {@link #set}.
961 */
962 setId: function (id, options) {
963 this.set(this.idProperty, id, options);
964 },
965
966 /**
967 * This method returns the value of a field given its name prior to its most recent
968 * change.
969 * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
970 * @return {Object} The value of the given field prior to its current value. `undefined`
971 * if there is no previous value;
972 */
973 getPrevious: function (fieldName) {
974 var previousValues = this.previousValues;
975 return previousValues && previousValues[fieldName];
976 },
977
978 /**
979 * Returns true if the passed field name has been `{@link #modified}` since the load or last commit.
980 * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
981 * @return {Boolean}
982 */
983 isModified: function (fieldName) {
984 var modified = this.modified;
985 return !!(modified && modified.hasOwnProperty(fieldName));
986 },
987
988 /**
989 * Returns the original value of a modified field. If there is no modified value,
990 * `undefined` will be return. Also see {@link #isModified}.
991 * @param {String} fieldName The name of the field for which to return the original value.
992 * @return {Object} modified
993 */
994 getModified: function (fieldName) {
995 var out;
996 if (this.isModified(fieldName)) {
997 out = this.modified[fieldName];
998 }
999 return out;
1000 },
1001
1002 /**
1003 * Returns the value of the given field.
1004 * @param {String} fieldName The name of the field.
1005 * @return {Object} The value of the specified field.
1006 */
1007 get: function (fieldName) {
1008 return this.data[fieldName];
1009 },
1010
1011 // This object is used whenever the set() method is called and given a string as the
1012 // first argument. This approach saves memory (and GC costs) since we could be called
1013 // a lot.
1014 _singleProp: {},
1015
1016 _rejectOptions: {
1017 convert: false,
1018 silent: true
1019 },
1020
1021 /**
1022 * Sets the given field to the given value. For example:
1023 *
1024 * record.set('name', 'value');
1025 *
1026 * This method can also be passed an object containing multiple values to set at once.
1027 * For example:
1028 *
1029 * record.set({
1030 * name: 'value',
1031 * age: 42
1032 * });
1033 *
1034 * The following store events are fired when the modified record belongs to a store:
1035 *
1036 * - {@link Ext.data.Store#event-beginupdate beginupdate}
1037 * - {@link Ext.data.Store#event-update update}
1038 * - {@link Ext.data.Store#event-endupdate endupdate}
1039 *
1040 * @param {String/Object} fieldName The field to set, or an object containing key/value
1041 * pairs.
1042 * @param {Object} newValue The value for the field (if `fieldName` is a string).
1043 * @param {Object} [options] Options for governing this update.
1044 * @param {Boolean} [options.convert=true] Set to `false` to prevent any converters from
1045 * being called during the set operation. This may be useful when setting a large bunch of
1046 * raw values.
1047 * @param {Boolean} [options.dirty=true] Pass `false` if the field values are to be
1048 * understood as non-dirty (fresh from the server). When `true`, this change will be
1049 * reflected in the `modified` collection.
1050 * @param {Boolean} [options.commit=false] Pass `true` to call the {@link #commit} method
1051 * after setting fields. If this option is passed, the usual after change processing will
1052 * be bypassed. {@link #commit Commit} will be called even if there are no field changes.
1053 * @param {Boolean} [options.silent=false] Pass `true` to suppress notification of any
1054 * changes made by this call. Use with caution.
1055 * @return {String[]} The array of modified field names or null if nothing was modified.
1056 */
1057 set: function (fieldName, newValue, options) {
1058 var me = this,
1059 cls = me.self,
1060 data = me.data,
1061 modified = me.modified,
1062 prevVals = me.previousValues,
1063 session = me.session,
1064 single = Ext.isString(fieldName),
1065 opt = (single ? options : newValue),
1066 convertOnSet = opt ? opt.convert !== false : me.convertOnSet,
1067 fieldsMap = me.fieldsMap,
1068 silent = opt && opt.silent,
1069 commit = opt && opt.commit,
1070 updateRefs = !(opt && opt.refs === false) && session,
1071 // Don't need to do dirty processing with commit, since we'll always
1072 // end up with nothing modified and not dirty
1073 dirty = !(opt && opt.dirty === false && !commit),
1074 modifiedFieldNames = null,
1075 currentValue, field, idChanged, key, name, oldId, comparator, dep, dependents,
1076 i, dirtyRank=0, numFields, newId, rankedFields, reference, value, values;
1077
1078 if (single) {
1079 values = me._singleProp;
1080 values[fieldName] = newValue;
1081 } else {
1082 values = fieldName;
1083 }
1084
1085 if (!(rankedFields = cls.rankedFields)) {
1086 // On the first edit of a record of this type we need to ensure we have the
1087 // topo-sort done:
1088 rankedFields = cls.rankFields();
1089 }
1090 numFields = rankedFields.length;
1091
1092 do {
1093 for (name in values) {
1094 value = values[name];
1095 currentValue = data[name];
1096 comparator = me;
1097 field = fieldsMap[name];
1098
1099 if (field) {
1100 if (convertOnSet && field.convert) {
1101 value = field.convert(value, me);
1102 }
1103 comparator = field;
1104 reference = field.reference;
1105 } else {
1106 reference = null;
1107 }
1108
1109 if (comparator.isEqual(currentValue, value)) {
1110 continue; // new value is the same, so no change...
1111 }
1112
1113 data[name] = value;
1114 (modifiedFieldNames || (modifiedFieldNames = [])).push(name);
1115 (prevVals || (me.previousValues = prevVals = {}))[name] = currentValue;
1116
1117 // We need the cls to be present because it means the association class is loaded,
1118 // otherwise it could be pending.
1119 if (reference && reference.cls) {
1120 if (updateRefs) {
1121 session.updateReference(me, field, value, currentValue);
1122 }
1123 reference.onValueChange(me, session, value, currentValue);
1124 }
1125
1126 i = (dependents = field && field.dependents) && dependents.length;
1127 while (i-- > 0) {
1128 // we use the field instance to hold the dirty bit to avoid any
1129 // extra allocations... we'll clear this before we depart. We do
1130 // this so we can perform the fewest recalculations possible as
1131 // each dependent field only needs to be recalculated once.
1132 (dep = dependents[i]).dirty = true;
1133 dirtyRank = dirtyRank ? Math.min(dirtyRank, dep.rank) : dep.rank;
1134 }
1135
1136 if (!field || field.persist) {
1137 if (modified && modified.hasOwnProperty(name)) {
1138 if (!dirty || comparator.isEqual(modified[name], value)) {
1139 // The original value in me.modified equals the new value, so
1140 // the field is no longer modified:
1141 delete modified[name];
1142 me.dirty = -1; // fix me.dirty later (still truthy)
1143 }
1144 } else if (dirty) {
1145 if (!modified) {
1146 me.modified = modified = {}; // create only when needed
1147 }
1148 me.dirty = true;
1149 modified[name] = currentValue;
1150 }
1151 }
1152
1153 if (name === me.idField.name) {
1154 idChanged = true;
1155 oldId = currentValue;
1156 newId = value;
1157 }
1158 }
1159
1160 if (!dirtyRank) {
1161 // Unless there are dependent fields to process we can break now. This is
1162 // what will happen for all code pre-dating the depends or simply not
1163 // using it, so it will add very little overhead when not used.
1164 break;
1165 }
1166
1167 // dirtyRank has the minimum rank (a 1-based value) of any dependent field
1168 // that needs recalculating due to changes above. The way we go about this
1169 // is to use our helper object for processing single argument invocations
1170 // to process just this one field. This is because the act of setting it
1171 // may cause another field to be invalidated, so while we cannot know at
1172 // this moment all the fields we need to recalculate, we know that only
1173 // those following this field in rankedFields can possibly be among them.
1174
1175 field = rankedFields[dirtyRank - 1]; // dirtyRank is 1-based
1176 field.dirty = false; // clear just this field's dirty state
1177
1178 if (single) {
1179 delete values[fieldName]; // cleanup last value
1180 } else {
1181 values = me._singleProp; // switch over
1182 single = true;
1183 }
1184
1185 fieldName = field.name;
1186 values[fieldName] = data[fieldName];
1187 // We are now processing a dependent field, so we want to force a
1188 // convert to occur because it's the only way it will get a value
1189 convertOnSet = true;
1190
1191 // Since dirtyRank is 1-based and refers to the field we need to handle
1192 // on this pass, we can treat it like an index for a minute and look at
1193 // the next field on towards the end to find the index of the next dirty
1194 // field.
1195 for ( ; dirtyRank < numFields; ++dirtyRank) {
1196 if (rankedFields[dirtyRank].dirty) {
1197 break;
1198 }
1199 }
1200
1201 if (dirtyRank < numFields) {
1202 // We found a field after this one marked as dirty so make the index
1203 // a proper 1-based rank:
1204 ++dirtyRank;
1205 } else {
1206 // We did not find any more dirty fields after this one, so clear the
1207 // dirtyRank and we will perhaps fall out after the next update
1208 dirtyRank = 0;
1209 }
1210 } while (1);
1211
1212 if (me.dirty < 0) {
1213 // We might have removed the last modified field, so check to see if there
1214 // are any modified fields remaining and correct me.dirty:
1215 me.dirty = false;
1216 for (key in modified) {
1217 if (modified.hasOwnProperty(key)) {
1218 me.dirty = true;
1219 break;
1220 }
1221 }
1222 }
1223
1224 if (single) {
1225 // cleanup our reused object for next time... important to do this before
1226 // we fire any events or call anyone else (like afterEdit)!
1227 delete values[fieldName];
1228 }
1229
1230 ++me.generation;
1231
1232 if (idChanged) {
1233 me.id = newId;
1234 me.callJoined('onIdChanged', [oldId, newId]);
1235 }
1236
1237 if (commit) {
1238 me.commit(silent, modifiedFieldNames);
1239 } else if (!silent && !me.editing && modifiedFieldNames) {
1240 me.callJoined('afterEdit', [modifiedFieldNames]);
1241 }
1242
1243 return modifiedFieldNames;
1244 },
1245
1246 /**
1247 * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. Rejects
1248 * all changes made to the model instance since either creation, or the last commit operation. Modified fields are
1249 * reverted to their original values.
1250 *
1251 * Developers should subscribe to the {@link Ext.data.Store#event-update} event to have their code notified of reject
1252 * operations.
1253 *
1254 * @param {Boolean} [silent=false] `true` to skip notification of the owning store of the change.
1255 */
1256 reject: function (silent) {
1257 var me = this,
1258 modified = me.modified;
1259
1260 //<debug>
1261 if (me.erased) {
1262 Ext.raise('Cannot reject once a record has been erased.');
1263 }
1264 //</debug>
1265
1266 if (modified) {
1267 me.set(modified, me._rejectOptions);
1268 }
1269
1270 me.dropped = false;
1271 me.clearState();
1272
1273 if (!silent) {
1274 me.callJoined('afterReject');
1275 }
1276 },
1277
1278 /**
1279 * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all changes made to the
1280 * instance since either creation or the last commit operation.
1281 *
1282 * Developers should subscribe to the {@link Ext.data.Store#event-update} event to have their code notified of commit
1283 * operations.
1284 *
1285 * @param {Boolean} [silent=false] Pass `true` to skip notification of the owning store of the change.
1286 * @param {String[]} [modifiedFieldNames] Array of field names changed during sync with server if known.
1287 * Omit or pass `null` if unknown. An empty array means that it is known that no fields were modified
1288 * by the server's response.
1289 * Defaults to false.
1290 */
1291 commit: function (silent, modifiedFieldNames) {
1292 var me = this,
1293 versionProperty = me.versionProperty,
1294 data = me.data,
1295 erased;
1296
1297 me.clearState();
1298 if (versionProperty && !me.phantom && !isNaN(data[versionProperty])) {
1299 ++data[versionProperty];
1300 }
1301 me.phantom = false;
1302
1303 if (me.dropped) {
1304 me.erased = erased = true;
1305 }
1306
1307 if (!silent) {
1308 if (erased) {
1309 me.callJoined('afterErase');
1310 } else {
1311 me.callJoined('afterCommit', [modifiedFieldNames]);
1312 }
1313 }
1314 },
1315
1316 clearState: function() {
1317 var me = this;
1318
1319 me.dirty = me.editing = false;
1320 me.editMemento = me.modified = null;
1321 },
1322
1323 /**
1324 * Marks this record as `dropped` and waiting to be deleted on the server. When a
1325 * record is dropped, it is automatically removed from all association stores and
1326 * any child records associated to this record are also dropped (a "cascade delete")
1327 * depending on the `cascade` parameter.
1328 *
1329 * @param {Boolean} [cascade=true] Pass `false` to disable the cascade to drop child
1330 * records.
1331 * @since 5.0.0
1332 */
1333 drop: function (cascade) {
1334 var me = this,
1335 associations = me.associations,
1336 session = me.session,
1337 roleName;
1338
1339 if (me.erased || me.dropped) {
1340 return;
1341 }
1342
1343 me.dropped = true;
1344 if (associations && cascade !== false) {
1345 for (roleName in associations) {
1346 associations[roleName].onDrop(me, session);
1347 }
1348 }
1349 me.callJoined('afterDrop');
1350 if (me.phantom) {
1351 me.setErased();
1352 }
1353 },
1354
1355 /**
1356 * Tells this model instance that an observer is looking at it.
1357 * @param {Ext.data.Store} item The store to which this model has been added.
1358 */
1359 join: function (item) {
1360 var me = this,
1361 joined = me.joined;
1362
1363 // Optimize this, gets called a lot
1364 if (!joined) {
1365 joined = me.joined = [item];
1366 } else if (!joined.length) {
1367 joined[0] = item;
1368 } else {
1369 // TODO: do we need joined here? Perhaps push will do.
1370 Ext.Array.include(joined, item);
1371 }
1372
1373 if (item.isStore && !me.store) {
1374 /**
1375 * @property {Ext.data.Store} store
1376 * The {@link Ext.data.Store Store} to which this instance belongs.
1377 *
1378 * **Note:** If this instance is bound to multiple stores, this property
1379 * will reference only the first.
1380 */
1381 me.store = item;
1382 }
1383 },
1384
1385 /**
1386 * Tells this model instance that it has been removed from the store.
1387 * @param {Ext.data.Store} store The store from which this model has been removed.
1388 */
1389 unjoin: function (item) {
1390 var me = this,
1391 joined = me.joined,
1392
1393 // TreeModels are never joined to their TreeStore.
1394 // But unjoin is called by the base class's onCollectionRemove, so joined may be undefined.
1395 len = joined && joined.length,
1396 store = me.store,
1397 i;
1398
1399 if (len === 1 && joined[0] === item) {
1400 joined.length = 0;
1401 } else if (len) {
1402 Ext.Array.remove(joined, item);
1403 }
1404
1405 if (store === item) {
1406 store = null;
1407 if (joined) {
1408 for (i = 0, len = joined.length; i < len; ++i) {
1409 item = joined[i];
1410 if (item.isStore) {
1411 store = item;
1412 break;
1413 }
1414 }
1415 }
1416 me.store = store;
1417 }
1418 },
1419
1420 /**
1421 * Creates a clone of this record. States like `dropped`, `phantom` and `dirty` are
1422 * all preserved in the cloned record.
1423 *
1424 * @param {Ext.data.Session} [session] The session to which the new record
1425 * belongs.
1426 * @return {Ext.data.Model} The cloned record.
1427 */
1428 clone: function (session) {
1429 var me = this,
1430 modified = me.modified,
1431 ret = me.copy(me.id, session);
1432
1433 if (modified) {
1434 // Restore the modified fields state
1435 ret.modified = Ext.apply({}, modified);
1436 }
1437
1438 ret.dirty = me.dirty;
1439 ret.dropped = me.dropped;
1440 ret.phantom = me.phantom;
1441
1442 return ret;
1443 },
1444
1445 /**
1446 * Creates a clean copy of this record. The returned record will not consider any its
1447 * fields as modified.
1448 *
1449 * To generate a phantom instance with a new id pass `null`:
1450 *
1451 * var rec = record.copy(null); // clone the record but no id (one is generated)
1452 *
1453 * @param {String} [newId] A new id, defaults to the id of the instance being copied.
1454 * See `{@link Ext.data.Model#idProperty idProperty}`.
1455 * @param {Ext.data.Session} [session] The session to which the new record
1456 * belongs.
1457 *
1458 * @return {Ext.data.Model}
1459 */
1460 copy: function (newId, session) {
1461 var me = this,
1462 data = Ext.apply({}, me.data),
1463 idProperty = me.idProperty,
1464 T = me.self;
1465
1466 if (newId || newId === 0) {
1467 data[idProperty] = newId;
1468 } else if (newId === null) {
1469 delete data[idProperty];
1470 }
1471
1472 return new T(data, session);
1473 },
1474
1475 /**
1476 * Returns the configured Proxy for this Model.
1477 * @return {Ext.data.proxy.Proxy} The proxy
1478 */
1479 getProxy: function() {
1480 return this.self.getProxy();
1481 },
1482
1483 /**
1484 * Returns the `Ext.data.Validation` record holding the results of this record's
1485 * `validators`. This record is lazily created on first request and is then kept on
1486 * this record to be updated later.
1487 *
1488 * See the class description for more about `validators`.
1489 *
1490 * @param {Boolean} [refresh] Pass `false` to not call the `refresh` method on the
1491 * validation instance prior to returning it. Pass `true` to force a `refresh` of the
1492 * validation instance. By default the returned record is only refreshed if changes
1493 * have been made to this record.
1494 * @return {Ext.data.Validation} The `Validation` record for this record.
1495 * @since 5.0.0
1496 */
1497 getValidation: function (refresh) {
1498 var me = this,
1499 ret = me.validation;
1500
1501 if (!ret) {
1502 me.validation = ret = new Ext.data.Validation();
1503 ret.attach(me);
1504 }
1505
1506 if (refresh === true || (refresh !== false && ret.syncGeneration !== me.generation)) {
1507 ret.refresh(refresh);
1508 }
1509
1510 return ret;
1511 },
1512
1513 /**
1514 * Validates the current data against all of its configured {@link #validators}. The
1515 * returned collection holds an object for each reported problem from a `validator`.
1516 *
1517 * @return {Ext.data.ErrorCollection} The errors collection.
1518 * @deprecated 5.0 Use `getValidation` instead.
1519 */
1520 validate: function() {
1521 return new Ext.data.ErrorCollection().init(this);
1522 },
1523
1524 /**
1525 * Checks if the model is valid. See {@link #getValidation}.
1526 * @return {Boolean} True if the model is valid.
1527 */
1528 isValid: function () {
1529 return this.getValidation().isValid();
1530 },
1531
1532 /**
1533 * Returns a url-suitable string for this model instance. By default this just returns the name of the Model class
1534 * followed by the instance ID - for example an instance of MyApp.model.User with ID 123 will return 'user/123'.
1535 * @return {String} The url string for this model instance.
1536 */
1537 toUrl: function() {
1538 var pieces = this.$className.split('.'),
1539 name = pieces[pieces.length - 1].toLowerCase();
1540
1541 return name + '/' + this.getId();
1542 },
1543
1544 /**
1545 * @localdoc Destroys the model using the configured proxy. The erase action is
1546 * asynchronous. Any processing of the erased record should be done in a callback.
1547 *
1548 * Ext.define('MyApp.model.User', {
1549 * extend: 'Ext.data.Model',
1550 * fields: [
1551 * {name: 'id', type: 'int'},
1552 * {name: 'name', type: 'string'}
1553 * ],
1554 * proxy: {
1555 * type: 'ajax',
1556 * url: 'server.url'
1557 * }
1558 * });
1559 *
1560 * var user = new MyApp.model.User({
1561 * name: 'Foo'
1562 * });
1563 *
1564 * // pass the phantom record data to the server to be saved
1565 * user.save({
1566 * success: function(record, operation) {
1567 * // do something if the save succeeded
1568 * // erase the created record
1569 * record.erase({
1570 * failure: function(record, operation) {
1571 * // do something if the erase failed
1572 * },
1573 * success: function(record, operation) {
1574 * // do something if the erase succeeded
1575 * },
1576 * callback: function(record, operation, success) {
1577 * // do something if the erase succeeded or failed
1578 * }
1579 * });
1580 * }
1581 * });
1582 *
1583 * **NOTE:** If a {@link #phantom} record is erased it will not be processed via the
1584 * proxy. However, any passed `success` or `callback` functions will be called.
1585 *
1586 * The options param is an {@link Ext.data.operation.Destroy} config object
1587 * containing success, failure and callback functions, plus optional scope.
1588 *
1589 * @inheritdoc #method-load
1590 * @return {Ext.data.operation.Destroy} The destroy operation
1591 */
1592 erase: function(options) {
1593 var me = this;
1594
1595 me.erasing = true;
1596
1597 // Drop causes a removal from the backing Collection.
1598 // The store's onCollectionRemove will respond to this by adding the record to its "to remove" stack and setting its needsSync
1599 // flag unless the above "erasing" flag is set.
1600 me.drop();
1601
1602 me.erasing = false;
1603 return me.save(options);
1604 },
1605
1606 setErased: function() {
1607 this.erased = true;
1608 this.callJoined('afterErase');
1609 },
1610
1611 /**
1612 * Gets an object of only the fields that have been modified since this record was
1613 * created or committed. Only persistent fields are tracked in the `modified` set so
1614 * this method will only return changes to persistent fields.
1615 *
1616 * For more control over the returned data, see `{@link #getData}`.
1617 * @return {Object}
1618 */
1619 getChanges: function () {
1620 return this.getData(this._getChangesOptions);
1621 },
1622
1623 /**
1624 * Returns the array of fields that are declared as critical (must always send).
1625 * @return {Ext.data.field.Field[]}
1626 */
1627 getCriticalFields: function () {
1628 var cls = this.self,
1629 ret = cls.criticalFields;
1630
1631 if (!ret) {
1632 cls.rankFields();
1633 ret = cls.criticalFields;
1634 }
1635
1636 return ret;
1637 },
1638
1639 /**
1640 * This method is called by the {@link Ext.data.reader.Reader} after loading a model from
1641 * the server. This is after processing any inline associations that are available.
1642 *
1643 * @method onLoad
1644 *
1645 * @protected
1646 * @template
1647 */
1648
1649 /**
1650 * Gets all of the data from this Models *loaded* associations. It does this
1651 * recursively. For example if we have a User which hasMany Orders, and each Order
1652 * hasMany OrderItems, it will return an object like this:
1653 *
1654 * {
1655 * orders: [
1656 * {
1657 * id: 123,
1658 * status: 'shipped',
1659 * orderItems: [
1660 * ...
1661 * ]
1662 * }
1663 * ]
1664 * }
1665 *
1666 * @param {Object} [result] The object on to which the associations will be added. If
1667 * no object is passed one is created. This object is then returned.
1668 * @param {Boolean/Object} [options] An object containing options describing the data
1669 * desired.
1670 * @param {Boolean} [options.associated=true] Pass `true` to include associated data from
1671 * other associated records.
1672 * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
1673 * have been modified. Note that field modifications are only tracked for fields that
1674 * are not declared with `persist` set to `false`. In other words, only persistent
1675 * fields have changes tracked so passing `true` for this means `options.persist` is
1676 * redundant.
1677 * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
1678 * This is only meaningful when `options.changes` is `true` since critical fields may
1679 * not have been modified.
1680 * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
1681 * This is implied when `options.changes` is set to `true`.
1682 * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
1683 * method on the returned fields.
1684 * @return {Object} The nested data set for the Model's loaded associations.
1685 */
1686 getAssociatedData: function (result, options) {
1687 var me = this,
1688 associations = me.associations,
1689 deep, i, item, items, itemData, length,
1690 record, role, roleName, opts, clear, associated;
1691
1692 result = result || {};
1693
1694 me.$gathering = 1;
1695
1696 if (options) {
1697 options = Ext.Object.chain(options);
1698 }
1699
1700 for (roleName in associations) {
1701 role = associations[roleName];
1702 item = role.getAssociatedItem(me);
1703 if (!item || item.$gathering) {
1704 continue;
1705 }
1706
1707 if (item.isStore) {
1708 item.$gathering = 1;
1709
1710 items = item.getData().items; // get the records for the store
1711 length = items.length;
1712 itemData = [];
1713
1714 for (i = 0; i < length; ++i) {
1715 // NOTE - we don't check whether the record is gathering here because
1716 // we cannot remove it from the store (it would invalidate the index
1717 // values and misrepresent the content). Instead we tell getData to
1718 // only get the fields vs descend further.
1719 record = items[i];
1720 deep = !record.$gathering;
1721 record.$gathering = 1;
1722 if (options) {
1723 associated = options.associated;
1724 if (associated === undefined) {
1725 options.associated = deep;
1726 clear = true;
1727 } else if (!deep) {
1728 options.associated = false;
1729 clear = true;
1730 }
1731 opts = options;
1732 } else {
1733 opts = deep ? me._getAssociatedOptions: me._getNotAssociatedOptions;
1734 }
1735 itemData.push(record.getData(opts));
1736 if (clear) {
1737 options.associated = associated;
1738 clear = false;
1739 }
1740 delete record.$gathering;
1741 }
1742
1743 delete item.$gathering;
1744 } else {
1745 opts = options || me._getAssociatedOptions;
1746 if (options && options.associated === undefined) {
1747 opts.associated = true;
1748 }
1749 itemData = item.getData(opts);
1750 }
1751
1752 result[roleName] = itemData;
1753 }
1754
1755 delete me.$gathering;
1756
1757 return result;
1758 },
1759
1760 /**
1761 * Gets all values for each field in this model and returns an object containing the
1762 * current data. This can be tuned by passing an `options` object with various
1763 * properties describing the desired result. Passing `true` simply returns all fields
1764 * *and* all associated record data.
1765 *
1766 * @param {Boolean/Object} [options] An object containing options describing the data
1767 * desired. If `true` is passed it is treated as an object with `associated` set to
1768 * `true`.
1769 * @param {Boolean} [options.associated=false] Pass `true` to include associated data.
1770 * This is equivalent to pass `true` as the only argument. See `getAssociatedData`.
1771 * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
1772 * have been modified. Note that field modifications are only tracked for fields that
1773 * are not declared with `persist` set to `false`. In other words, only persistent
1774 * fields have changes tracked so passing `true` for this means `options.persist` is
1775 * redundant.
1776 * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
1777 * This is only meaningful when `options.changes` is `true` since critical fields may
1778 * not have been modified.
1779 * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
1780 * This is implied when `options.changes` is set to `true`.
1781 * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
1782 * method on the returned fields.
1783 * @return {Object} An object containing all the values in this model.
1784 */
1785 getData: function (options) {
1786 var me = this,
1787 ret = {},
1788 opts = (options === true) ? me._getAssociatedOptions : (options || ret), //cheat
1789 data = me.data,
1790 associated = opts.associated,
1791 changes = opts.changes,
1792 critical = changes && opts.critical,
1793 content = changes ? me.modified : data,
1794 fieldsMap = me.fieldsMap,
1795 persist = opts.persist,
1796 serialize = opts.serialize,
1797 criticalFields, field, n, name, value;
1798
1799 // DON'T use "opts" from here on...
1800
1801 // Keep in mind the two legacy use cases:
1802 // - getData() ==> Ext.apply({}, me.data)
1803 // - getData(true) ==> Ext.apply(Ext.apply({}, me.data), me.getAssociatedData())
1804
1805 if (content) { // when processing only changes, me.modified could be null
1806 for (name in content) {
1807 value = data[name];
1808
1809 field = fieldsMap[name];
1810 if (field) {
1811 if (persist && !field.persist) {
1812 continue;
1813 }
1814 if (serialize && field.serialize) {
1815 value = field.serialize(value, me);
1816 }
1817 }
1818
1819 ret[name] = value;
1820 }
1821 }
1822
1823 if (critical) {
1824 criticalFields = me.self.criticalFields || me.getCriticalFields();
1825 for (n = criticalFields.length; n-- > 0; ) {
1826 name = (field = criticalFields[n]).name;
1827
1828 if (!(name in ret)) {
1829 value = data[name];
1830 if (serialize && field.serialize) {
1831 value = field.serialize(value, me);
1832 }
1833 ret[name] = value;
1834 }
1835 }
1836 }
1837
1838 if (associated) {
1839 me.getAssociatedData(ret, opts); // pass ret so new data is added to our object
1840 }
1841
1842 return ret;
1843 },
1844
1845 /**
1846 * Returns the array of fields that are declared as non-persist or "transient".
1847 * @return {Ext.data.field.Field[]}
1848 * @since 5.0.0
1849 */
1850 getTransientFields: function () {
1851 var cls = this.self,
1852 ret = cls.transientFields;
1853
1854 if (!ret) {
1855 cls.rankFields(); // populates transientFields as well as rank
1856 ret = cls.transientFields;
1857 }
1858
1859 return ret;
1860 },
1861
1862 /**
1863 * Checks whether this model is loading data from the {@link #proxy}.
1864 * @return {Boolean} `true` if in a loading state.
1865 */
1866 isLoading: function() {
1867 return !!this.loadOperation;
1868 },
1869
1870 /**
1871 * Aborts a pending {@link #load} operation. If the record is not loading, this does nothing.
1872 */
1873 abort: function() {
1874 var operation = this.loadOperation;
1875 if (operation) {
1876 operation.abort();
1877 }
1878 },
1879
1880 /**
1881 * @localdoc Loads the model instance using the configured proxy. The load action
1882 * is asynchronous. Any processing of the loaded record should be done in a
1883 * callback.
1884 *
1885 * Ext.define('MyApp.model.User', {
1886 * extend: 'Ext.data.Model',
1887 * fields: [
1888 * {name: 'id', type: 'int'},
1889 * {name: 'name', type: 'string'}
1890 * ],
1891 * proxy: {
1892 * type: 'ajax',
1893 * url: 'server.url'
1894 * }
1895 * });
1896 *
1897 * var user = new MyApp.model.User();
1898 * user.load({
1899 * scope: this,
1900 * failure: function(record, operation) {
1901 * // do something if the load failed
1902 * },
1903 * success: function(record, operation) {
1904 * // do something if the load succeeded
1905 * },
1906 * callback: function(record, operation, success) {
1907 * // do something whether the load succeeded or failed
1908 * }
1909 * });
1910 *
1911 * The options param is an {@link Ext.data.operation.Read} config object containing
1912 * success, failure and callback functions, plus optional scope.
1913 *
1914 * @param {Object} [options] Options to pass to the proxy.
1915 * @param {Function} options.success A function to be called when the
1916 * model is processed by the proxy successfully.
1917 * The callback is passed the following parameters:
1918 * @param {Ext.data.Model} options.success.record The record.
1919 * @param {Ext.data.operation.Operation} options.success.operation The operation.
1920 *
1921 * @param {Function} options.failure A function to be called when the
1922 * model is unable to be processed by the server.
1923 * The callback is passed the following parameters:
1924 * @param {Ext.data.Model} options.failure.record The record.
1925 * @param {Ext.data.operation.Operation} options.failure.operation The operation.
1926 *
1927 * @param {Function} options.callback A function to be called whether the proxy
1928 * transaction was successful or not.
1929 * The callback is passed the following parameters:
1930 * @param {Ext.data.Model} options.callback.record The record.
1931 * @param {Ext.data.operation.Operation} options.callback.operation The operation.
1932 * @param {Boolean} options.callback.success `true` if the operation was successful.
1933 *
1934 * @param {Object} options.scope The scope in which to execute the callback
1935 * functions. Defaults to the model instance.
1936 *
1937 * @return {Ext.data.operation.Read} The read operation.
1938 */
1939 load: function(options) {
1940 options = Ext.apply({}, options);
1941
1942 var me = this,
1943 scope = options.scope || me,
1944 proxy = me.getProxy(),
1945 callback = options.callback,
1946 operation = me.loadOperation,
1947 id = me.getId(),
1948 extras;
1949
1950 if (operation) {
1951 // Already loading, push any callbacks on and jump out
1952 extras = operation.extraCalls;
1953 if (!extras) {
1954 extras = operation.extraCalls = [];
1955 }
1956 extras.push(options);
1957 return operation;
1958 }
1959
1960 //<debug>
1961 var doIdCheck = true;
1962 if (me.phantom) {
1963 doIdCheck = false;
1964 }
1965 //</debug>
1966
1967 options.id = id;
1968
1969 // Always set the recordCreator. If we have a session, we're already
1970 // part of said session, so we don't need to handle that.
1971 options.recordCreator = function(data, type, readOptions) {
1972 // Important to change this here, because we might be loading associations,
1973 // so we do not want this to propagate down. If we have a session, use that
1974 // so that we end up getting the same record. Otherwise, just remove it.
1975 var session = me.session;
1976 if (readOptions) {
1977 readOptions.recordCreator = session ? session.recordCreator : null;
1978 }
1979 me.set(data, me._commitOptions);
1980 //<debug>
1981 // Do the id check after set since converters may have run
1982 if (doIdCheck && me.getId() !== id) {
1983 Ext.raise('Invalid record id returned for ' + id + '@' + me.entityName);
1984 }
1985 //</debug>
1986 return me;
1987 };
1988
1989 options.internalCallback = function(operation) {
1990 var success = operation.wasSuccessful() && operation.getRecords().length > 0,
1991 op = me.loadOperation,
1992 extras = op.extraCalls,
1993 successFailArgs = [me, operation],
1994 callbackArgs = [me, operation, success],
1995 i, len;
1996
1997 me.loadOperation = null;
1998
1999 if (success) {
2000 Ext.callback(options.success, scope, successFailArgs);
2001 } else {
2002 Ext.callback(options.failure, scope, successFailArgs);
2003 }
2004 Ext.callback(callback, scope, callbackArgs);
2005
2006 // Some code repetition here, however in a vast majority of cases
2007 // we'll only have a single callback, so optimize for that case rather
2008 // than setup arrays for all the callback options
2009 if (extras) {
2010 for (i = 0, len = extras.length; i < len; ++i) {
2011 options = extras[i];
2012 if (success) {
2013 Ext.callback(options.success, scope, successFailArgs);
2014 } else {
2015 Ext.callback(options.failure, scope, successFailArgs);
2016 }
2017 Ext.callback(options.callback, scope, callbackArgs);
2018 }
2019 }
2020 me.callJoined('afterLoad');
2021 };
2022 delete options.callback;
2023
2024 me.loadOperation = operation = proxy.createOperation('read', options);
2025 operation.execute();
2026
2027 return operation;
2028 },
2029
2030 /**
2031 * @localdoc Saves the model instance using the configured proxy. The save action
2032 * is asynchronous. Any processing of the saved record should be done in a callback.
2033 *
2034 * Create example:
2035 *
2036 * Ext.define('MyApp.model.User', {
2037 * extend: 'Ext.data.Model',
2038 * fields: [
2039 * {name: 'id', type: 'int'},
2040 * {name: 'name', type: 'string'}
2041 * ],
2042 * proxy: {
2043 * type: 'ajax',
2044 * url: 'server.url'
2045 * }
2046 * });
2047 *
2048 * var user = new MyApp.model.User({
2049 * name: 'Foo'
2050 * });
2051 *
2052 * // pass the phantom record data to the server to be saved
2053 * user.save({
2054 * failure: function(record, operation) {
2055 * // do something if the save failed
2056 * },
2057 * success: function(record, operation) {
2058 * // do something if the save succeeded
2059 * },
2060 * callback: function(record, operation, success) {
2061 * // do something whether the save succeeded or failed
2062 * }
2063 * });
2064 *
2065 * The response from a create operation should include the ID for the newly created
2066 * record:
2067 *
2068 * // sample response
2069 * {
2070 * success: true,
2071 * id: 1
2072 * }
2073 *
2074 * // the id may be nested if the proxy's reader has a rootProperty config
2075 * Ext.define('MyApp.model.User', {
2076 * extend: 'Ext.data.Model',
2077 * proxy: {
2078 * type: 'ajax',
2079 * url: 'server.url',
2080 * reader: {
2081 * type: 'ajax',
2082 * rootProperty: 'data'
2083 * }
2084 * }
2085 * });
2086 *
2087 * // sample nested response
2088 * {
2089 * success: true,
2090 * data: {
2091 * id: 1
2092 * }
2093 * }
2094 *
2095 * (Create + ) Update example:
2096 *
2097 * Ext.define('MyApp.model.User', {
2098 * extend: 'Ext.data.Model',
2099 * fields: [
2100 * {name: 'id', type: 'int'},
2101 * {name: 'name', type: 'string'}
2102 * ],
2103 * proxy: {
2104 * type: 'ajax',
2105 * url: 'server.url'
2106 * }
2107 * });
2108 *
2109 * var user = new MyApp.model.User({
2110 * name: 'Foo'
2111 * });
2112 * user.save({
2113 * success: function(record, operation) {
2114 * record.set('name', 'Bar');
2115 * // updates the remote record via the proxy
2116 * record.save();
2117 * }
2118 * });
2119 *
2120 * (Create + ) Destroy example - see also {@link #erase}:
2121 *
2122 * Ext.define('MyApp.model.User', {
2123 * extend: 'Ext.data.Model',
2124 * fields: [
2125 * {name: 'id', type: 'int'},
2126 * {name: 'name', type: 'string'}
2127 * ],
2128 * proxy: {
2129 * type: 'ajax',
2130 * url: 'server.url'
2131 * }
2132 * });
2133 *
2134 * var user = new MyApp.model.User({
2135 * name: 'Foo'
2136 * });
2137 * user.save({
2138 * success: function(record, operation) {
2139 * record.drop();
2140 * // destroys the remote record via the proxy
2141 * record.save();
2142 * }
2143 * });
2144 *
2145 * **NOTE:** If a {@link #phantom} record is {@link #drop dropped} and subsequently
2146 * saved it will not be processed via the proxy. However, any passed `success`
2147 * or `callback` functions will be called.
2148 *
2149 * The options param is an Operation config object containing success, failure and
2150 * callback functions, plus optional scope. The type of Operation depends on the
2151 * state of the model being saved.
2152 *
2153 * - {@link #phantom} model - {@link Ext.data.operation.Create}
2154 * - {@link #isModified modified} model - {@link Ext.data.operation.Update}
2155 * - {@link #dropped} model - {@link Ext.data.operation.Destroy}
2156 *
2157 * @inheritdoc #method-load
2158 * @return {Ext.data.operation.Create/Ext.data.operation.Update/Ext.data.operation.Destroy}
2159 * The operation instance for saving this model. The type of operation returned
2160 * depends on the model state at the time of the action.
2161 *
2162 * - {@link #phantom} model - {@link Ext.data.operation.Create}
2163 * - {@link #isModified modified} model - {@link Ext.data.operation.Update}
2164 * - {@link #dropped} model - {@link Ext.data.operation.Destroy}
2165 */
2166 save: function(options) {
2167 options = Ext.apply({}, options);
2168
2169 var me = this,
2170 phantom = me.phantom,
2171 dropped = me.dropped,
2172 action = dropped ? 'destroy' : (phantom ? 'create' : 'update'),
2173 scope = options.scope || me,
2174 callback = options.callback,
2175 proxy = me.getProxy(),
2176 operation;
2177
2178 options.records = [me];
2179 options.internalCallback = function(operation) {
2180 var args = [me, operation],
2181 success = operation.wasSuccessful();
2182 if (success) {
2183 Ext.callback(options.success, scope, args);
2184 } else {
2185 Ext.callback(options.failure, scope, args);
2186 }
2187 args.push(success);
2188 Ext.callback(callback, scope, args);
2189 };
2190 delete options.callback;
2191
2192 operation = proxy.createOperation(action, options);
2193
2194 // Not a phantom, then we must perform this operation on the remote datasource.
2195 // Record will be removed from the store in the callback upon a success response
2196 if (dropped && phantom) {
2197 // If it's a phantom, then call the callback directly with a dummy successful ResultSet
2198 operation.setResultSet(Ext.data.reader.Reader.prototype.nullResultSet);
2199 me.setErased();
2200 operation.setSuccessful(true);
2201 } else {
2202 operation.execute();
2203 }
2204 return operation;
2205 },
2206
2207 //-------------------------------------------------------------------------
2208 // Statics
2209
2210 inheritableStatics: {
2211 /**
2212 * This method adds the given set of fields to this model class.
2213 *
2214 * @param {String[]/Object[]} newFields The new fields to add. Based on the `name`
2215 * of a field this may replace a previous field definition.
2216 *
2217 * @protected
2218 * @static
2219 * @inheritable
2220 * @since 5.0.0
2221 */
2222 addFields: function (newFields) {
2223 this.replaceFields(newFields);
2224 },
2225
2226 /**
2227 * This method replaces the specified set of fields with a given set of new fields.
2228 * Fields should normally be considered immutable, but if the timing is right (that
2229 * is, before derived classes are declared), it is permissible to change the fields
2230 * collection.
2231 *
2232 * @param {String[]/Object[]} newFields The new fields to add. Based on the `name`
2233 * of a field this may replace a previous field definition.
2234 * @param {Boolean/String[]} removeFields The names of fields to remove or `true`
2235 * to remove all existing fields. Removes are processed first followed by adds so
2236 * if a field name appears in `newFields` as well that field will effectively be
2237 * added (however, in that case there is no need to include the field in this
2238 * array).
2239 *
2240 * @protected
2241 * @static
2242 * @inheritable
2243 * @since 5.0.0
2244 */
2245 replaceFields: function (newFields, removeFields) {
2246 var me = this,
2247 proto = me.prototype,
2248 Field = Ext.data.field.Field,
2249 fields = me.fields,
2250 fieldsMap = me.fieldsMap,
2251 ordinals = me.fieldOrdinals,
2252 field, i, idField, len, name, ordinal;
2253
2254 if (removeFields === true) {
2255 fields.length = 0;
2256 me.fieldsMap = fieldsMap = {};
2257 me.fieldOrdinals = ordinals = {};
2258 } else if (removeFields) {
2259 for (i = removeFields.length; i-- > 0; ) {
2260 name = removeFields[i];
2261 if (name in ordinals) {
2262 delete ordinals[name];
2263 delete fieldsMap[name];
2264 }
2265 }
2266
2267 for (i = 0, len = fields.length; i < len; ++i) {
2268 name = (field = fields[i]).name;
2269
2270 if (name in ordinals) {
2271 ordinals[name] = i;
2272 } else {
2273 // This field is being removed (it is no longer in ordinals).
2274 fields.splice(i, 1);
2275 --i;
2276 --len;
2277 // we need to do this forwards so that ordinals don't become
2278 // invalid due to a splice
2279 }
2280 }
2281 }
2282
2283 for (i = 0, len = newFields ? newFields.length : 0; i < len; i++) {
2284 name = (field = newFields[i]).name;
2285
2286 if (!(name in ordinals)) {
2287 ordinals[name] = ordinal = fields.length; // 0-based
2288 fields.push(field = Field.create(field));
2289
2290 fieldsMap[name] = field;
2291 field.ordinal = ordinal;
2292 field.definedBy = field.owner = this; // Ext.data.NodeInterface
2293 }
2294 }
2295
2296 // The idField could have been replaced, so reacquire it.
2297 me.idField = proto.idField = idField = fieldsMap[proto.idProperty];
2298 idField.allowNull = idField.critical = idField.identifier = true;
2299 idField.defaultValue = null;
2300
2301 // In case we've created the initializer we need to zap it so we recreate it
2302 // next time. Likewise with field ranking.
2303 me.initializeFn = me.rankedFields = me.transientFields = me.criticalFields = null;
2304 },
2305
2306 /**
2307 * Removes the given set of fields from this model.
2308 *
2309 * @param {Boolean/String[]} removeFields The names of fields to remove or `true`
2310 * to remove all existing fields. Removes are processed first followed by adds so
2311 * if a field name appears in `newFields` as well that field will effectively be
2312 * added (however, in that case there is no need to include the field in this
2313 * array).
2314 *
2315 * @protected
2316 * @static
2317 * @inheritable
2318 * @since 5.0.0
2319 */
2320 removeFields: function (removeFields) {
2321 this.replaceFields(null, removeFields);
2322 },
2323
2324 /**
2325 * @private
2326 * @static
2327 * @inheritable
2328 */
2329 getIdFromData: function(data) {
2330 var T = this,
2331 idField = T.idField,
2332 id = idField.calculated ? (new T(data)).id : data[idField.name];
2333
2334 return id;
2335 },
2336
2337 /**
2338 * @private
2339 * @static
2340 * @inheritable
2341 */
2342 createWithId: function (id, data, session) {
2343 var d = data,
2344 T = this;
2345
2346 if (id || id === 0) {
2347 d = {};
2348 if (data) {
2349 Ext.apply(d, data);
2350 }
2351
2352 d[T.idField.name] = id;
2353 }
2354
2355 return new T(d, session);
2356 },
2357
2358 /**
2359 * @private
2360 * @static
2361 * @inheritable
2362 */
2363 getFields: function() {
2364 return this.fields;
2365 },
2366
2367 /**
2368 * @private
2369 * @static
2370 * @inheritable
2371 */
2372 getFieldsMap: function() {
2373 return this.fieldsMap;
2374 },
2375
2376 /**
2377 * @private
2378 * @static
2379 * @inheritable
2380 */
2381 getField: function (name) {
2382 return this.fieldsMap[name] || null;
2383 },
2384
2385 /**
2386 * Returns the configured Proxy for this Model.
2387 * @return {Ext.data.proxy.Proxy} The proxy
2388 * @static
2389 * @inheritable
2390 */
2391 getProxy: function() {
2392 var me = this,
2393 proxy = me.proxy,
2394 defaultProxy = me.defaultProxy,
2395 defaults;
2396
2397 if (!proxy) {
2398 // Check what was defined by the class (via onClassExtended):
2399 proxy = me.proxyConfig;
2400
2401 if (!proxy && defaultProxy) {
2402 proxy = defaultProxy;
2403 }
2404
2405 if (!proxy || !proxy.isProxy) {
2406 if (typeof proxy === 'string') {
2407 proxy = {
2408 type: proxy
2409 };
2410 }
2411 // We have nothing or a config for the proxy. Get some defaults from
2412 // the Schema and smash anything we've provided over the top.
2413 defaults = me.schema.constructProxy(me);
2414 proxy = proxy ? Ext.merge(defaults, proxy) : defaults;
2415 }
2416
2417 proxy = me.setProxy(proxy);
2418 }
2419
2420 return proxy;
2421 },
2422
2423 /**
2424 * Sets the Proxy to use for this model. Accepts any options that can be accepted by
2425 * {@link Ext#createByAlias Ext.createByAlias}.
2426 * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
2427 * @return {Ext.data.proxy.Proxy}
2428 * @static
2429 * @inheritable
2430 */
2431 setProxy: function (proxy) {
2432 var me = this,
2433 model;
2434
2435 if (proxy) {
2436 if (!proxy.isProxy) {
2437 proxy = Ext.Factory.proxy(proxy);
2438 } else {
2439 model = proxy.getModel();
2440 if (model && model !== me) {
2441 proxy = proxy.clone();
2442 }
2443 }
2444
2445 proxy.setModel(me);
2446 }
2447
2448 return (me.prototype.proxy = me.proxy = proxy);
2449 },
2450
2451 /**
2452 * Asynchronously loads a model instance by id. Any processing of the loaded
2453 * record should be done in a callback.
2454 *
2455 * Sample usage:
2456 *
2457 * Ext.define('MyApp.User', {
2458 * extend: 'Ext.data.Model',
2459 * fields: [
2460 * {name: 'id', type: 'int'},
2461 * {name: 'name', type: 'string'}
2462 * ]
2463 * });
2464 *
2465 * MyApp.User.load(10, {
2466 * scope: this,
2467 * failure: function(record, operation) {
2468 * //do something if the load failed
2469 * },
2470 * success: function(record, operation) {
2471 * //do something if the load succeeded
2472 * },
2473 * callback: function(record, operation, success) {
2474 * //do something whether the load succeeded or failed
2475 * }
2476 * });
2477 *
2478 * @param {Number/String} id The ID of the model to load.
2479 * **NOTE:** The model returned must have an ID matching the param in the load
2480 * request.
2481 *
2482 * @param {Object} [options] The options param is an
2483 * {@link Ext.data.operation.Read} config object containing success, failure and
2484 * callback functions, plus optional scope.
2485 *
2486 * @param {Function} options.success A function to be called when the
2487 * model is processed by the proxy successfully.
2488 * The callback is passed the following parameters:
2489 * @param {Ext.data.Model} options.success.record The record.
2490 * @param {Ext.data.operation.Operation} options.success.operation The operation.
2491 *
2492 * @param {Function} options.failure A function to be called when the
2493 * model is unable to be processed by the server.
2494 * The callback is passed the following parameters:
2495 * @param {Ext.data.Model} options.failure.record The record.
2496 * @param {Ext.data.operation.Operation} options.failure.operation The operation.
2497 *
2498 * @param {Function} options.callback A function to be called whether the proxy
2499 * transaction was successful or not.
2500 * The callback is passed the following parameters:
2501 * @param {Ext.data.Model} options.callback.record The record.
2502 * @param {Ext.data.operation.Operation} options.callback.operation The
2503 * operation.
2504 * @param {Boolean} options.callback.success `true` if the operation was
2505 * successful.
2506 *
2507 * @param {Object} options.scope The scope in which to execute the callback
2508 * functions. Defaults to the model instance.
2509 *
2510 * @param {Ext.data.Session} [session] The session for this record.
2511 *
2512 * @return {Ext.data.Model} The newly created model. Note that the model will
2513 * (probably) still be loading once it is returned from this method. To do any
2514 * post-processing on the data, the appropriate place to do see is in the
2515 * callback.
2516 *
2517 * @static
2518 * @inheritable
2519 */
2520 load: function(id, options, session) {
2521 var data = {},
2522 rec;
2523
2524 data[this.prototype.idProperty] = id;
2525 rec = new this(data, session);
2526
2527 rec.load(options);
2528 return rec;
2529 }
2530 },
2531
2532 deprecated: {
2533 5: {
2534 methods: {
2535 hasId: null,
2536 markDirty: null,
2537 setDirty: null,
2538 eachStore: function (callback, scope) {
2539 var me = this,
2540 stores = me.stores,
2541 len = stores.length,
2542 i;
2543
2544 for (i = 0; i < len; ++i) {
2545 callback.call(scope, stores[i]);
2546 }
2547 },
2548
2549 join: function(item) {
2550 var me = this,
2551 stores = me.stores,
2552 joined = me.joined;
2553
2554 if (!joined) {
2555 joined = me.joined = [item];
2556 } else {
2557 joined.push(item);
2558 }
2559
2560 if (item.isStore) {
2561 me.store = me.store || item;
2562 if (!stores) {
2563 stores = me.stores = [];
2564 }
2565 stores.push(item);
2566 }
2567 },
2568
2569 unjoin: function(item) {
2570 var me = this,
2571 stores = me.stores,
2572 joined = me.joined;
2573
2574 if (joined.length === 1) {
2575 joined.length = 0;
2576 } else {
2577 Ext.Array.remove(joined, item);
2578 }
2579
2580 if (item.isStore) {
2581 Ext.Array.remove(stores, item);
2582 me.store = stores[0] || null;
2583 }
2584 }
2585 },
2586 properties: {
2587 persistenceProperty: null
2588 },
2589 inheritableStatics: {
2590 methods: {
2591 setFields: null
2592 }
2593 }
2594 }
2595 },
2596
2597 //-------------------------------------------------------------------------
2598 privates: {
2599 _commitOptions: {
2600 commit: true
2601 },
2602 _getChangesOptions: {
2603 changes: true
2604 },
2605 _getAssociatedOptions: {
2606 associated: true
2607 },
2608 _getNotAssociatedOptions: {
2609 associated: false
2610 },
2611
2612 /**
2613 * Copies data from the passed record into this record. If the passed record is undefined, does nothing.
2614 *
2615 * If this is a phantom record (represented only in the client, with no corresponding database entry), and
2616 * the source record is not a phantom, then this record acquires the id of the source record.
2617 *
2618 * @param {Ext.data.Model} sourceRecord The record to copy data from.
2619 * @return {String[]} The names of the fields which changed value.
2620 * @private
2621 */
2622 copyFrom: function (sourceRecord) {
2623 var me = this,
2624 fields = me.fields,
2625 fieldCount = fields.length,
2626 modifiedFieldNames = [],
2627 field, i = 0,
2628 myData,
2629 sourceData,
2630 idProperty = me.idProperty,
2631 name,
2632 value;
2633
2634 if (sourceRecord) {
2635 myData = me.data;
2636 sourceData = sourceRecord.data;
2637 for (; i < fieldCount; i++) {
2638 field = fields[i];
2639 name = field.name;
2640
2641 // Do not use setters.
2642 // Copy returned values in directly from the data object.
2643 // Converters have already been called because new Records
2644 // have been created to copy from.
2645 // This is a direct record-to-record value copy operation.
2646 // don't copy the id, we'll do it at the end
2647 if (name !== idProperty) {
2648 value = sourceData[name];
2649
2650 // If source property is specified, and value is different
2651 // copy field value in and build updatedFields
2652 if (value !== undefined && !me.isEqual(myData[name], value)) {
2653 myData[name] = value;
2654 modifiedFieldNames.push(name);
2655 }
2656 }
2657 }
2658
2659 // If this is a phantom record being updated from a concrete record, copy the ID in.
2660 if (me.phantom && !sourceRecord.phantom) {
2661 // beginEdit to prevent events firing
2662 // commit at the end to prevent dirty being set
2663 me.beginEdit();
2664 me.setId(sourceRecord.getId());
2665 me.endEdit(true);
2666 me.commit(true);
2667 }
2668 }
2669 return modifiedFieldNames;
2670 },
2671
2672 /**
2673 * Helper function used by afterEdit, afterReject and afterCommit. Calls the given
2674 * method on the `Ext.data.Store` that this instance has {@link #join joined}, if any.
2675 * The store function will always be called with the model instance as its single
2676 * argument. If this model is joined to a Ext.data.NodeStore, then this method calls
2677 * the given method on the NodeStore and the associated Ext.data.TreeStore.
2678 * @param {String} funcName The name function to call on each store.
2679 * @param {Array} [args] The arguments to pass to the method. This instance is
2680 * always inserted as the first argument.
2681 * @private
2682 */
2683 callJoined: function (funcName, args) {
2684 var me = this,
2685 joined = me.joined,
2686 session = me.session,
2687 i, len, fn, item;
2688
2689 if (!joined && !session) {
2690 return;
2691 }
2692
2693 if (args) {
2694 args.unshift(me);
2695 } else {
2696 args = [me];
2697 }
2698
2699 if (joined) {
2700 for (i = 0, len = joined.length; i < len; ++i) {
2701 item = joined[i];
2702 if (item && (fn = item[funcName])) {
2703 fn.apply(item, args);
2704 }
2705 }
2706 }
2707
2708 fn = session && session[funcName];
2709 if (fn) {
2710 fn.apply(session, args);
2711 }
2712 },
2713
2714 /**
2715 * Set the session for this record.
2716 * @param {Ext.data.Session} session The session
2717 */
2718 setSession: function(session) {
2719 //<debug>
2720 if (session) {
2721 if (this.session) {
2722 Ext.raise('This model already belongs to a session.');
2723 }
2724 if (!this.id) {
2725 Ext.raise('The model must have an id to participate in a session.');
2726 }
2727 }
2728 //</debug>
2729 this.session = session;
2730 if (session) {
2731 session.add(this);
2732 }
2733 },
2734
2735 /**
2736 * Gets the names of all the fields that were modified during an edit.
2737 * @param {Object} [old] The saved data from `beginEdit`.
2738 * @return {String[]} The array of modified field names.
2739 * @private
2740 */
2741 getModifiedFieldNames: function (old) {
2742 var me = this,
2743 data = me.data,
2744 modified = [],
2745 oldData = old || me.editMemento.data,
2746 key;
2747
2748 for (key in data) {
2749 if (data.hasOwnProperty(key)) {
2750 if (!me.isEqual(data[key], oldData[key], key)) {
2751 modified.push(key);
2752 }
2753 }
2754 }
2755
2756 return modified;
2757 },
2758
2759 /**
2760 * Checks if two values are equal, taking into account certain special factors, for
2761 * example dates.
2762 * @param {Object} lhs The first value.
2763 * @param {Object} rhs The second value.
2764 * @return {Boolean} True if the values are equal.
2765 * @private
2766 */
2767 isEqual: function (lhs, rhs, field) {
2768 var f;
2769
2770 if (field) {
2771 f = field.isField ? field : this.fieldsMap[field];
2772 if (f) {
2773 return f.isEqual(lhs, rhs);
2774 }
2775 }
2776
2777 // instanceof is ~10 times faster then Ext.isDate. Values here will not be
2778 // cross-document objects
2779 if (lhs instanceof Date && rhs instanceof Date) {
2780 return lhs.getTime() === rhs.getTime();
2781 }
2782 return lhs === rhs;
2783 },
2784
2785 statics: {
2786 /**
2787 * @property
2788 * @static
2789 * @private
2790 * @readonly
2791 * @deprecated
2792 * The update operation of type 'edit'. Used by {@link Ext.data.Store#event-update Store.update} event.
2793 */
2794 EDIT : 'edit',
2795 /**
2796 * @property
2797 * @static
2798 * @private
2799 * @readonly
2800 * @deprecated
2801 * The update operation of type 'reject'. Used by {@link Ext.data.Store#event-update Store.update} event.
2802 */
2803 REJECT : 'reject',
2804 /**
2805 * @property
2806 * @static
2807 * @private
2808 * @readonly
2809 * @deprecated
2810 * The update operation of type 'commit'. Used by {@link Ext.data.Store#event-update Store.update} event.
2811 */
2812 COMMIT : 'commit',
2813
2814 /**
2815 * @property {String/Object}
2816 * @static
2817 * @protected
2818 * The default proxy to use for instances of this Model when no proxy is configured
2819 * on the instance. When specified, the model will use this proxy instead of
2820 * requesting one from the {@link Ext.data.Session Session}.
2821 *
2822 * Can be a string "type", or a {@link Ext.data.proxy.Proxy Proxy} config object.
2823 *
2824 * This proxy is not inherited by subclasses.
2825 */
2826 defaultProxy: 'memory',
2827
2828 rankFields: function () {
2829 var cls = this,
2830 prototype = cls.prototype,
2831 fields = cls.fields,
2832 length = fields.length,
2833 rankedFields = [],
2834 criticalFields = [],
2835 transientFields = [],
2836 evilFields, field, i;
2837
2838 cls.rankedFields = prototype.rankedFields = rankedFields;
2839 cls.criticalFields = prototype.criticalFields = criticalFields;
2840 cls.transientFields = prototype.transientFields = transientFields;
2841
2842 // This first pass brings over any fields that have no dependencies at all
2843 // and gathers the evil fields to the side (the fields that could depend on
2844 // anything). This avoids the call to topoAdd that we must perform on all of
2845 // the fields that do have depends (which is good since most fields will be
2846 // handled here).
2847 for (i = 0; i < length; ++i) {
2848 field = fields[i];
2849 if (field.critical) {
2850 criticalFields.push(field);
2851 }
2852 if (!field.persist) {
2853 transientFields.push(field);
2854 }
2855 if (field.evil) {
2856 (evilFields || (evilFields = [])).push(field);
2857 } else if (!field.depends) {
2858 rankedFields.push(field);
2859 field.rank = rankedFields.length; // 1-based
2860 }
2861 }
2862
2863 for (i = 0; i < length; ++i) {
2864 if (!(field = fields[i]).rank && !field.evil) {
2865 cls.topoAdd(field);
2866 }
2867 }
2868
2869 if (evilFields) {
2870 for (i = 0, length = evilFields.length; i < length; ++i) {
2871 rankedFields.push(field = evilFields[i]);
2872 field.rank = rankedFields.length; // 1-based
2873 }
2874 }
2875
2876 //<debug>
2877 cls.topoStack = null; // cleanup diagnostic stack
2878 //</debug>
2879
2880 return rankedFields;
2881 },
2882
2883 topoAdd: function (field) {
2884 var cls = this,
2885 dep = field.depends,
2886 dependsLength = dep ? dep.length : 0,
2887 rankedFields = cls.rankedFields,
2888 i, targetField;
2889
2890 //<debug>
2891 var topoStack = cls.topoStack || (cls.topoStack = []);
2892 topoStack.push(field.name);
2893
2894 if (field.rank === 0) { // if (adding)
2895 Ext.raise(cls.$className + " has circular field dependencies: " +
2896 topoStack.join(" --> "));
2897 }
2898
2899 if (topoStack.length && field.evil) {
2900 Ext.raise(cls.$className + ": Field " +
2901 topoStack[topoStack.length - 1] +
2902 " cannot depend on depends-less field " + field.name);
2903 }
2904
2905 field.rank = 0; // adding (falsey but we can still detect cycles)
2906 //</debug>
2907
2908 for (i = 0; i < dependsLength; ++i) {
2909 // Get the targetField on which we depend and add this field to the
2910 // targetField.dependents[]
2911 targetField = cls.fieldsMap[dep[i]];
2912 //<debug>
2913 if (!targetField) {
2914 Ext.raise(cls.$className + ": Field " + field.name + " depends on undefined field " + dep[i]);
2915 }
2916 //</debug>
2917 (targetField.dependents || (targetField.dependents = [])).push(field);
2918
2919 if (!targetField.rank) { // if (!added)
2920 cls.topoAdd(targetField);
2921 }
2922 }
2923
2924 rankedFields.push(field);
2925 field.rank = rankedFields.length; // 1-based (truthy to track "added" state)
2926
2927 //<debug>
2928 topoStack.pop();
2929 //</debug>
2930 },
2931
2932 initFields: function(data, cls, proto) {
2933 var Field = Ext.data.field.Field,
2934 fieldDefs = data.fields,
2935 // allocate fields [] and ordinals {} for the new class:
2936 fields = [],
2937 fieldOrdinals = {},
2938 fieldsMap = {},
2939 references = [],
2940 superFields = proto.fields,
2941 versionProperty = data.versionProperty || proto.versionProperty,
2942 idProperty = cls.idProperty,
2943 idField, field, i, length, name, ordinal,
2944 reference, superIdField, superIdFieldName, idDeclared;
2945
2946 // Process any inherited fields to produce a fields [] and ordinals {} for
2947 // this class:
2948 cls.fields = proto.fields = fields;
2949 cls.fieldOrdinals = proto.fieldOrdinals = fieldOrdinals;
2950 cls.fieldsMap = proto.fieldsMap = fieldsMap;
2951 cls.references = proto.references = references;
2952
2953 if (superFields) {
2954 // We chain the super field so we can write to it
2955 for (i = 0, length = superFields.length; i < length; ++i) {
2956 fields[i] = field = Ext.Object.chain(superFields[i]);
2957
2958 field.dependents = null; // we need to recalculate these
2959 field.owner = cls;
2960 fieldOrdinals[name = field.name] = i;
2961 fieldsMap[name] = field;
2962 // Clear the rank because it needs to be set on the first pass through
2963 // the fields in the subclass, don't inherit it from the parent
2964 field.rank = null;
2965
2966 if (field.generated) {
2967 superIdField = field;
2968 superIdFieldName = field.name;
2969 }
2970 }
2971 }
2972
2973 // Merge in any fields from this class:
2974 if (fieldDefs) {
2975 delete data.fields;
2976 for (i = 0, length = fieldDefs.length; i < length; ++i) {
2977 field = fieldDefs[i];
2978 reference = field.reference;
2979 // Create a copy of the reference since we'll modify
2980 // the reference on the field. Needed for subclasses
2981 if (reference && typeof reference !== 'string') {
2982 // Can have child objects, so merge it deeply
2983 reference = Ext.merge({}, reference);
2984 }
2985 field.$reference = reference;
2986 field = Field.create(fieldDefs[i]);
2987 name = field.name;
2988 ordinal = fieldOrdinals[name];
2989 if (ordinal === undefined) {
2990 // If the field is new, add it to the end of the fields[]
2991 fieldOrdinals[name] = ordinal = fields.length;
2992 }
2993 // else, overwrite the field at the established ordinal
2994
2995 fieldsMap[name] = field;
2996 fields[ordinal] = field;
2997 field.definedBy = field.owner = cls;
2998 field.ordinal = ordinal;
2999 if (name === idProperty) {
3000 idDeclared = field;
3001 }
3002 }
3003 }
3004
3005 // Lookup the idProperty in the ordinals map and create a synthetic field if
3006 // we don't have one.
3007 idField = fieldsMap[idProperty];
3008 if (!idField) {
3009 if (superIdField && superIdField.generated) {
3010 ordinal = superIdField.ordinal;
3011 } else {
3012 ordinal = fields.length;
3013 }
3014 delete fieldsMap[superIdFieldName];
3015 delete fieldOrdinals[superIdFieldName]
3016 idField = new Field(idProperty);
3017 fields[ordinal] = idField;
3018 fieldOrdinals[idProperty] = ordinal;
3019 fieldsMap[idProperty] = idField;
3020 idField.definedBy = cls;
3021 idField.ordinal = ordinal;
3022 idField.generated = true;
3023 } else if (idDeclared && superIdField && superIdField.generated) {
3024 // If we're declaring the id as a field in our fields array and it's different to
3025 // the super id field that has been generated, pull it out and fix up the ordinals. This
3026 // likely won't happen often, to do it earlier we would need to know the contents of the fields
3027 // which would mean iterating over them twice.
3028 Ext.Array.remove(fields, superIdField);
3029 delete fieldsMap[superIdFieldName];
3030 delete fieldOrdinals[superIdFieldName];
3031 fieldsMap[idProperty] = idDeclared;
3032 for (i = 0, length = fields.length; i < length; ++i) {
3033 field = fields[i];
3034 fields.ordinal = i;
3035 fieldOrdinals[field.name] = i;
3036 }
3037 }
3038
3039 idField.allowNull = idField.critical = idField.identifier = true;
3040 idField.defaultValue = null;
3041
3042 cls.idField = proto.idField = idField;
3043
3044 if (versionProperty) {
3045 field = fieldsMap[versionProperty];
3046 if (!field) {
3047 ordinal = fields.length;
3048 field = new Field({
3049 name: versionProperty,
3050 type: 'int'
3051 });
3052 fields[ordinal] = field;
3053 fieldOrdinals[versionProperty] = ordinal;
3054 fieldsMap[versionProperty] = field;
3055 field.definedBy = cls;
3056 field.ordinal = ordinal;
3057 field.generated = true;
3058 }
3059 field.defaultValue = 1;
3060 field.critical = true;
3061 }
3062
3063 // NOTE: Be aware that the one fellow that manipulates these after this
3064 // point is Ext.data.NodeInterface.
3065 },
3066
3067 initValidators: function(data, cls, proto) {
3068 var superValidators = proto.validators,
3069 validators, field, copy, validatorDefs,
3070 i, length, fieldValidator, name, validator, item;
3071
3072 if (superValidators) {
3073 validators = {};
3074 for (field in superValidators) {
3075 validators[field] = Ext.Array.clone(superValidators[field]);
3076 }
3077 }
3078
3079 validatorDefs = data.validators || data.validations;
3080 //<debug>
3081 if (data.validations) {
3082 delete data.validations;
3083 Ext.log.warn((cls.$className || 'Ext.data.Model' ) +
3084 ': validations has been deprecated. Please use validators instead.');
3085 }
3086 //</debug>
3087 if (validatorDefs) {
3088 delete data.validators;
3089
3090 validators = validators || {};
3091
3092 // Support older array syntax
3093 if (Ext.isArray(validatorDefs)) {
3094 copy = {};
3095 for (i = 0, length = validatorDefs.length; i < length; ++i) {
3096 item = validatorDefs[i];
3097 name = item.field;
3098 if (!copy[name]) {
3099 copy[name] = [];
3100 }
3101 // Check for function form
3102 item = item.fn || item;
3103 copy[name].push(item);
3104 }
3105 validatorDefs = copy;
3106 }
3107
3108 for (name in validatorDefs) {
3109 fieldValidator = validatorDefs[name];
3110 if (!Ext.isArray(fieldValidator)) {
3111 fieldValidator = [fieldValidator];
3112 }
3113
3114 validator = validators[name];
3115 if (validators[name]) {
3116 // Declared in super
3117 Ext.Array.push(validator, fieldValidator);
3118 } else {
3119 validators[name] = fieldValidator;
3120 }
3121 }
3122 }
3123 if (validators) {
3124 for (name in validators) {
3125 field = cls.getField(name);
3126 if (field) {
3127 field.setModelValidators(validators[name]);
3128 }
3129 }
3130 }
3131 cls.validators = proto.validators = validators;
3132 },
3133
3134 initAssociations: function (schema, data, cls) {
3135 // The 4 legacy associations:
3136 var associations = data.associations,
3137 belongsTo = data.belongsTo,
3138 hasMany = data.hasMany,
3139 hasOne = data.hasOne,
3140 // The new one:
3141 matrices = data.manyToMany,
3142 i, length, assoc;
3143
3144 //<debug>
3145 if (data.belongsTo) {
3146 Ext.log.warn('Use of "belongsTo" is obsolete' +
3147 (cls.$className ? ' in ' + cls.$className : ''));
3148 delete data.belongsTo;
3149 }
3150 //</debug>
3151
3152 delete data.manyToMany;
3153 if (matrices) {
3154 schema.addMatrices(cls, matrices);
3155 }
3156
3157 // Legacy:
3158 delete data.associations;
3159 delete data.belongsTo;
3160 delete data.hasMany;
3161 delete data.hasOne;
3162
3163 if (associations) {
3164 associations = Ext.isArray(associations) ? associations : [ associations ];
3165 for (i = 0, length = associations.length; i < length; ++i) {
3166 assoc = associations[i];
3167 switch (assoc.type) {
3168 case 'belongsTo':
3169 schema.addLegacyBelongsTo(cls, assoc);
3170 break;
3171 case 'hasMany':
3172 schema.addLegacyHasMany(cls, assoc);
3173 break;
3174 case 'hasOne':
3175 schema.addLegacyHasOne(cls, assoc);
3176 break;
3177
3178 //<debug>
3179 default:
3180 Ext.raise('Invalid association type: "' + assoc.type + '"');
3181 //</debug>
3182 }
3183 }
3184 }
3185
3186 if (belongsTo) {
3187 belongsTo = Ext.isArray(belongsTo) ? belongsTo : [ belongsTo ];
3188 for (i = 0, length = belongsTo.length; i < length; ++i) {
3189 schema.addLegacyBelongsTo(cls, belongsTo[i]);
3190 }
3191 }
3192
3193 if (hasMany) {
3194 hasMany = Ext.isArray(hasMany) ? hasMany : [ hasMany ];
3195 for (i = 0, length = hasMany.length; i < length; ++i) {
3196 schema.addLegacyHasMany(cls, hasMany[i]);
3197 }
3198 }
3199
3200 if (hasOne) {
3201 hasOne = Ext.isArray(hasOne) ? hasOne : [ hasOne ];
3202 for (i = 0, length = hasOne.length; i < length; ++i) {
3203 schema.addLegacyHasOne(cls, hasOne[i]);
3204 }
3205 }
3206 schema.afterLegacyAssociations(cls);
3207 },
3208
3209 initIdentifier: function (data, cls, proto) {
3210 var identifier = data.identifier || data.idgen,
3211 superIdent = proto.identifier || cls.schema._defaultIdentifier,
3212 generatorPrefix;
3213
3214 //<debug>
3215 if (data.idgen) {
3216 Ext.log.warn('Ext.data.Model: idgen has been deprecated. Please use identifier instead.');
3217 }
3218 //</debug>
3219
3220 if (identifier) {
3221 delete data.identifier;
3222 delete data.idgen;
3223
3224 // An idgen was specified on the definition, use it explicitly.
3225 identifier = Ext.Factory.dataIdentifier(identifier);
3226 } else if (superIdent) {
3227 // If we have a cloneable instance, and we don't have an id
3228 // clone it. If we have an id, then we should use the same
3229 // instance since it's the same as looking it up via id.
3230 if (superIdent.clone && !superIdent.getId()) {
3231 identifier = superIdent.clone();
3232 } else if (superIdent.isGenerator) {
3233 identifier = superIdent;
3234 } else {
3235 identifier = Ext.Factory.dataIdentifier(superIdent);
3236 }
3237 }
3238
3239 cls.identifier = proto.identifier = identifier;
3240
3241 if (!identifier) {
3242 // If we didn't find one, create it and push it onto the class.
3243 // Don't put it on the prototype, so a subclass will create
3244 // it's own generator. If we have an anonymous model, go ahead and
3245 // generate a unique prefix for it.
3246 generatorPrefix = cls.entityName;
3247 if (!generatorPrefix) {
3248 generatorPrefix = Ext.id(null, 'extModel');
3249 }
3250 cls.identifier = Ext.Factory.dataIdentifier({
3251 type: 'sequential',
3252 prefix: generatorPrefix + '-'
3253 });
3254 }
3255 },
3256
3257 findValidator: function(validators, name, cfg) {
3258 var type = cfg.type || cfg,
3259 field = validators[name],
3260 len, i, item;
3261
3262 if (field) {
3263 for (i = 0, len = field.length; i < len; ++i) {
3264 item = field[i];
3265 if (item.type === type) {
3266 return item;
3267 }
3268 }
3269 }
3270 return null;
3271 },
3272
3273 /**
3274 * This method produces the `initializeFn` for this class. If there are no fields
3275 * requiring {@link Ext.data.field.Field#cfg-convert conversion} and no fields requiring
3276 * a {@link Ext.data.field.Field#defaultValue default value} then this method will
3277 * return `null`.
3278 * @return {Function} The `initializeFn` for this class (or null).
3279 * @private
3280 */
3281 makeInitializeFn: function (cls) {
3282 var code = ['var '],
3283 body = ['\nreturn function (e) {\n var data = e.data, v;\n'],
3284 fieldVars = [],
3285 work = 0,
3286 bc, ec, // == beginClone, endClone
3287 convert, expr, factory, field, fields, fs, hasDefValue, i, length;
3288
3289 if (!(fields = cls.rankedFields)) {
3290 // On the first edit of a record of this type we need to ensure we have the
3291 // topo-sort done:
3292 fields = cls.rankFields();
3293 }
3294
3295 for (i = 0, length = fields.length; i < length; ++i) {
3296 // The generated method declares vars for each field using "f0".."fN' as the
3297 // name. These are used to access properties of the field (e.g., the convert
3298 // method or defaultValue).
3299 field = fields[i];
3300 fieldVars[i] = fs = 'f' + i;
3301 convert = field.convert;
3302
3303 if (i) {
3304 code.push(', \n ');
3305 }
3306 code.push(fs, ' = $fields[' + i + ']');
3307 //<debug>
3308 // this can be helpful when debugging (at least in Chrome):
3309 code.push(' /* ', field.name, ' */');
3310 //</debug>
3311
3312 // NOTE: added string literals are "folded" by the compiler so we
3313 // are better off doing an "'foo' + 'bar'" then "'foo', 'bar'". But
3314 // for variables we are better off pushing them into the array for
3315 // the final join.
3316
3317 if ((hasDefValue = (field.defaultValue !== undefined)) || convert) {
3318 // For non-calculated fields that have some work required (a convert method
3319 // and/or defaultValue), generate a chunk of logic appropriate for the
3320 // field.
3321 //expr = data["fieldName"];
3322 expr = 'data["' + field.name + '"]';
3323 ++work;
3324
3325 bc = ec = '';
3326 if (field.cloneDefaultValue) {
3327 bc = 'Ext.clone(';
3328 ec = ')';
3329 }
3330
3331 body.push('\n');
3332 if (convert && hasDefValue) {
3333 // v = data.fieldName;
3334 // if (v !== undefined) {
3335 // v = f2.convert(v, e);
3336 // }
3337 // if (v === undefined) {
3338 // v = f2.defaultValue;
3339 // // or
3340 // v = Ext.clone(f2.defaultValue);
3341 // }
3342 // data.fieldName = v;
3343 //
3344 body.push(' v = ', expr, ';\n' +
3345 ' if (v !== undefined) {\n' +
3346 ' v = ', fs, '.convert(v, e);\n' +
3347 ' }\n' +
3348 ' if (v === undefined) {\n' +
3349 ' v = ', bc, fs, '.defaultValue',ec,';\n' +
3350 ' }\n' +
3351 ' ', expr, ' = v;');
3352 } else if (convert) { // no defaultValue
3353 // v = f2.convert(data.fieldName,e);
3354 // if (v !== undefined) {
3355 // data.fieldName = v;
3356 // }
3357 //
3358 body.push(' v = ', fs, '.convert(', expr, ',e);\n' +
3359 ' if (v !== undefined) {\n' +
3360 ' ', expr, ' = v;\n' +
3361 ' }\n');
3362 } else if (hasDefValue) { // no convert
3363 // if (data.fieldName === undefined) {
3364 // data.fieldName = f2.defaultValue;
3365 // // or
3366 // data.fieldName = Ext.clone(f2.defaultValue);
3367 // }
3368 //
3369 body.push(' if (', expr, ' === undefined) {\n' +
3370 ' ', expr, ' = ',bc,fs,'.defaultValue',ec,';\n' +
3371 ' }\n');
3372 }
3373 }
3374 }
3375
3376 if (!work) {
3377 // There are no fields that need special processing
3378 return Ext.emptyFn;
3379 }
3380
3381 code.push(';\n');
3382 code.push.apply(code, body);
3383 code.push('}');
3384 code = code.join('');
3385
3386 // Ensure that Ext in the function code refers to the same Ext that we are using here.
3387 // If we are in a sandbox, global.Ext might be different.
3388 factory = new Function('$fields', 'Ext', code);
3389
3390 return factory(fields, Ext);
3391 }
3392 } // static
3393 } // privates
3394 },
3395 function () {
3396 var Model = this,
3397 proto = Model.prototype,
3398 Schema = Ext.data.schema.Schema,
3399 defaultSchema;
3400
3401 Model.proxyConfig = proto.proxy;
3402 delete proto.proxy;
3403
3404 // Base Model class may be used. It needs an empty fields array.
3405 Model.fields = [];
3406
3407 // Base Model class may be used. It needs an empty fieldsMap hash.
3408 Model.fieldsMap = proto.fieldsMap = {};
3409
3410 Model.schema = proto.schema = Schema.get(proto.schema);
3411 proto.idField = new Ext.data.field.Field(proto.idProperty);
3412 Model.identifier = new Ext.data.identifier.Sequential();
3413
3414 Model.onExtended(function (cls, data) {
3415 var proto = cls.prototype,
3416 schemaName = data.schema,
3417 superCls = proto.superclass.self,
3418 schema, entityName, proxy;
3419
3420 cls.idProperty = data.idProperty || proto.idProperty;
3421
3422 if (schemaName) {
3423 delete data.schema;
3424 schema = Schema.get(schemaName);
3425 } else if (!(schema = proto.schema)) {
3426 schema = defaultSchema || (defaultSchema = Schema.get('default'));
3427 }
3428
3429 // These are in "privates" so we manually make them inherited:
3430 cls.rankFields = Model.rankFields;
3431 cls.topoAdd = Model.topoAdd;
3432
3433 // if we picked up a schema from cls.prototype.schema, it is because it was found
3434 // in the prototype chain on a base class.
3435 proto.schema = cls.schema = schema;
3436
3437 // Unless specified on the declaration data, we need to provide the entityName of
3438 // the new Entity-derived class. Store it on the prototype and the class.
3439 if (!(entityName = data.entityName)) {
3440 proto.entityName = entityName = schema.getEntityName(cls);
3441 //<debug>
3442 if (!entityName) {
3443 if (data.associations) {
3444 Ext.raise('Anonymous entities cannot specify "associations"');
3445 }
3446 if (data.belongsTo) {
3447 Ext.raise('Anonymous entities cannot specify "belongsTo"');
3448 }
3449 if (data.hasMany) {
3450 Ext.raise('Anonymous entities cannot specify "hasMany"');
3451 }
3452 if (data.hasOne) {
3453 Ext.raise('Anonymous entities cannot specify "hasOne"');
3454 }
3455 if (data.matrices) {
3456 Ext.raise('Anonymous entities cannot specify "manyToMany"');
3457 }
3458 }
3459 //</debug>
3460 }
3461 cls.entityName = entityName;
3462 cls.fieldExtractors = {};
3463
3464 Model.initIdentifier(data, cls, proto);
3465 Model.initFields(data, cls, proto);
3466 Model.initValidators(data, cls, proto);
3467
3468 // This is a compat hack to allow "rec.fields.items" to work as it used to when
3469 // fields was a MixedCollection
3470 cls.fields.items = cls.fields;
3471
3472 if (entityName) {
3473 schema.addEntity(cls);
3474 Model.initAssociations(schema, data, cls);
3475 }
3476
3477 proxy = data.proxy;
3478 if (proxy) {
3479 delete data.proxy;
3480 } else if (superCls !== Model) {
3481 proxy = superCls.proxyConfig || superCls.proxy;
3482 }
3483
3484 cls.proxyConfig = proxy;
3485 });
3486 });