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