Commit | Line | Data |
---|---|---|
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); |