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 * Connect a "collection" class to CiviCRM's APIv3
118 * Note: the collection supports a special property, crmCriteria, which is an array of
119 * query options to send to the API
123 * var ContactModel = Backbone.Model.extend({});
124 * CRM.Backbone.extendModel(ContactModel, "Contact");
125 * var ContactCollection = Backbone.Collection.extend({
126 * model: ContactModel
128 * CRM.Backbone.extendCollection(ContactCollection);
131 * var c = new ContactCollection([], {
132 * crmCriteria: {contact_type: 'Organization'}
137 * @param Class CollectionClass
138 * @see tests/qunit/crm-backbone
140 CRM
.Backbone
.extendCollection = function(CollectionClass
) {
141 var origInit
= CollectionClass
.prototype.initialize
;
142 // Defaults - if specified in CollectionClass, preserve
143 _
.defaults(CollectionClass
.prototype, {
144 crmEntityName
: CollectionClass
.prototype.model
.prototype.crmEntityName
,
145 toCrmCriteria: function() {
146 return this.crmCriteria
|| {};
149 // Overrides - if specified in CollectionClass, replace
150 _
.extend(CollectionClass
.prototype, {
151 sync
: CRM
.Backbone
.sync
,
152 initialize: function(models
, options
) {
153 options
|| (options
= {});
154 if (options
.crmCriteria
) {
155 this.crmCriteria
= options
.crmCriteria
;
158 return origInit
.apply(this, arguments
);
165 * Find a single record, or create a new record.
167 * @param Object options:
168 * - CollectionClass: class
169 * - crmCriteria: Object values to search/default on
170 * - defaults: Object values to put on newly created model (if needed)
171 * - success: function(model)
172 * - error: function(collection, error)
174 CRM
.Backbone
.findCreate = function(options
) {
175 options
|| (options
= {});
176 var collection
= new options
.CollectionClass([], {
177 crmCriteria
: options
.crmCriteria
180 success: function(collection
) {
181 if (collection
.length
== 0) {
182 var attrs
= _
.extend({}, collection
.crmCriteria
, options
.defaults
|| {});
183 var model
= collection
._prepareModel(attrs
, options
);
184 options
.success(model
);
185 } else if (collection
.length
== 1) {
186 options
.success(collection
.first());
188 options
.error(collection
, {
190 error_message
: 'Too many matches'
194 error: function(collection
, errorData
) {
196 options
.error(collection
, errorData
);
203 CRM
.Backbone
.Model
= Backbone
.Model
.extend({
205 * Return JSON version of model -- but only include fields that are
206 * listed in the 'schema'.
210 toStrictJSON: function() {
211 var schema
= this.schema
;
212 var result
= this.toJSON();
213 _
.each(result
, function(value
, key
) {
220 setRel: function(key
, value
, options
) {
221 this.rels
= this.rels
|| {};
222 if (this.rels
[key
] != value
) {
223 this.rels
[key
] = value
;
224 this.trigger("rel:" + key
, value
);
227 getRel: function(key
) {
228 return this.rels
? this.rels
[key
] : null;
232 CRM
.Backbone
.Collection
= Backbone
.Collection
.extend({
234 * Store 'key' on this.rel and automatically copy it to
239 * @param initialModels
241 initializeCopyToChildrenRelation: function(key
, value
, initialModels
) {
242 this.setRel(key
, value
, {silent
: true});
243 this.on('reset', this._copyToChildren
, this);
244 this.on('add', this._copyToChild
, this);
246 _copyToChildren: function() {
247 var collection
= this;
248 collection
.each(function(model
) {
249 collection
._copyToChild(model
);
252 _copyToChild: function(model
) {
253 _
.each(this.rels
, function(relValue
, relKey
) {
254 model
.setRel(relKey
, relValue
, {silent
: true});
257 setRel: function(key
, value
, options
) {
258 this.rels
= this.rels
|| {};
259 if (this.rels
[key
] != value
) {
260 this.rels
[key
] = value
;
261 this.trigger("rel:" + key
, value
);
264 getRel: function(key
) {
265 return this.rels
? this.rels
[key
] : null;
270 CRM.Backbone.Form = Backbone.Form.extend({
271 validate: function() {
272 // Add support for form-level validators
273 var errors = Backbone.Form.prototype.validate.apply(this, []) || {};
275 if (this.validators) {
276 _.each(this.validators, function(validator) {
277 var modelErrors = validator(this.getValue());
279 // The following if() has been copied-pasted from the parent's
280 // handling of model-validators. They are similar in that the errors are
281 // probably keyed by field names... but not necessarily, so we use _others
284 var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
286 //If errors are not in object form then just store on the error object
288 errors._others = errors._others || [];
289 errors._others.push(modelErrors);
292 //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
294 _.each(modelErrors, function(val, key) {
295 //Set error on field if there isn't one already
296 if (self.fields[key] && !errors[key]) {
297 self.fields[key].setError(val);
302 //Otherwise add to '_others' key
303 errors._others = errors._others || [];
306 errors._others.push(tmpErr);
314 return _.isEmpty(errors) ? null : errors;