2 openerp.web.data = function(openerp) {
5 * Serializes the sort criterion array of a dataset into a form which can be
6 * consumed by OpenERP's RPC APIs.
8 * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting
9 * @returns {String} SQL-like sorting string (``ORDER BY``) clause
11 openerp.web.serialize_sort = function (criterion) {
12 return _.map(criterion,
14 if (criteria[0] === '-') {
15 return criteria.slice(1) + ' DESC';
17 return criteria + ' ASC';
21 openerp.web.DataGroup = openerp.web.Widget.extend( /** @lends openerp.web.DataGroup# */{
23 * Management interface between views and grouped collections of OpenERP
26 * The root DataGroup is instantiated with the relevant information
27 * (a session, a model, a domain, a context and a group_by sequence), the
28 * domain and context may be empty. It is then interacted with via
29 * :js:func:`~openerp.web.DataGroup.list`, which is used to read the
30 * content of the current grouping level.
32 * @constructs openerp.web.DataGroup
33 * @extends openerp.web.Widget
35 * @param {openerp.web.Widget} parent widget
36 * @param {String} model name of the model managed by this DataGroup
37 * @param {Array} domain search domain for this DataGroup
38 * @param {Object} context context of the DataGroup's searches
39 * @param {Array} group_by sequence of fields by which to group
40 * @param {Number} [level=0] nesting level of the group
42 init: function(parent, model, domain, context, group_by, level) {
43 this._super(parent, null);
45 if (group_by.length || context['group_by_no_leaf']) {
46 return new openerp.web.ContainerDataGroup( this, model, domain, context, group_by, level);
48 return new openerp.web.GrouplessDataGroup( this, model, domain, context, level);
53 this.context = context;
56 this.level = level || 0;
60 openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.ContainerDataGroup# */ {
63 * @constructs openerp.web.ContainerDataGroup
64 * @extends openerp.web.DataGroup
73 init: function (parent, model, domain, context, group_by, level) {
74 this._super(parent, model, domain, context, null, level);
76 this.group_by = group_by;
79 * The format returned by ``read_group`` is absolutely dreadful:
81 * * A ``__context`` key provides future grouping levels
82 * * A ``__domain`` key provides the domain for the next search
83 * * The current grouping value is provided through the name of the
84 * current grouping name e.g. if currently grouping on ``user_id``, then
85 * the ``user_id`` value for this group will be provided through the
87 * * Similarly, the number of items in the group (not necessarily direct)
88 * is provided via ``${current_field}_count``
89 * * Other aggregate fields are just dumped there
91 * This function slightly improves the grouping records by:
93 * * Adding a ``grouped_on`` property providing the current grouping field
94 * * Adding a ``value`` and a ``length`` properties which replace the
95 * ``$current_field`` and ``${current_field}_count`` ones
96 * * Moving aggregate values into an ``aggregates`` property object
98 * Context and domain keys remain as-is, they should not be used externally
99 * but in case they're needed...
101 * @param {Object} group ``read_group`` record
103 transform_group: function (group) {
104 var field_name = this.group_by[0];
105 // In cases where group_by_no_leaf and no group_by, the result of
106 // read_group has aggregate fields but no __context or __domain.
107 // Create default (empty) values for those so that things don't break
108 var fixed_group = _.extend(
109 {__context: {group_by: []}, __domain: []},
113 _(fixed_group).each(function (value, key) {
114 if (key.indexOf('__') === 0
115 || key === field_name
116 || key === field_name + '_count') {
119 aggregates[key] = value || 0;
122 var group_size = fixed_group[field_name + '_count'] || fixed_group.__count || 0;
123 var leaf_group = fixed_group.__context.group_by.length === 0;
125 __context: fixed_group.__context,
126 __domain: fixed_group.__domain,
128 grouped_on: field_name,
129 // if terminal group (or no group) and group_by_no_leaf => use group.__count
131 value: fixed_group[field_name],
132 // A group is openable if it's not a leaf in group_by_no_leaf mode
133 openable: !(leaf_group && this.context['group_by_no_leaf']),
135 aggregates: aggregates
138 fetch: function (fields) {
140 var d = new $.Deferred();
143 this.rpc('/web/group/read', {
145 context: this.context,
147 fields: _.uniq(this.group_by.concat(fields)),
148 group_by_fields: this.group_by,
149 sort: openerp.web.serialize_sort(this.sort)
150 }, function () { }).then(function (response) {
151 var data_groups = _(response).map(
152 _.bind(self.transform_group, self));
153 self.groups = data_groups;
154 d.resolveWith(self, [data_groups]);
156 d.rejectWith.apply(d, [self, arguments]);
161 * The items of a list have the following properties:
164 * the number of records contained in the group (and all of its
165 * sub-groups). This does *not* provide the size of the "next level"
166 * of the group, unless the group is terminal (no more groups within
169 * the name of the field this level was grouped on, this is mostly
170 * used for display purposes, in order to know the name of the current
171 * level of grouping. The ``grouped_on`` should be the same for all
172 * objects of the list.
174 * the value which led to this group (this is the value all contained
175 * records have for the current ``grouped_on`` field name).
177 * a mapping of other aggregation fields provided by ``read_group``
179 * @param {Array} fields the list of fields to aggregate in each group, can be empty
180 * @param {Function} ifGroups function executed if any group is found (DataGroup.group_by is non-null and non-empty), called with a (potentially empty) list of groups as parameters.
181 * @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter
183 list: function (fields, ifGroups, ifRecords) {
185 this.fetch(fields).then(function (group_records) {
186 ifGroups(_(group_records).map(function (group) {
187 var child_context = _.extend({}, self.context, group.__context);
189 new openerp.web.DataGroup(
190 self, self.model, group.__domain,
191 child_context, child_context.group_by,
193 group, {sort: self.sort});
198 openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.GrouplessDataGroup# */ {
201 * @constructs openerp.web.GrouplessDataGroup
202 * @extends openerp.web.DataGroup
210 init: function (parent, model, domain, context, level) {
211 this._super(parent, model, domain, context, null, level);
213 list: function (fields, ifGroups, ifRecords) {
215 new openerp.web.DataSetSearch(this, this.model),
216 {domain: this.domain, context: this.context, _sort: this.sort}));
219 openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ {
221 * A specialization of groupless data groups, relying on a single static
222 * dataset as its records provider.
224 * @constructs openerp.web.StaticDataGroup
225 * @extends openerp.web.GrouplessDataGroup
226 * @param {openep.web.DataSetStatic} dataset a static dataset backing the groups
228 init: function (dataset) {
229 this.dataset = dataset;
231 list: function (fields, ifGroups, ifRecords) {
232 ifRecords(this.dataset);
236 openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet# */{
237 identifier_prefix: "dataset",
239 * DateaManagement interface between views and the collection of selected
240 * OpenERP records (represents the view's state?)
242 * @constructs openerp.web.DataSet
243 * @extends openerp.web.Widget
245 * @param {String} model the OpenERP model this dataset will manage
247 init: function(parent, model, context) {
250 this.context = context || {};
253 previous: function () {
255 if (this.index < 0) {
256 this.index = this.ids.length - 1;
262 if (this.index >= this.ids.length) {
267 select_id: function(id) {
268 var idx = this.get_id_index(id);
276 get_id_index: function(id) {
277 for (var i=0, ii=this.ids.length; i<ii; i++) {
278 // Here we use type coercion because of the mess potentially caused by
279 // OpenERP ids fetched from the DOM as string. (eg: dhtmlxcalendar)
280 // OpenERP ids can be non-numeric too ! (eg: recursive events in calendar)
281 if (id == this.ids[i]) {
290 * @param {Array} ids identifiers of the records to read
291 * @param {Array} fields fields to read and return, by default all fields are returned
292 * @param {Function} callback function called with read result
293 * @returns {$.Deferred}
295 read_ids: function (ids, fields, callback) {
296 return this.rpc('/web/dataset/get', {
300 context: this.get_context()
304 * Read a slice of the records represented by this DataSet, based on its
305 * domain and context.
307 * @param {Array} [fields] fields to read and return, by default all fields are returned
308 * @params {Object} options
309 * @param {Number} [options.offset=0] The index from which selected records should be returned
310 * @param {Number} [options.limit=null] The maximum number of records to return
311 * @param {Function} callback function called with read_slice result
312 * @returns {$.Deferred}
314 read_slice: function (fields, options, callback) {
318 * Reads the current dataset record (from its index)
320 * @params {Array} [fields] fields to read and return, by default all fields are returned
321 * @params {Function} callback function called with read_index result
322 * @returns {$.Deferred}
324 read_index: function (fields, callback) {
325 var def = $.Deferred().then(callback);
326 if (_.isEmpty(this.ids)) {
329 fields = fields || false;
330 this.read_ids([this.ids[this.index]], fields).then(function(records) {
331 def.resolve(records[0]);
333 def.reject.apply(def, arguments);
336 return def.promise();
339 * Reads default values for the current model
341 * @param {Array} [fields] fields to get default values for, by default all defaults are read
342 * @param {Function} callback function called with default_get result
343 * @returns {$.Deferred}
345 default_get: function(fields, callback) {
346 return this.rpc('/web/dataset/default_get', {
349 context: this.get_context()
353 * Creates a new record in db
355 * @param {Object} data field values to set on the new record
356 * @param {Function} callback function called with operation result
357 * @param {Function} error_callback function called in case of creation error
358 * @returns {$.Deferred}
360 create: function(data, callback, error_callback) {
361 return this.rpc('/web/dataset/create', {
364 context: this.get_context()
365 }, callback, error_callback);
368 * Saves the provided data in an existing db record
370 * @param {Number|String} id identifier for the record to alter
371 * @param {Object} data field values to write into the record
372 * @param {Function} callback function called with operation result
373 * @param {Function} error_callback function called in case of write error
374 * @returns {$.Deferred}
376 write: function (id, data, options, callback, error_callback) {
377 options = options || {};
378 return this.rpc('/web/dataset/save', {
382 context: this.get_context(options.context)
383 }, callback, error_callback);
386 * Deletes an existing record from the database
388 * @param {Number|String} ids identifier of the record to delete
389 * @param {Function} callback function called with operation result
390 * @param {Function} error_callback function called in case of deletion error
392 unlink: function(ids, callback, error_callback) {
394 return this.call_and_eval("unlink", [ids, this.get_context()], null, 1,
395 callback, error_callback);
398 * Calls an arbitrary RPC method
400 * @param {String} method name of the method (on the current model) to call
401 * @param {Array} [args] arguments to pass to the method
402 * @param {Function} callback
403 * @param {Function} error_callback
404 * @returns {$.Deferred}
406 call: function (method, args, callback, error_callback) {
407 return this.rpc('/web/dataset/call', {
411 }, callback, error_callback);
414 * Calls an arbitrary method, with more crazy
416 * @param {String} method
417 * @param {Array} [args]
418 * @param {Number} [domain_index] index of a domain to evaluate in the args array
419 * @param {Number} [context_index] index of a context to evaluate in the args array
420 * @param {Function} callback
421 * @param {Function }error_callback
422 * @returns {$.Deferred}
424 call_and_eval: function (method, args, domain_index, context_index, callback, error_callback) {
425 return this.rpc('/web/dataset/call', {
428 domain_id: domain_index == undefined ? null : domain_index,
429 context_id: context_index == undefined ? null : context_index,
431 }, callback, error_callback);
434 * Calls a button method, usually returning some sort of action
436 * @param {String} method
437 * @param {Array} [args]
438 * @param {Function} callback
439 * @param {Function} error_callback
440 * @returns {$.Deferred}
442 call_button: function (method, args, callback, error_callback) {
443 return this.rpc('/web/dataset/call_button', {
449 }, callback, error_callback);
452 * Fetches the "readable name" for records, based on intrinsic rules
455 * @param {Function} callback
456 * @returns {$.Deferred}
458 name_get: function(ids, callback) {
459 return this.call_and_eval('name_get', [ids, this.get_context()], null, 1, callback);
463 * @param {String} name name to perform a search for/on
464 * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
465 * @param {String} [operator='ilike'] matching operator to use with the provided name value
466 * @param {Number} [limit=100] maximum number of matches to return
467 * @param {Function} callback function to call with name_search result
468 * @returns {$.Deferred}
470 name_search: function (name, domain, operator, limit, callback) {
471 return this.call_and_eval('name_search',
472 [name || '', domain || false, operator || 'ilike', this.get_context(), limit || 100],
479 name_create: function(name, callback) {
480 return this.call_and_eval('name_create', [name, this.get_context()], null, 1, callback);
482 exec_workflow: function (id, signal, callback) {
483 return this.rpc('/web/dataset/exec_workflow', {
489 get_context: function(request_context) {
490 if (request_context) {
491 return new openerp.web.CompoundContext(this.context, request_context);
496 openerp.web.DataSetStatic = openerp.web.DataSet.extend({
497 init: function(parent, model, context, ids) {
498 this._super(parent, model, context);
500 this.ids = ids || [];
502 read_slice: function (fields, options, callback) {
503 // TODO remove fields from options
505 offset = options.offset || 0,
506 limit = options.limit || false,
507 fields = fields || false;
508 var end_pos = limit && limit !== -1 ? offset + limit : this.ids.length;
509 return this.read_ids(this.ids.slice(offset, end_pos), fields, callback);
511 set_ids: function (ids) {
513 if (this.index !== null) {
514 this.index = this.index <= this.ids.length - 1 ?
515 this.index : (this.ids.length > 0 ? this.length - 1 : 0);
518 unlink: function(ids) {
520 return $.Deferred().resolve({result: true});
522 on_unlink: function(ids) {
523 this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
526 openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{
528 * @constructs openerp.web.DataSetSearch
529 * @extends openerp.web.DataSet
531 * @param {Object} parent
532 * @param {String} model
533 * @param {Object} context
534 * @param {Array} domain
536 init: function(parent, model, context, domain) {
537 this._super(parent, model, context);
538 this.domain = domain || [];
541 // subset records[offset:offset+limit]
546 * Read a slice of the records represented by this DataSet, based on its
547 * domain and context.
549 * @params {Object} options
550 * @param {Array} [options.fields] fields to read and return, by default all fields are returned
551 * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
552 * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
553 * @param {Number} [options.offset=0] The index from which selected records should be returned
554 * @param {Number} [options.limit=null] The maximum number of records to return
555 * @param {Function} callback function called with read_slice result
556 * @returns {$.Deferred}
558 read_slice: function (fields, options, callback) {
560 var options = options || {};
561 var offset = options.offset || 0;
562 return this.rpc('/web/dataset/search_read', {
564 fields: fields || false,
565 domain: this.get_domain(options.domain),
566 context: this.get_context(options.context),
569 limit: options.limit || false
570 }).pipe(function (result) {
571 self.ids = result.ids;
572 self.offset = offset;
573 return result.records;
576 get_domain: function (other_domain) {
578 return new openerp.web.CompoundDomain(this.domain, other_domain);
583 * Reads or changes sort criteria on the dataset.
585 * If not provided with any argument, serializes the sort criteria to
586 * an SQL-like form usable by OpenERP's ORM.
588 * If given a field, will set that field as first sorting criteria or,
589 * if the field is already the first sorting criteria, will reverse it.
591 * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
592 * @param {Boolean} [force_reverse=false] forces inserting the field as DESC
593 * @returns {String|undefined}
595 sort: function (field, force_reverse) {
597 return openerp.web.serialize_sort(this._sort);
599 var reverse = force_reverse || (this._sort[0] === field);
600 this._sort.splice.apply(
601 this._sort, [0, this._sort.length].concat(
602 _.without(this._sort, field, '-' + field)));
604 this._sort.unshift((reverse ? '-' : '') + field);
607 unlink: function(ids, callback, error_callback) {
609 return this._super(ids, function(result) {
610 self.ids = _.without.apply(_, [self.ids].concat(ids));
611 if (this.index !== null) {
612 self.index = self.index <= self.ids.length - 1 ?
613 self.index : (self.ids.length > 0 ? self.ids.length -1 : 0);
620 openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({
621 virtual_id_prefix: "one2many_v_id_",
624 this._super.apply(this, arguments);
626 this.last_default_get = {};
628 default_get: function(fields, callback) {
629 return this._super(fields).then(this.on_default_get).then(callback);
631 on_default_get: function(res) {
632 this.last_default_get = res;
634 create: function(data, callback, error_callback) {
635 var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
636 defaults: this.last_default_get};
637 this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)}));
638 this.cache.push(cached);
640 var prom = $.Deferred().then(callback);
641 prom.resolve({result: cached.id});
642 return prom.promise();
644 write: function (id, data, options, callback) {
646 var record = _.detect(this.to_create, function(x) {return x.id === id;});
647 record = record || _.detect(this.to_write, function(x) {return x.id === id;});
650 for (var k in data) {
651 if (record.values[k] === undefined || record.values[k] !== data[k]) {
656 $.extend(record.values, data);
659 record = {id: id, values: data};
660 self.to_write.push(record);
662 var cached = _.detect(this.cache, function(x) {return x.id === id;});
664 cached = {id: id, values: {}};
665 this.cache.push(cached);
667 $.extend(cached.values, record.values);
670 var to_return = $.Deferred().then(callback);
671 to_return.resolve({result: true});
672 return to_return.promise();
674 unlink: function(ids, callback, error_callback) {
676 _.each(ids, function(id) {
677 if (! _.detect(self.to_create, function(x) { return x.id === id; })) {
678 self.to_delete.push({id: id})
681 this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);});
682 this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);});
683 this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);});
684 this.set_ids(_.without.apply(_, [this.ids].concat(ids)));
686 var to_return = $.Deferred().then(callback);
687 setTimeout(function () {to_return.resolve({result: true});}, 0);
688 return to_return.promise();
690 reset_ids: function(ids) {
696 this.delete_all = false;
698 on_change: function() {},
699 read_ids: function (ids, fields, callback) {
702 _.each(ids, function(id) {
703 var cached = _.detect(self.cache, function(x) {return x.id === id;});
704 var created = _.detect(self.to_create, function(x) {return x.id === id;});
706 _.each(fields, function(x) {if (cached.values[x] === undefined)
707 cached.values[x] = created.defaults[x] || false;});
709 if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined}))
713 var completion = $.Deferred().then(callback);
714 var return_records = function() {
715 var records = _.map(ids, function(id) {
716 return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id});
718 if (self.debug_mode) {
719 if (_.include(records, undefined)) {
720 throw "Record not correctly loaded";
723 completion.resolve(records);
725 if(to_get.length > 0) {
726 var rpc_promise = this._super(to_get, fields, function(records) {
727 _.each(records, function(record, index) {
728 var id = to_get[index];
729 var cached = _.detect(self.cache, function(x) {return x.id === id;});
731 self.cache.push({id: id, values: record});
733 // I assume cache value is prioritary
734 _.defaults(cached.values, record);
739 $.when(rpc_promise).fail(function() {completion.reject();});
743 return completion.promise();
745 call_button: function (method, args, callback, error_callback) {
746 var id = args[0][0], index;
747 for(var i=0, len=this.cache.length; i<len; ++i) {
748 var record = this.cache[i];
749 // if record we call the button upon is in the cache
750 if (record.id === id) {
751 // evict it so it gets reloaded from server
752 this.cache.splice(i, 1);
756 return this._super(method, args, callback, error_callback);
759 openerp.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
761 openerp.web.ProxyDataSet = openerp.web.DataSetSearch.extend({
763 this._super.apply(this, arguments);
764 this.create_function = null;
765 this.write_function = null;
766 this.read_function = null;
768 read_ids: function () {
769 if (this.read_function) {
770 return this.read_function.apply(null, arguments);
772 return this._super.apply(this, arguments);
775 default_get: function(fields, callback) {
776 return this._super(fields, callback).then(this.on_default_get);
778 on_default_get: function(result) {},
779 create: function(data, callback, error_callback) {
780 this.on_create(data);
781 if (this.create_function) {
782 return this.create_function(data, callback, error_callback);
784 console.warn("trying to create a record using default proxy dataset behavior");
785 var to_return = $.Deferred().then(callback);
786 setTimeout(function () {to_return.resolve({"result": undefined});}, 0);
787 return to_return.promise();
790 on_create: function(data) {},
791 write: function (id, data, options, callback) {
792 this.on_write(id, data);
793 if (this.write_function) {
794 return this.write_function(id, data, options, callback);
796 console.warn("trying to write a record using default proxy dataset behavior");
797 var to_return = $.Deferred().then(callback);
798 setTimeout(function () {to_return.resolve({"result": true});}, 0);
799 return to_return.promise();
802 on_write: function(id, data) {},
803 unlink: function(ids, callback, error_callback) {
805 console.warn("trying to unlink a record using default proxy dataset behavior");
806 var to_return = $.Deferred().then(callback);
807 setTimeout(function () {to_return.resolve({"result": true});}, 0);
808 return to_return.promise();
810 on_unlink: function(ids) {}
813 openerp.web.Model = openerp.web.CallbackEnabled.extend({
814 init: function(_, model_name) {
816 this.model_name = model_name;
819 var c = openerp.connection;
820 return c.rpc.apply(c, arguments);
822 get_func: function(method_name) {
825 if (method_name == "search_read")
826 return self._search_read.apply(self, arguments);
827 return self._call(method_name, _.toArray(arguments));
830 _call: function (method, args) {
831 return this.rpc('/web/dataset/call', {
832 model: this.model_name,
835 }).pipe(function(result) {
836 if (method == "read" && result instanceof Array && result.length > 0 && result[0]["id"]) {
838 _.each(_.range(result.length), function(i) {
839 index[result[i]["id"]] = result[i];
841 result = _.map(args[0], function(x) {return index[x];});
846 _search_read: function(domain, fields, offset, limit, order, context) {
847 return this.rpc('/web/dataset/search_read', {
848 model: this.model_name,
855 }).pipe(function(result) {
856 return result.records;
861 openerp.web.CompoundContext = openerp.web.Class.extend({
863 this.__ref = "compound_context";
864 this.__contexts = [];
865 this.__eval_context = null;
867 _.each(arguments, function(x) {
871 add: function (context) {
872 this.__contexts.push(context);
875 set_eval_context: function (eval_context) {
876 this.__eval_context = eval_context;
879 get_eval_context: function () {
880 return this.__eval_context;
884 openerp.web.CompoundDomain = openerp.web.Class.extend({
886 this.__ref = "compound_domain";
888 this.__eval_context = null;
890 _.each(arguments, function(x) {
894 add: function(domain) {
895 this.__domains.push(domain);
898 set_eval_context: function(eval_context) {
899 this.__eval_context = eval_context;
902 get_eval_context: function() {
903 return this.__eval_context;
908 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: