9d5e2cba7e322cfeac00ce0ef585086058c1215e
2 var CRM
= (window
.CRM
) ? (window
.CRM
) : (window
.CRM
= {});
3 if (!CRM
.Backbone
) CRM
.Backbone
= {};
6 * Backbone.sync provider which uses CRM.api() for I/O.
7 * To support CRUD operations, model classes must be defined with a "crmEntityName" property.
8 * To load collections using API queries, set the "crmCriteria" property or override the
9 * method "toCrmCriteria".
14 * @see tests/qunit/crm-backbone
16 CRM
.Backbone
.sync = function(method
, model
, options
) {
17 var isCollection
= _
.isArray(model
.models
);
21 success: function(data
) {
23 options
.success(_
.toArray(data
.values
));
25 error: function(data
) {
26 // CRM.api displays errors by default, but Backbone.sync
27 // protocol requires us to override "error". This restores
28 // the default behavior.
29 $().crmError(data
.error_message
, ts('Error'));
35 CRM
.api(model
.crmEntityName
, 'get', model
.toCrmCriteria(), apiOptions
);
38 apiOptions
.error({is_error
: 1, error_message
: "CRM.Backbone.sync(" + method
+ ") not implemented for collections"});
42 // callback options to pass to CRM.api
44 success: function(data
) {
46 var values
= _
.toArray(data
['values']);
47 if (data
.count
== 1) {
48 options
.success(values
[0]);
51 data
.error_message
= ts("Expected exactly one response");
52 apiOptions
.error(data
);
55 error: function(data
) {
56 // CRM.api displays errors by default, but Backbone.sync
57 // protocol requires us to override "error". This restores
58 // the default behavior.
59 $().crmError(data
.error_message
, ts('Error'));
64 case 'create': // pass-through
66 CRM
.api(model
.crmEntityName
, 'create', model
.toJSON(), apiOptions
);
70 var apiAction
= (method
== 'delete') ? 'delete' : 'get';
71 var params
= model
.toCrmCriteria();
73 apiOptions
.error({is_error
: 1, error_message
: 'Missing ID for ' + model
.crmEntityName
});
76 CRM
.api(model
.crmEntityName
, apiAction
, params
, apiOptions
);
79 apiOptions
.error({is_error
: 1, error_message
: "CRM.Backbone.sync(" + method
+ ") not implemented for models"});
85 * Connect a "model" class to CiviCRM's APIv3
89 * var ContactModel = Backbone.Model.extend({});
90 * CRM.Backbone.extendModel(ContactModel, "Contact");
93 * c = new ContactModel({id: 3});
97 * @param Class ModelClass
98 * @param string crmEntityName APIv3 entity name, such as "Contact" or "CustomField"
99 * @see tests/qunit/crm-backbone
101 CRM
.Backbone
.extendModel = function(ModelClass
, crmEntityName
) {
102 // Defaults - if specified in ModelClass, preserve
103 _
.defaults(ModelClass
.prototype, {
104 crmEntityName
: crmEntityName
,
105 toCrmCriteria: function() {
106 return (this.get('id')) ? {id
: this.get('id')} : {};
109 // Overrides - if specified in ModelClass, replace
110 _
.extend(ModelClass
.prototype, {
111 sync
: CRM
.Backbone
.sync
116 * Configure a model class to track whether a model has unsaved changes.
118 * The ModelClass will be extended with:
119 * - Method: isSaved() - true if there have been no changes to the data since the last fetch or save
120 * - Event: saved(object model, bool is_saved) - triggered whenever isSaved() value would change
122 * Note: You should not directly call isSaved() within the context of the success/error/sync callback;
123 * I haven't found a way to make isSaved() behave correctly within these callbacks without patching
124 * Backbone. Instead, attach an event listener to the 'saved' event.
128 CRM
.Backbone
.trackSaved = function(ModelClass
) {
129 // Retain references to some of the original class's functions
130 var Parent
= _
.pick(ModelClass
.prototype, 'initialize', 'save', 'fetch');
133 var onSyncSuccess = function() {
134 this._modified
= false;
135 if (this._oldModified
.length
> 0) {
136 this._oldModified
.pop();
138 this.trigger('saved', this, this.isSaved());
140 var onSaveError = function() {
141 if (this._oldModified
.length
> 0) {
142 this._modified
= this._oldModified
.pop();
143 this.trigger('saved', this, this.isSaved());
147 // Defaults - if specified in ModelClass, preserve
148 _
.defaults(ModelClass
.prototype, {
149 isSaved: function() {
150 var result
= !this.isNew() && !this._modified
;
153 _saved_onchange: function(model
, options
) {
154 if (options
.parse
) return;
157 setModified: function() {
158 var oldModified
= this._modified
;
159 this._modified
= true;
161 this.trigger('saved', this, this.isSaved());
166 // Overrides - if specified in ModelClass, replace
167 _
.extend(ModelClass
.prototype, {
168 initialize: function(options
) {
169 this._modified
= false;
170 this._oldModified
= [];
171 this.listenTo(this, 'change', this._saved_onchange
);
172 this.listenTo(this, 'error', onSaveError
);
173 this.listenTo(this, 'sync', onSyncSuccess
);
174 if (Parent
.initialize
) {
175 return Parent
.initialize
.apply(this, arguments
);
179 // we'll assume success
180 this._oldModified
.push(this._modified
);
181 return Parent
.save
.apply(this, arguments
);
184 this._oldModified
.push(this._modified
);
185 return Parent
.fetch
.apply(this, arguments
);
191 * Configure a model class to support client-side soft deletion.
192 * One can call "model.setDeleted(BOOLEAN)" to flag an entity for
193 * deletion (or not) -- however, deletion will be deferred until save()
197 * softDelete: function(model, is_deleted) -- change value of is_deleted
201 CRM
.Backbone
.trackSoftDelete = function(ModelClass
) {
202 // Retain references to some of the original class's functions
203 var Parent
= _
.pick(ModelClass
.prototype, 'save');
205 // Defaults - if specified in ModelClass, preserve
206 _
.defaults(ModelClass
.prototype, {
207 is_soft_deleted
: false,
208 setSoftDeleted: function(is_deleted
) {
209 if (this.is_soft_deleted
!= is_deleted
) {
210 this.is_soft_deleted
= is_deleted
;
211 this.trigger('softDelete', this, is_deleted
);
212 if (this.setModified
) this.setModified(); // FIXME: ugly interaction, trackSoftDelete-trackSaved
215 isSoftDeleted: function() {
216 return this.is_soft_deleted
;
220 // Overrides - if specified in ModelClass, replace
221 _
.extend(ModelClass
.prototype, {
222 save: function(attributes
, options
) {
223 if (this.isSoftDeleted()) {
224 return this.destroy(options
);
226 return Parent
.save
.apply(this, arguments
);
233 * Connect a "collection" class to CiviCRM's APIv3
235 * Note: the collection supports a special property, crmCriteria, which is an array of
236 * query options to send to the API
240 * var ContactModel = Backbone.Model.extend({});
241 * CRM.Backbone.extendModel(ContactModel, "Contact");
242 * var ContactCollection = Backbone.Collection.extend({
243 * model: ContactModel
245 * CRM.Backbone.extendCollection(ContactCollection);
248 * var c = new ContactCollection([], {
249 * crmCriteria: {contact_type: 'Organization'}
254 * @param Class CollectionClass
255 * @see tests/qunit/crm-backbone
257 CRM
.Backbone
.extendCollection = function(CollectionClass
) {
258 var origInit
= CollectionClass
.prototype.initialize
;
259 // Defaults - if specified in CollectionClass, preserve
260 _
.defaults(CollectionClass
.prototype, {
261 crmEntityName
: CollectionClass
.prototype.model
.prototype.crmEntityName
,
262 toCrmCriteria: function() {
263 return this.crmCriteria
|| {};
266 // Overrides - if specified in CollectionClass, replace
267 _
.extend(CollectionClass
.prototype, {
268 sync
: CRM
.Backbone
.sync
,
269 initialize: function(models
, options
) {
270 options
|| (options
= {});
271 if (options
.crmCriteria
) {
272 this.crmCriteria
= options
.crmCriteria
;
275 return origInit
.apply(this, arguments
);
282 * Find a single record, or create a new record.
284 * @param Object options:
285 * - CollectionClass: class
286 * - crmCriteria: Object values to search/default on
287 * - defaults: Object values to put on newly created model (if needed)
288 * - success: function(model)
289 * - error: function(collection, error)
291 CRM
.Backbone
.findCreate = function(options
) {
292 options
|| (options
= {});
293 var collection
= new options
.CollectionClass([], {
294 crmCriteria
: options
.crmCriteria
297 success: function(collection
) {
298 if (collection
.length
== 0) {
299 var attrs
= _
.extend({}, collection
.crmCriteria
, options
.defaults
|| {});
300 var model
= collection
._prepareModel(attrs
, options
);
301 options
.success(model
);
302 } else if (collection
.length
== 1) {
303 options
.success(collection
.first());
305 options
.error(collection
, {
307 error_message
: 'Too many matches'
311 error: function(collection
, errorData
) {
313 options
.error(collection
, errorData
);
320 CRM
.Backbone
.Model
= Backbone
.Model
.extend({
322 * Return JSON version of model -- but only include fields that are
323 * listed in the 'schema'.
327 toStrictJSON: function() {
328 var schema
= this.schema
;
329 var result
= this.toJSON();
330 _
.each(result
, function(value
, key
) {
337 setRel: function(key
, value
, options
) {
338 this.rels
= this.rels
|| {};
339 if (this.rels
[key
] != value
) {
340 this.rels
[key
] = value
;
341 this.trigger("rel:" + key
, value
);
344 getRel: function(key
) {
345 return this.rels
? this.rels
[key
] : null;
349 CRM
.Backbone
.Collection
= Backbone
.Collection
.extend({
351 * Store 'key' on this.rel and automatically copy it to
356 * @param initialModels
358 initializeCopyToChildrenRelation: function(key
, value
, initialModels
) {
359 this.setRel(key
, value
, {silent
: true});
360 this.on('reset', this._copyToChildren
, this);
361 this.on('add', this._copyToChild
, this);
363 _copyToChildren: function() {
364 var collection
= this;
365 collection
.each(function(model
) {
366 collection
._copyToChild(model
);
369 _copyToChild: function(model
) {
370 _
.each(this.rels
, function(relValue
, relKey
) {
371 model
.setRel(relKey
, relValue
, {silent
: true});
374 setRel: function(key
, value
, options
) {
375 this.rels
= this.rels
|| {};
376 if (this.rels
[key
] != value
) {
377 this.rels
[key
] = value
;
378 this.trigger("rel:" + key
, value
);
381 getRel: function(key
) {
382 return this.rels
? this.rels
[key
] : null;
387 CRM.Backbone.Form = Backbone.Form.extend({
388 validate: function() {
389 // Add support for form-level validators
390 var errors = Backbone.Form.prototype.validate.apply(this, []) || {};
392 if (this.validators) {
393 _.each(this.validators, function(validator) {
394 var modelErrors = validator(this.getValue());
396 // The following if() has been copied-pasted from the parent's
397 // handling of model-validators. They are similar in that the errors are
398 // probably keyed by field names... but not necessarily, so we use _others
401 var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
403 //If errors are not in object form then just store on the error object
405 errors._others = errors._others || [];
406 errors._others.push(modelErrors);
409 //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
411 _.each(modelErrors, function(val, key) {
412 //Set error on field if there isn't one already
413 if (self.fields[key] && !errors[key]) {
414 self.fields[key].setError(val);
419 //Otherwise add to '_others' key
420 errors._others = errors._others || [];
423 errors._others.push(tmpErr);
431 return _.isEmpty(errors) ? null : errors;