Merge pull request #1222 from lcdservices/CRM-13099
[civicrm-core.git] / js / crm.backbone.js
1 (function($) {
2 var CRM = (window.CRM) ? (window.CRM) : (window.CRM = {});
3 if (!CRM.Backbone) CRM.Backbone = {};
4
5 /**
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".
10 *
11 * @param method
12 * @param model
13 * @param options
14 * @see tests/qunit/crm-backbone
15 */
16 CRM.Backbone.sync = function(method, model, options) {
17 var isCollection = _.isArray(model.models);
18
19 if (isCollection) {
20 var apiOptions = {
21 success: function(data) {
22 // unwrap data
23 options.success(_.toArray(data.values));
24 },
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'));
30 options.error(data);
31 }
32 };
33 switch (method) {
34 case 'read':
35 CRM.api(model.crmEntityName, 'get', model.toCrmCriteria(), apiOptions);
36 break;
37 default:
38 apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for collections"});
39 break;
40 }
41 } else {
42 // callback options to pass to CRM.api
43 var apiOptions = {
44 success: function(data) {
45 // unwrap data
46 var values = _.toArray(data['values']);
47 if (data.count == 1) {
48 options.success(values[0]);
49 } else {
50 data.is_error = 1;
51 data.error_message = ts("Expected exactly one response");
52 apiOptions.error(data);
53 }
54 },
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'));
60 options.error(data);
61 }
62 };
63 switch (method) {
64 case 'create': // pass-through
65 case 'update':
66 CRM.api(model.crmEntityName, 'create', model.toJSON(), apiOptions);
67 break;
68 case 'read':
69 case 'delete':
70 var apiAction = (method == 'delete') ? 'delete' : 'get';
71 var params = model.toCrmCriteria();
72 if (!params.id) {
73 apiOptions.error({is_error: 1, error_message: 'Missing ID for ' + model.crmEntityName});
74 return;
75 }
76 CRM.api(model.crmEntityName, apiAction, params, apiOptions);
77 break;
78 default:
79 apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for models"});
80 }
81 }
82 };
83
84 /**
85 * Connect a "model" class to CiviCRM's APIv3
86 *
87 * @code
88 * // Setup class
89 * var ContactModel = Backbone.Model.extend({});
90 * CRM.Backbone.extendModel(ContactModel, "Contact");
91 *
92 * // Use class
93 * c = new ContactModel({id: 3});
94 * c.fetch();
95 * @endcode
96 *
97 * @param Class ModelClass
98 * @param string crmEntityName APIv3 entity name, such as "Contact" or "CustomField"
99 * @see tests/qunit/crm-backbone
100 */
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')} : {};
107 }
108 });
109 // Overrides - if specified in ModelClass, replace
110 _.extend(ModelClass.prototype, {
111 sync: CRM.Backbone.sync
112 });
113 };
114
115 /**
116 * Connect a "collection" class to CiviCRM's APIv3
117 *
118 * Note: the collection supports a special property, crmCriteria, which is an array of
119 * query options to send to the API
120 *
121 * @code
122 * // Setup class
123 * var ContactModel = Backbone.Model.extend({});
124 * CRM.Backbone.extendModel(ContactModel, "Contact");
125 * var ContactCollection = Backbone.Collection.extend({
126 * model: ContactModel
127 * });
128 * CRM.Backbone.extendCollection(ContactCollection);
129 *
130 * // Use class
131 * var c = new ContactCollection([], {
132 * crmCriteria: {contact_type: 'Organization'}
133 * });
134 * c.fetch();
135 * @endcode
136 *
137 * @param Class CollectionClass
138 * @see tests/qunit/crm-backbone
139 */
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 || {};
147 }
148 });
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;
156 }
157 if (origInit) {
158 return origInit.apply(this, arguments);
159 }
160 }
161 });
162 };
163
164 /**
165 * Find a single record, or create a new record.
166 *
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)
173 */
174 CRM.Backbone.findCreate = function(options) {
175 options || (options = {});
176 var collection = new options.CollectionClass([], {
177 crmCriteria: options.crmCriteria
178 });
179 collection.fetch({
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());
187 } else {
188 options.error(collection, {
189 is_error: 1,
190 error_message: 'Too many matches'
191 });
192 }
193 },
194 error: function(collection, errorData) {
195 if (options.error) {
196 options.error(collection, errorData);
197 }
198 }
199 });
200 };
201
202
203 CRM.Backbone.Model = Backbone.Model.extend({
204 /**
205 * Return JSON version of model -- but only include fields that are
206 * listed in the 'schema'.
207 *
208 * @return {*}
209 */
210 toStrictJSON: function() {
211 var schema = this.schema;
212 var result = this.toJSON();
213 _.each(result, function(value, key) {
214 if (!schema[key]) {
215 delete result[key];
216 }
217 });
218 return result;
219 },
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);
225 }
226 },
227 getRel: function(key) {
228 return this.rels ? this.rels[key] : null;
229 }
230 });
231
232 CRM.Backbone.Collection = Backbone.Collection.extend({
233 /**
234 * Store 'key' on this.rel and automatically copy it to
235 * any children.
236 *
237 * @param key
238 * @param value
239 * @param initialModels
240 */
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);
245 },
246 _copyToChildren: function() {
247 var collection = this;
248 collection.each(function(model) {
249 collection._copyToChild(model);
250 });
251 },
252 _copyToChild: function(model) {
253 _.each(this.rels, function(relValue, relKey) {
254 model.setRel(relKey, relValue, {silent: true});
255 });
256 },
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);
262 }
263 },
264 getRel: function(key) {
265 return this.rels ? this.rels[key] : null;
266 }
267 });
268
269 /*
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, []) || {};
274 var self = this;
275 if (this.validators) {
276 _.each(this.validators, function(validator) {
277 var modelErrors = validator(this.getValue());
278
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
282 // as a fallback.
283 if (modelErrors) {
284 var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
285
286 //If errors are not in object form then just store on the error object
287 if (!isDictionary) {
288 errors._others = errors._others || [];
289 errors._others.push(modelErrors);
290 }
291
292 //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
293 if (isDictionary) {
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);
298 errors[key] = val;
299 }
300
301 else {
302 //Otherwise add to '_others' key
303 errors._others = errors._others || [];
304 var tmpErr = {};
305 tmpErr[key] = val;
306 errors._others.push(tmpErr);
307 }
308 });
309 }
310 }
311
312 });
313 }
314 return _.isEmpty(errors) ? null : errors;
315 }
316 });
317 */
318 })(cj);