CRM-12943 - Add CRM.Backbone.sync with qUnit tests
[civicrm-core.git] / js / crm.backbone.js
CommitLineData
6a488035
TO
1(function($) {
2 var CRM = (window.CRM) ? (window.CRM) : (window.CRM = {});
3 if (!CRM.Backbone) CRM.Backbone = {};
4
231a4c0f
TO
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 */
15 CRM.Backbone.sync = function(method, model, options) {
16 var isCollection = _.isArray(model.models);
17
18 if (isCollection) {
19 var apiOptions = {
20 success: function(data) {
21 // unwrap data
22 options.success(_.toArray(data.values));
23 },
24 error: function(data) {
25 // CRM.api displays errors by default, but Backbone.sync
26 // protocol requires us to override "error". This restores
27 // the default behavior.
28 $().crmError(data.error_message, ts('Error'));
29 options.error(data);
30 }
31 };
32 switch (method) {
33 case 'read':
34 CRM.api(model.crmEntityName, 'get', model.toCrmCriteria(), apiOptions);
35 break;
36 default:
37 apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for collections"});
38 break;
39 }
40 } else {
41 // callback options to pass to CRM.api
42 var apiOptions = {
43 success: function(data) {
44 // unwrap data
45 var values = _.toArray(data['values']);
46 if (values.length == 1) {
47 options.success(values[0]);
48 } else {
49 data.is_error = 1;
50 data.error_message = ts("Expected exactly one response");
51 apiOptions.error(data);
52 }
53 },
54 error: function(data) {
55 // CRM.api displays errors by default, but Backbone.sync
56 // protocol requires us to override "error". This restores
57 // the default behavior.
58 $().crmError(data.error_message, ts('Error'));
59 options.error(data);
60 }
61 };
62 switch (method) {
63 case 'create': // pass-through
64 case 'update':
65 CRM.api(model.crmEntityName, 'create', model.toJSON(), apiOptions);
66 break;
67 case 'read':
68 var params = model.toCrmCriteria();
69 if (!params.id) {
70 apiOptions.error({is_error: 1, error_message: 'Missing ID for ' + model.crmEntityName});
71 return;
72 }
73 CRM.api(model.crmEntityName, 'get', params, apiOptions);
74 break;
75 case 'delete':
76 default:
77 apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for models"});
78 }
79 }
80 };
81
82 /**
83 * Connect a "model" class to CiviCRM's APIv3
84 *
85 * @code
86 * // Setup class
87 * var ContactModel = Backbone.Model.extend({});
88 * CRM.Backbone.extendModel(ContactModel, "Contact");
89 *
90 * // Use class
91 * c = new ContactModel({id: 3});
92 * c.fetch();
93 * @endcode
94 *
95 * @param Class ModelClass
96 * @param string crmEntityName APIv3 entity name, such as "Contact" or "CustomField"
97 */
98 CRM.Backbone.extendModel = function(ModelClass, crmEntityName) {
99 // Defaults - if specified in ModelClass, preserve
100 _.defaults(ModelClass.prototype, {
101 crmEntityName: crmEntityName,
102 toCrmCriteria: function() {
103 return (this.get('id')) ? {id: this.get('id')} : {};
104 }
105 });
106 // Overrides - if specified in ModelClass, replace
107 _.extend(ModelClass.prototype, {
108 sync: CRM.Backbone.sync
109 });
110 };
111
112 /**
113 * Connect a "collection" class to CiviCRM's APIv3
114 *
115 * Note: the collection supports a special property, crmCriteria, which is an array of
116 * query options to send to the API
117 *
118 * @code
119 * // Setup class
120 * var ContactModel = Backbone.Model.extend({});
121 * CRM.Backbone.extendModel(ContactModel, "Contact");
122 * var ContactCollection = Backbone.Collection.extend({
123 * model: ContactModel
124 * });
125 * CRM.Backbone.extendCollection(ContactCollection);
126 *
127 * // Use class
128 * var c = new ContactCollection([], {
129 * crmCriteria: {contact_type: 'Organization'}
130 * });
131 * c.fetch();
132 * @endcode
133 *
134 * @param Class CollectionClass
135 */
136 CRM.Backbone.extendCollection = function(CollectionClass) {
137 var origInit = CollectionClass.prototype.initialize;
138 // Defaults - if specified in CollectionClass, preserve
139 _.defaults(CollectionClass.prototype, {
140 crmEntityName: CollectionClass.prototype.model.prototype.crmEntityName,
141 toCrmCriteria: function() {
142 return this.crmCriteria || {};
143 }
144 });
145 // Overrides - if specified in CollectionClass, replace
146 _.extend(CollectionClass.prototype, {
147 sync: CRM.Backbone.sync,
148 initialize: function(models, options) {
149 options || (options = {});
150 if (options.crmCriteria) {
151 this.crmCriteria = options.crmCriteria;
152 }
153 if (origInit) {
154 return origInit.apply(this, arguments);
155 }
156 }
157 });
158 };
159
6a488035
TO
160 CRM.Backbone.Model = Backbone.Model.extend({
161 /**
162 * Return JSON version of model -- but only include fields that are
163 * listed in the 'schema'.
164 *
165 * @return {*}
166 */
167 toStrictJSON: function() {
168 var schema = this.schema;
169 var result = this.toJSON();
170 _.each(result, function(value, key){
171 if (! schema[key]) {
172 delete result[key];
173 }
174 });
175 return result;
176 },
177 setRel: function(key, value, options) {
178 this.rels = this.rels || {};
179 if (this.rels[key] != value) {
180 this.rels[key] = value;
181 this.trigger("rel:"+key, value);
182 }
183 },
184 getRel: function(key) {
185 return this.rels ? this.rels[key] : null;
186 }
187 });
188
189 CRM.Backbone.Collection = Backbone.Collection.extend({
190 /**
191 * Store 'key' on this.rel and automatically copy it to
192 * any children.
193 *
194 * @param key
195 * @param value
196 * @param initialModels
197 */
198 initializeCopyToChildrenRelation: function(key, value, initialModels) {
199 this.setRel(key, value, {silent: true});
200 this.on('reset', this._copyToChildren, this);
201 this.on('add', this._copyToChild, this);
202 },
203 _copyToChildren: function() {
204 var collection = this;
205 collection.each(function(model){
206 collection._copyToChild(model);
207 });
208 },
209 _copyToChild: function(model) {
210 _.each(this.rels, function(relValue, relKey){
211 model.setRel(relKey, relValue, {silent: true});
212 });
213 },
214 setRel: function(key, value, options) {
215 this.rels = this.rels || {};
216 if (this.rels[key] != value) {
217 this.rels[key] = value;
218 this.trigger("rel:"+key, value);
219 }
220 },
221 getRel: function(key) {
222 return this.rels ? this.rels[key] : null;
223 }
224 });
225
226 /*
227 CRM.Backbone.Form = Backbone.Form.extend({
228 validate: function() {
229 // Add support for form-level validators
230 var errors = Backbone.Form.prototype.validate.apply(this, []) || {};
231 var self = this;
232 if (this.validators) {
233 _.each(this.validators, function(validator) {
234 var modelErrors = validator(this.getValue());
235
236 // The following if() has been copied-pasted from the parent's
237 // handling of model-validators. They are similar in that the errors are
238 // probably keyed by field names... but not necessarily, so we use _others
239 // as a fallback.
240 if (modelErrors) {
241 var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
242
243 //If errors are not in object form then just store on the error object
244 if (!isDictionary) {
245 errors._others = errors._others || [];
246 errors._others.push(modelErrors);
247 }
248
249 //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
250 if (isDictionary) {
251 _.each(modelErrors, function(val, key) {
252 //Set error on field if there isn't one already
253 if (self.fields[key] && !errors[key]) {
254 self.fields[key].setError(val);
255 errors[key] = val;
256 }
257
258 else {
259 //Otherwise add to '_others' key
260 errors._others = errors._others || [];
261 var tmpErr = {};
262 tmpErr[key] = val;
263 errors._others.push(tmpErr);
264 }
265 });
266 }
267 }
268
269 });
270 }
271 return _.isEmpty(errors) ? null : errors;
272 }
273 });
274 */
275})(cj);