[FIX] web: instance.web.DataSetSearch.get_domain() returns the domain.
[odoo/odoo.git] / addons / web / static / src / js / data.js
1
2 openerp.web.data = function(instance) {
3
4 /**
5  * Serializes the sort criterion array of a dataset into a form which can be
6  * consumed by OpenERP's RPC APIs.
7  *
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
10  */
11 instance.web.serialize_sort = function (criterion) {
12     return _.map(criterion,
13         function (criteria) {
14             if (criteria[0] === '-') {
15                 return criteria.slice(1) + ' DESC';
16             }
17             return criteria + ' ASC';
18         }).join(', ');
19 };
20
21 instance.web.Query = instance.web.Class.extend({
22     init: function (model, fields) {
23         this._model = model;
24         this._fields = fields;
25         this._filter = [];
26         this._context = {};
27         this._limit = false;
28         this._offset = 0;
29         this._order_by = [];
30     },
31     clone: function (to_set) {
32         to_set = to_set || {};
33         var q = new instance.web.Query(this._model, this._fields);
34         q._context = this._context;
35         q._filter = this._filter;
36         q._limit = this._limit;
37         q._offset = this._offset;
38         q._order_by = this._order_by;
39
40         for(var key in to_set) {
41             if (!to_set.hasOwnProperty(key)) { continue; }
42             switch(key) {
43             case 'filter':
44                 q._filter = new instance.web.CompoundDomain(
45                         q._filter, to_set.filter);
46                 break;
47             case 'context':
48                 q._context = new instance.web.CompoundContext(
49                         q._context, to_set.context);
50                 break;
51             case 'limit':
52             case 'offset':
53             case 'order_by':
54                 q['_' + key] = to_set[key];
55             }
56         }
57         return q;
58     },
59     _execute: function () {
60         var self = this;
61         return instance.session.rpc('/web/dataset/search_read', {
62             model: this._model.name,
63             fields: this._fields || false,
64             domain: instance.web.pyeval.eval('domains',
65                     [this._model.domain(this._filter)]),
66             context: instance.web.pyeval.eval('contexts',
67                     [this._model.context(this._context)]),
68             offset: this._offset,
69             limit: this._limit,
70             sort: instance.web.serialize_sort(this._order_by)
71         }).then(function (results) {
72             self._count = results.length;
73             return results.records;
74         }, null);
75     },
76     /**
77      * Fetches the first record matching the query, or null
78      *
79      * @returns {jQuery.Deferred<Object|null>}
80      */
81     first: function () {
82         var self = this;
83         return this.clone({limit: 1})._execute().then(function (records) {
84             delete self._count;
85             if (records.length) { return records[0]; }
86             return null;
87         });
88     },
89     /**
90      * Fetches all records matching the query
91      *
92      * @returns {jQuery.Deferred<Array<>>}
93      */
94     all: function () {
95         return this._execute();
96     },
97     /**
98      * Fetches the number of records matching the query in the database
99      *
100      * @returns {jQuery.Deferred<Number>}
101      */
102     count: function () {
103         if (this._count != undefined) { return $.when(this._count); }
104         return this._model.call(
105             'search_count', [this._filter], {
106                 context: this._model.context(this._context)});
107     },
108     /**
109      * Performs a groups read according to the provided grouping criterion
110      *
111      * @param {String|Array<String>} grouping
112      * @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null}
113      */
114     group_by: function (grouping) {
115         var ctx = instance.web.pyeval.eval(
116             'context', this._model.context(this._context));
117
118         // undefined passed in explicitly (!)
119         if (_.isUndefined(grouping)) {
120             grouping = [];
121         }
122
123         if (!(grouping instanceof Array)) {
124             grouping = _.toArray(arguments);
125         }
126         if (_.isEmpty(grouping) && !ctx['group_by_no_leaf']) {
127             return null;
128         }
129
130         var self = this;
131         return this._model.call('read_group', {
132             groupby: grouping,
133             fields: _.uniq(grouping.concat(this._fields || [])),
134             domain: this._model.domain(this._filter),
135             context: ctx,
136             offset: this._offset,
137             limit: this._limit,
138             orderby: instance.web.serialize_sort(this._order_by) || false
139         }).then(function (results) {
140             return _(results).map(function (result) {
141                 // FIX: querygroup initialization
142                 result.__context = result.__context || {};
143                 result.__context.group_by = result.__context.group_by || [];
144                 _.defaults(result.__context, ctx);
145                 return new instance.web.QueryGroup(
146                     self._model.name, grouping[0], result);
147             });
148         });
149     },
150     /**
151      * Creates a new query with the union of the current query's context and
152      * the new context.
153      *
154      * @param context context data to add to the query
155      * @returns {openerp.web.Query}
156      */
157     context: function (context) {
158         if (!context) { return this; }
159         return this.clone({context: context});
160     },
161     /**
162      * Creates a new query with the union of the current query's filter and
163      * the new domain.
164      *
165      * @param domain domain data to AND with the current query filter
166      * @returns {openerp.web.Query}
167      */
168     filter: function (domain) {
169         if (!domain) { return this; }
170         return this.clone({filter: domain});
171     },
172     /**
173      * Creates a new query with the provided limit replacing the current
174      * query's own limit
175      *
176      * @param {Number} limit maximum number of records the query should retrieve
177      * @returns {openerp.web.Query}
178      */
179     limit: function (limit) {
180         return this.clone({limit: limit});
181     },
182     /**
183      * Creates a new query with the provided offset replacing the current
184      * query's own offset
185      *
186      * @param {Number} offset number of records the query should skip before starting its retrieval
187      * @returns {openerp.web.Query}
188      */
189     offset: function (offset) {
190         return this.clone({offset: offset});
191     },
192     /**
193      * Creates a new query with the provided ordering parameters replacing
194      * those of the current query
195      *
196      * @param {String...} fields ordering clauses
197      * @returns {openerp.web.Query}
198      */
199     order_by: function (fields) {
200         if (fields === undefined) { return this; }
201         if (!(fields instanceof Array)) {
202             fields = _.toArray(arguments);
203         }
204         if (_.isEmpty(fields)) { return this; }
205         return this.clone({order_by: fields});
206     }
207 });
208
209 instance.web.QueryGroup = instance.web.Class.extend({
210     init: function (model, grouping_field, read_group_group) {
211         // In cases where group_by_no_leaf and no group_by, the result of
212         // read_group has aggregate fields but no __context or __domain.
213         // Create default (empty) values for those so that things don't break
214         var fixed_group = _.extend(
215             {__context: {group_by: []}, __domain: []},
216             read_group_group);
217
218         var aggregates = {};
219         _(fixed_group).each(function (value, key) {
220             if (key.indexOf('__') === 0
221                     || key === grouping_field
222                     || key === grouping_field + '_count') {
223                 return;
224             }
225             aggregates[key] = value || 0;
226         });
227
228         this.model = new instance.web.Model(
229             model, fixed_group.__context, fixed_group.__domain);
230
231         var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0;
232         var leaf_group = fixed_group.__context.group_by.length === 0;
233         this.attributes = {
234             folded: !!(fixed_group.__fold),
235             grouped_on: grouping_field,
236             // if terminal group (or no group) and group_by_no_leaf => use group.__count
237             length: group_size,
238             value: fixed_group[grouping_field],
239             // A group is open-able if it's not a leaf in group_by_no_leaf mode
240             has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']),
241
242             aggregates: aggregates
243         };
244     },
245     get: function (key) {
246         return this.attributes[key];
247     },
248     subgroups: function () {
249         return this.model.query().group_by(this.model.context().group_by);
250     },
251     query: function () {
252         return this.model.query.apply(this.model, arguments);
253     }
254 });
255
256 instance.web.Model = instance.web.Class.extend({
257     /**
258      * @constructs instance.web.Model
259      * @extends instance.web.Class
260      *
261      * @param {String} model_name name of the OpenERP model this object is bound to
262      * @param {Object} [context]
263      * @param {Array} [domain]
264      */
265     init: function (model_name, context, domain) {
266         this.name = model_name;
267         this._context = context || {};
268         this._domain = domain || [];
269     },
270     /**
271      * @deprecated does not allow to specify kwargs, directly use call() instead
272      */
273     get_func: function (method_name) {
274         var self = this;
275         return function () {
276             return self.call(method_name, _.toArray(arguments));
277         };
278     },
279     /**
280      * Call a method (over RPC) on the bound OpenERP model.
281      *
282      * @param {String} method name of the method to call
283      * @param {Array} [args] positional arguments
284      * @param {Object} [kwargs] keyword arguments
285      * @param {Object} [options] additional options for the rpc() method
286      * @returns {jQuery.Deferred<>} call result
287      */
288     call: function (method, args, kwargs, options) {
289         args = args || [];
290         kwargs = kwargs || {};
291         if (!_.isArray(args)) {
292             // call(method, kwargs)
293             kwargs = args;
294             args = [];
295         }
296         instance.web.pyeval.ensure_evaluated(args, kwargs);
297         var debug = instance.session.debug ? '/'+this.name+':'+method : '';
298         return instance.session.rpc('/web/dataset/call_kw' + debug, {
299             model: this.name,
300             method: method,
301             args: args,
302             kwargs: kwargs
303         }, options);
304     },
305     /**
306      * Fetches a Query instance bound to this model, for searching
307      *
308      * @param {Array<String>} [fields] fields to ultimately fetch during the search
309      * @returns {instance.web.Query}
310      */
311     query: function (fields) {
312         return new instance.web.Query(this, fields);
313     },
314     /**
315      * Executes a signal on the designated workflow, on the bound OpenERP model
316      *
317      * @param {Number} id workflow identifier
318      * @param {String} signal signal to trigger on the workflow
319      */
320     exec_workflow: function (id, signal) {
321         return instance.session.rpc('/web/dataset/exec_workflow', {
322             model: this.name,
323             id: id,
324             signal: signal
325         });
326     },
327     /**
328      * Fetches the model's domain, combined with the provided domain if any
329      *
330      * @param {Array} [domain] to combine with the model's internal domain
331      * @returns {instance.web.CompoundDomain} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
332      */
333     domain: function (domain) {
334         if (!domain) { return this._domain; }
335         return new instance.web.CompoundDomain(
336             this._domain, domain);
337     },
338     /**
339      * Fetches the combination of the user's context and the domain context,
340      * combined with the provided context if any
341      *
342      * @param {Object} [context] to combine with the model's internal context
343      * @returns {instance.web.CompoundContext} The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
344      */
345     context: function (context) {
346         return new instance.web.CompoundContext(
347             instance.session.user_context, this._context, context || {});
348     },
349     /**
350      * Button action caller, needs to perform cleanup if an action is returned
351      * from the button (parsing of context and domain, and fixup of the views
352      * collection for act_window actions)
353      *
354      * FIXME: remove when evaluator integrated
355      */
356     call_button: function (method, args) {
357         instance.web.pyeval.ensure_evaluated(args, {});
358         return instance.session.rpc('/web/dataset/call_button', {
359             model: this.name,
360             method: method,
361             // Should not be necessary anymore. Integrate remote in this?
362             domain_id: null,
363             context_id: args.length - 1,
364             args: args || []
365         });
366     },
367 });
368
369 instance.web.DataSet =  instance.web.Class.extend(instance.web.PropertiesMixin, {
370     /**
371      * Collection of OpenERP records, used to share records and the current selection between views.
372      *
373      * @constructs instance.web.DataSet
374      *
375      * @param {String} model the OpenERP model this dataset will manage
376      */
377     init: function(parent, model, context) {
378         instance.web.PropertiesMixin.init.call(this);
379         this.model = model;
380         this.context = context || {};
381         this.index = null;
382         this._sort = [];
383         this._model = new instance.web.Model(model, context);
384     },
385     previous: function () {
386         this.index -= 1;
387         if (!this.ids.length) {
388             this.index = null;
389         } else if (this.index < 0) {
390             this.index = this.ids.length - 1;
391         }
392         return this;
393     },
394     next: function () {
395         this.index += 1;
396         if (!this.ids.length) {
397             this.index = null;
398         } else if (this.index >= this.ids.length) {
399             this.index = 0;
400         }
401         return this;
402     },
403     select_id: function(id) {
404         var idx = this.get_id_index(id);
405         if (idx === null) {
406             return false;
407         } else {
408             this.index = idx;
409             return true;
410         }
411     },
412     get_id_index: function(id) {
413         for (var i=0, ii=this.ids.length; i<ii; i++) {
414             // Here we use type coercion because of the mess potentially caused by
415             // OpenERP ids fetched from the DOM as string. (eg: dhtmlxcalendar)
416             // OpenERP ids can be non-numeric too ! (eg: recursive events in calendar)
417             if (id == this.ids[i]) {
418                 return i;
419             }
420         }
421         return null;
422     },
423     /**
424      * Read records.
425      *
426      * @param {Array} ids identifiers of the records to read
427      * @param {Array} fields fields to read and return, by default all fields are returned
428      * @returns {$.Deferred}
429      */
430     read_ids: function (ids, fields, options) {
431         options = options || {};
432         // TODO: reorder results to match ids list
433         return this._model.call('read',
434             [ids, fields || false],
435             {context: this.get_context(options.context)});
436     },
437     /**
438      * Read a slice of the records represented by this DataSet, based on its
439      * domain and context.
440      *
441      * @param {Array} [fields] fields to read and return, by default all fields are returned
442      * @params {Object} [options]
443      * @param {Number} [options.offset=0] The index from which selected records should be returned
444      * @param {Number} [options.limit=null] The maximum number of records to return
445      * @returns {$.Deferred}
446      */
447     read_slice: function (fields, options) {
448         var self = this;
449         options = options || {};
450         return this._model.query(fields)
451                 .limit(options.limit || false)
452                 .offset(options.offset || 0)
453                 .all().done(function (records) {
454             self.ids = _(records).pluck('id');
455         });
456     },
457     /**
458      * Reads the current dataset record (from its index)
459      *
460      * @params {Array} [fields] fields to read and return, by default all fields are returned
461      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
462      * @returns {$.Deferred}
463      */
464     read_index: function (fields, options) {
465         options = options || {};
466         return this.read_ids([this.ids[this.index]], fields, options).then(function (records) {
467             if (_.isEmpty(records)) { return $.Deferred().reject().promise(); }
468             return records[0];
469         });
470     },
471     /**
472      * Reads default values for the current model
473      *
474      * @param {Array} [fields] fields to get default values for, by default all defaults are read
475      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
476      * @returns {$.Deferred}
477      */
478     default_get: function(fields, options) {
479         options = options || {};
480         return this._model.call('default_get',
481             [fields], {context: this.get_context(options.context)});
482     },
483     /**
484      * Creates a new record in db
485      *
486      * @param {Object} data field values to set on the new record
487      * @param {Object} options Dictionary that can contain the following keys:
488      *   - readonly_fields: Values from readonly fields that were updated by
489      *     on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
490      * @returns {$.Deferred}
491      */
492     create: function(data, options) {
493         var self = this;
494         return this._model.call('create', [data], {
495             context: this.get_context()
496         }).done(function () {
497             self.trigger('dataset_changed', data, options)
498         });
499     },
500     /**
501      * Saves the provided data in an existing db record
502      *
503      * @param {Number|String} id identifier for the record to alter
504      * @param {Object} data field values to write into the record
505      * @param {Object} options Dictionary that can contain the following keys:
506      *   - context: The context to use in the server-side call.
507      *   - readonly_fields: Values from readonly fields that were updated by
508      *     on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
509      * @returns {$.Deferred}
510      */
511     write: function (id, data, options) {
512         options = options || {};
513         var self = this;
514         return this._model.call('write', [[id], data], {
515             context: this.get_context(options.context)
516         }).done(function () {
517             self.trigger('dataset_changed', id, data, options)
518         });
519     },
520     /**
521      * Deletes an existing record from the database
522      *
523      * @param {Number|String} ids identifier of the record to delete
524      */
525     unlink: function(ids) {
526         var self = this;
527         return this._model.call('unlink', [ids], {
528             context: this.get_context()
529         }).done(function () {
530             self.trigger('dataset_changed', ids)
531         });
532     },
533     /**
534      * Calls an arbitrary RPC method
535      *
536      * @param {String} method name of the method (on the current model) to call
537      * @param {Array} [args] arguments to pass to the method
538      * @param {Function} callback
539      * @param {Function} error_callback
540      * @returns {$.Deferred}
541      */
542     call: function (method, args) {
543         return this._model.call(method, args);
544     },
545     /**
546      * Calls a button method, usually returning some sort of action
547      *
548      * @param {String} method
549      * @param {Array} [args]
550      * @returns {$.Deferred}
551      */
552     call_button: function (method, args) {
553         return this._model.call_button(method, args);
554     },
555     /**
556      * Fetches the "readable name" for records, based on intrinsic rules
557      *
558      * @param {Array} ids
559      * @returns {$.Deferred}
560      */
561     name_get: function(ids) {
562         return this._model.call('name_get', [ids], {context: this.get_context()});
563     },
564     /**
565      * 
566      * @param {String} name name to perform a search for/on
567      * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
568      * @param {String} [operator='ilike'] matching operator to use with the provided name value
569      * @param {Number} [limit=0] maximum number of matches to return
570      * @param {Function} callback function to call with name_search result
571      * @returns {$.Deferred}
572      */
573     name_search: function (name, domain, operator, limit) {
574         return this._model.call('name_search', {
575             name: name || '',
576             args: domain || false,
577             operator: operator || 'ilike',
578             context: this._model.context(),
579             limit: limit || 0
580         });
581     },
582     /**
583      * @param name
584      */
585     name_create: function(name) {
586         return this._model.call('name_create', [name], {context: this.get_context()});
587     },
588     exec_workflow: function (id, signal) {
589         return this._model.exec_workflow(id, signal);
590     },
591     get_context: function(request_context) {
592         return this._model.context(request_context);
593     },
594     /**
595      * Reads or changes sort criteria on the dataset.
596      *
597      * If not provided with any argument, serializes the sort criteria to
598      * an SQL-like form usable by OpenERP's ORM.
599      *
600      * If given a field, will set that field as first sorting criteria or,
601      * if the field is already the first sorting criteria, will reverse it.
602      *
603      * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
604      * @param {Boolean} [force_reverse=false] forces inserting the field as DESC
605      * @returns {String|undefined}
606      */
607     sort: function (field, force_reverse) {
608         if (!field) {
609             return instance.web.serialize_sort(this._sort);
610         }
611         var reverse = force_reverse || (this._sort[0] === field);
612         this._sort.splice.apply(
613             this._sort, [0, this._sort.length].concat(
614                 _.without(this._sort, field, '-' + field)));
615
616         this._sort.unshift((reverse ? '-' : '') + field);
617         return undefined;
618     },
619     size: function () {
620         return this.ids.length;
621     },
622     alter_ids: function(n_ids) {
623         this.ids = n_ids;
624     },
625     remove_ids: function (ids) {
626         this.alter_ids(_(this.ids).difference(ids));
627     },
628     add_ids: function(ids, at) {
629         var args = [at, 0].concat(_.difference(ids, this.ids));
630         this.ids.splice.apply(this.ids, args);
631     },
632     /**
633      * Resequence records.
634      *
635      * @param {Array} ids identifiers of the records to resequence
636      * @returns {$.Deferred}
637      */
638     resequence: function (ids, options) {
639         options = options || {};
640         return instance.session.rpc('/web/dataset/resequence', {
641             model: this.model,
642             ids: ids,
643             context: instance.web.pyeval.eval(
644                 'context', this.get_context(options.context)),
645         }).then(function (results) {
646             return results;
647         });
648     },
649 });
650
651 instance.web.DataSetStatic =  instance.web.DataSet.extend({
652     init: function(parent, model, context, ids) {
653         var self = this;
654         this._super(parent, model, context);
655         // all local records
656         this.ids = ids || [];
657     },
658     read_slice: function (fields, options) {
659         options = options || {};
660         fields = fields || {};
661         var offset = options.offset || 0,
662             limit = options.limit || false;
663         var end_pos = limit && limit !== -1 ? offset + limit : this.ids.length;
664         return this.read_ids(this.ids.slice(offset, end_pos), fields);
665     },
666     set_ids: function (ids) {
667         this.ids = ids;
668         if (ids.length === 0) {
669             this.index = null;
670         } else if (this.index >= ids.length - 1) {
671             this.index = ids.length - 1;
672         }
673     },
674     unlink: function(ids) {
675         this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
676         this.trigger('unlink', ids);
677         return $.Deferred().resolve({result: true});
678     },
679 });
680
681 instance.web.DataSetSearch =  instance.web.DataSet.extend({
682     /**
683      * @constructs instance.web.DataSetSearch
684      * @extends instance.web.DataSet
685      *
686      * @param {Object} parent
687      * @param {String} model
688      * @param {Object} context
689      * @param {Array} domain
690      */
691     init: function(parent, model, context, domain) {
692         this._super(parent, model, context);
693         this.domain = domain || [];
694         this._length = null;
695         this.ids = [];
696         this._model = new instance.web.Model(model, context, domain);
697     },
698     /**
699      * Read a slice of the records represented by this DataSet, based on its
700      * domain and context.
701      *
702      * @params {Object} options
703      * @param {Array} [options.fields] fields to read and return, by default all fields are returned
704      * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
705      * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
706      * @param {Number} [options.offset=0] The index from which selected records should be returned
707      * @param {Number} [options.limit=null] The maximum number of records to return
708      * @returns {$.Deferred}
709      */
710     read_slice: function (fields, options) {
711         options = options || {};
712         var self = this;
713         var q = this._model.query(fields || false)
714             .filter(options.domain)
715             .context(options.context)
716             .offset(options.offset || 0)
717             .limit(options.limit || false);
718         q = q.order_by.apply(q, this._sort);
719
720         return q.all().done(function (records) {
721             // FIXME: not sure about that one, *could* have discarded count
722             q.count().done(function (count) { self._length = count; });
723             self.ids = _(records).pluck('id');
724         });
725     },
726     get_domain: function (other_domain) {
727         return this._model.domain(other_domain);
728     },
729     alter_ids: function (ids) {
730         this._super(ids);
731         if (this.index !== null && this.index >= this.ids.length) {
732             this.index = this.ids.length > 0 ? this.ids.length - 1 : 0;
733         }
734     },
735     remove_ids: function (ids) {
736         var before = this.ids.length;
737         this._super(ids);
738         if (this._length) {
739             this._length -= (before - this.ids.length);
740         }
741     },
742     unlink: function(ids, callback, error_callback) {
743         var self = this;
744         return this._super(ids).done(function(result) {
745             self.remove_ids( ids);
746             self.trigger("dataset_changed", ids, callback, error_callback);
747         });
748     },
749     size: function () {
750         if (this._length != null) {
751             return this._length;
752         }
753         return this._super();
754     }
755 });
756
757 instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
758     virtual_id_prefix: "one2many_v_id_",
759     debug_mode: true,
760     init: function() {
761         this._super.apply(this, arguments);
762         this.reset_ids([]);
763         this.last_default_get = {};
764     },
765     default_get: function(fields, options) {
766         var self = this;
767         return this._super(fields, options).done(function(res) {
768             self.last_default_get = res;
769         });
770     },
771     create: function(data, options) {
772         var cached = {
773             id:_.uniqueId(this.virtual_id_prefix),
774             values: _.extend({}, data, (options || {}).readonly_fields || {}),
775             defaults: this.last_default_get
776         };
777         this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)}));
778         this.cache.push(cached);
779         return $.Deferred().resolve(cached.id).promise();
780     },
781     write: function (id, data, options) {
782         var self = this;
783         var record = _.detect(this.to_create, function(x) {return x.id === id;});
784         record = record || _.detect(this.to_write, function(x) {return x.id === id;});
785         var dirty = false;
786         if (record) {
787             for (var k in data) {
788                 if (record.values[k] === undefined || record.values[k] !== data[k]) {
789                     dirty = true;
790                     break;
791                 }
792             }
793             $.extend(record.values, data);
794         } else {
795             dirty = true;
796             record = {id: id, values: data};
797             self.to_write.push(record);
798         }
799         var cached = _.detect(this.cache, function(x) {return x.id === id;});
800         if (!cached) {
801             cached = {id: id, values: {}};
802             this.cache.push(cached);
803         }
804         $.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
805         if (dirty)
806             this.trigger("dataset_changed", id, data, options);
807         return $.Deferred().resolve(true).promise();
808     },
809     unlink: function(ids, callback, error_callback) {
810         var self = this;
811         _.each(ids, function(id) {
812             if (! _.detect(self.to_create, function(x) { return x.id === id; })) {
813                 self.to_delete.push({id: id})
814             }
815         });
816         this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);});
817         this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);});
818         this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);});
819         this.set_ids(_.without.apply(_, [this.ids].concat(ids)));
820         this.trigger("dataset_changed", ids, callback, error_callback);
821         return $.async_when({result: true}).done(callback);
822     },
823     reset_ids: function(ids) {
824         this.set_ids(ids);
825         this.to_delete = [];
826         this.to_create = [];
827         this.to_write = [];
828         this.cache = [];
829         this.delete_all = false;
830     },
831     read_ids: function (ids, fields, options) {
832         var self = this;
833         var to_get = [];
834         _.each(ids, function(id) {
835             var cached = _.detect(self.cache, function(x) {return x.id === id;});
836             var created = _.detect(self.to_create, function(x) {return x.id === id;});
837             if (created) {
838                 _.each(fields, function(x) {if (cached.values[x] === undefined)
839                     cached.values[x] = created.defaults[x] || false;});
840             } else {
841                 if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined}))
842                     to_get.push(id);
843             }
844         });
845         var completion = $.Deferred();
846         var return_records = function() {
847             var records = _.map(ids, function(id) {
848                 return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id});
849             });
850             if (self.debug_mode) {
851                 if (_.include(records, undefined)) {
852                     throw "Record not correctly loaded";
853                 }
854             }
855             var sort_fields = self._sort,
856                     compare = function (v1, v2) {
857                         return (v1 < v2) ? -1
858                              : (v1 > v2) ? 1
859                              : 0;
860                     };
861             // Array.sort is not necessarily stable. We must be careful with this because
862             // sorting an array where all items are considered equal is a worst-case that
863             // will randomize the array with an unstable sort! Therefore we must avoid
864             // sorting if there are no sort_fields (i.e. all items are considered equal)
865             // See also: http://ecma262-5.com/ELS5_Section_15.htm#Section_15.4.4.11 
866             //           http://code.google.com/p/v8/issues/detail?id=90
867             if (sort_fields.length) {
868                 records.sort(function (a, b) {
869                     return _.reduce(sort_fields, function (acc, field) {
870                         if (acc) { return acc; }
871                         var sign = 1;
872                         if (field[0] === '-') {
873                             sign = -1;
874                             field = field.slice(1);
875                         }
876                         return sign * compare(a[field], b[field]);
877                     }, 0);
878                 });
879             }
880             completion.resolve(records);
881         };
882         if(to_get.length > 0) {
883             var rpc_promise = this._super(to_get, fields, options).done(function(records) {
884                 _.each(records, function(record, index) {
885                     var id = to_get[index];
886                     var cached = _.detect(self.cache, function(x) {return x.id === id;});
887                     if (!cached) {
888                         self.cache.push({id: id, values: record});
889                     } else {
890                         // I assume cache value is prioritary
891                         cached.values = _.defaults(_.clone(cached.values), record);
892                     }
893                 });
894                 return_records();
895             });
896             $.when(rpc_promise).fail(function() {completion.reject();});
897         } else {
898             return_records();
899         }
900         return completion.promise();
901     },
902     /**
903      * Invalidates caching of a record in the dataset to ensure the next read
904      * of that record will hit the server.
905      *
906      * Of use when an action is going to remote-alter a record which will then
907      * need to be reloaded, e.g. action button.
908      *
909      * @param {Object} id record to remove from the BDS's cache
910      */
911     evict_record: function (id) {
912         for(var i=0, len=this.cache.length; i<len; ++i) {
913             var record = this.cache[i];
914             // if record we call the button upon is in the cache
915             if (record.id === id) {
916                 // evict it so it gets reloaded from server
917                 this.cache.splice(i, 1);
918                 break;
919             }
920         }
921     },
922     call_button: function (method, args) {
923         this.evict_record(args[0][0]);
924         return this._super(method, args);
925     },
926     exec_workflow: function (id, signal) {
927         this.evict_record(id);
928         return this._super(id, signal);
929     },
930     alter_ids: function(n_ids) {
931         this._super(n_ids);
932         this.trigger("dataset_changed", n_ids);
933     },
934 });
935 instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
936
937 instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
938     init: function() {
939         this._super.apply(this, arguments);
940         this.create_function = null;
941         this.write_function = null;
942         this.read_function = null;
943         this.default_get_function = null;
944         this.unlink_function = null;
945     },
946     read_ids: function (ids, fields, options) {
947         if (this.read_function) {
948             return this.read_function(ids, fields, options, this._super);
949         } else {
950             return this._super.apply(this, arguments);
951         }
952     },
953     default_get: function(fields, options) {
954         if (this.default_get_function) {
955             return this.default_get_function(fields, options, this._super);
956         } else {
957             return this._super.apply(this, arguments);
958         }
959     },
960     create: function(data, options) {
961         if (this.create_function) {
962             return this.create_function(data, options, this._super);
963         } else {
964             return this._super.apply(this, arguments);
965         }
966     },
967     write: function (id, data, options) {
968         if (this.write_function) {
969             return this.write_function(id, data, options, this._super);
970         } else {
971             return this._super.apply(this, arguments);
972         }
973     },
974     unlink: function(ids) {
975         if (this.unlink_function) {
976             return this.unlink_function(ids, this._super);
977         } else {
978             return this._super.apply(this, arguments);
979         }
980     },
981 });
982
983 instance.web.CompoundContext = instance.web.Class.extend({
984     init: function () {
985         this.__ref = "compound_context";
986         this.__contexts = [];
987         this.__eval_context = null;
988         var self = this;
989         _.each(arguments, function(x) {
990             self.add(x);
991         });
992     },
993     add: function (context) {
994         this.__contexts.push(context);
995         return this;
996     },
997     set_eval_context: function (eval_context) {
998         this.__eval_context = eval_context;
999         return this;
1000     },
1001     get_eval_context: function () {
1002         return this.__eval_context;
1003     },
1004     eval: function() {
1005         return instance.web.pyeval.eval('context', this, undefined, {no_user_context: true});
1006     },
1007 });
1008
1009 instance.web.CompoundDomain = instance.web.Class.extend({
1010     init: function () {
1011         this.__ref = "compound_domain";
1012         this.__domains = [];
1013         this.__eval_context = null;
1014         var self = this;
1015         _.each(arguments, function(x) {
1016             self.add(x);
1017         });
1018     },
1019     add: function(domain) {
1020         this.__domains.push(domain);
1021         return this;
1022     },
1023     set_eval_context: function(eval_context) {
1024         this.__eval_context = eval_context;
1025         return this;
1026     },
1027     get_eval_context: function() {
1028         return this.__eval_context;
1029     },
1030     eval: function() {
1031         return instance.web.pyeval.eval('domain', this);
1032     },
1033 });
1034
1035 instance.web.DropMisordered = instance.web.Class.extend({
1036     /**
1037      * @constructs instance.web.DropMisordered
1038      * @extends instance.web.Class
1039      *
1040      * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored
1041      */
1042     init: function (failMisordered) {
1043         // local sequence number, for requests sent
1044         this.lsn = 0;
1045         // remote sequence number, seqnum of last received request
1046         this.rsn = -1;
1047         this.failMisordered = failMisordered || false;
1048     },
1049     /**
1050      * Adds a deferred (usually an async request) to the sequencer
1051      *
1052      * @param {$.Deferred} deferred to ensure add
1053      * @returns {$.Deferred}
1054      */
1055     add: function (deferred) {
1056         var res = $.Deferred();
1057
1058         var self = this, seq = this.lsn++;
1059         deferred.done(function () {
1060             if (seq > self.rsn) {
1061                 self.rsn = seq;
1062                 res.resolve.apply(res, arguments);
1063             } else if (self.failMisordered) {
1064                 res.reject();
1065             }
1066         }).fail(function () {
1067             res.reject.apply(res, arguments);
1068         });
1069
1070         return res.promise();
1071     }
1072 });
1073
1074 };
1075
1076 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: