summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/framework/data.js
diff options
context:
space:
mode:
authorGéry Debongnie <ged@odoo.com>2017-03-05 10:05:56 +0100
committerAaron Bohy <aab@odoo.com>2017-04-11 19:44:38 +0200
commit905e01921f3c3ef43ace6fc537d1d8c0d280002c (patch)
tree265c43ab21c5401a5d9f0f3d64d86f1ac417451c /addons/web/static/src/js/framework/data.js
parent54d4c73af4ad05c7640d28c1892247c9538b8017 (diff)
[REF] web, *: redesign all JS views
This commit introduce a full redesign of all JS views. We started basically from scratch. The goal was to unify all the various views under a common framework, to make them testable, to make then usable in different conditions (in studio, or in the frontend), and to make our lives easier. Some important points are: - we introduced new coding guidelines (camelCase, 80 chars width, ...) - we have a brand new testing framework (still QUnit based) - kanban view moved to the web addon - calendar view (formerly web_calender) moved to web as well - the tree view was removed - all new code should be documented We hope that this code is the start of a new era for the Odoo web client, we want to have a high quality codebase, well documented, well tested, well designed. Work done by the framework team: mostly aab, ged, chm, dmo, qsm
Diffstat (limited to 'addons/web/static/src/js/framework/data.js')
-rw-r--r--addons/web/static/src/js/framework/data.js1135
1 files changed, 0 insertions, 1135 deletions
diff --git a/addons/web/static/src/js/framework/data.js b/addons/web/static/src/js/framework/data.js
deleted file mode 100644
index 1d4258019ae..00000000000
--- a/addons/web/static/src/js/framework/data.js
+++ /dev/null
@@ -1,1135 +0,0 @@
-odoo.define('web.data', function (require) {
-"use strict";
-
-var Class = require('web.Class');
-var mixins = require('web.mixins');
-var Model = require('web.Model');
-var session = require('web.session');
-var translation = require('web.translation');
-var pyeval = require('web.pyeval');
-var utils = require('web.utils');
-
-var _t = translation._t;
-
-/**
- * Serializes the sort criterion array of a dataset into a form which can be
- * consumed by OpenERP's RPC APIs.
- *
- * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting
- * @returns {String} SQL-like sorting string (``ORDER BY``) clause
- */
-function serialize_sort(criterion) {
- return _.map(criterion,
- function (criteria) {
- if (criteria[0] === '-') {
- return criteria.slice(1) + ' DESC';
- }
- return criteria + ' ASC';
- }).join(', ');
-}
-
-/**
- * Reverse of the serialize_sort function: convert an array of SQL-like sort
- * descriptors into a list of fields prefixed with '-' if necessary.
- */
-function deserialize_sort(criterion) {
- return _.map(criterion, function (criteria) {
- var split = _.without(criteria.split(' '), '');
- return (split[1] && split[1].toLowerCase() === 'desc' ? '-' : '') + split[0];
- });
-}
-
-var Query = Class.extend({
- init: function (model, fields) {
- this._model = model;
- this._fields = fields;
- this._filter = [];
- this._context = {};
- this._lazy = true;
- this._limit = false;
- this._offset = 0;
- this._order_by = [];
- },
- clone: function (to_set) {
- to_set = to_set || {};
- var q = new Query(this._model, this._fields);
- q._context = this._context;
- q._filter = this._filter;
- q._lazy = this._lazy;
- q._limit = this._limit;
- q._offset = this._offset;
- q._order_by = this._order_by;
-
- for(var key in to_set) {
- if (!to_set.hasOwnProperty(key)) { continue; }
- switch(key) {
- case 'filter':
- q._filter = new CompoundDomain(
- q._filter, to_set.filter);
- break;
- case 'context':
- q._context = new CompoundContext(
- q._context, to_set.context);
- break;
- case 'lazy':
- case 'limit':
- case 'offset':
- case 'order_by':
- q['_' + key] = to_set[key];
- }
- }
- return q;
- },
- _execute: function (options) {
- var self = this;
- options = options || {};
- return session.rpc('/web/dataset/search_read', {
- model: this._model.name,
- fields: this._fields || false,
- domain: pyeval.eval('domains',
- [this._model.domain(this._filter)]),
- context: pyeval.eval('contexts',
- [this._model.context(this._context)]),
- offset: this._offset,
- limit: this._limit,
- sort: serialize_sort(this._order_by)
- }, options).then(function (results) {
- self._count = results.length;
- return results.records;
- }, null);
- },
- /**
- * Fetches the first record matching the query, or null
- *
- * @param {Object} [options] additional options for the rpc() method
- * @returns {jQuery.Deferred<Object|null>}
- */
- first: function (options) {
- var self = this;
- return this.clone({limit: 1})._execute(options).then(function (records) {
- delete self._count;
- if (records.length) { return records[0]; }
- return null;
- });
- },
- /**
- * Fetches all records matching the query
- *
- * @param {Object} [options] additional options for the rpc() method
- * @returns {jQuery.Deferred<Array<>>}
- */
- all: function (options) {
- return this._execute(options);
- },
- /**
- * Fetches the number of records matching the query in the database
- *
- * @returns {jQuery.Deferred<Number>}
- */
- count: function () {
- if (this._count !== undefined) { return $.when(this._count); }
- return this._model.call(
- 'search_count', [this._filter], {
- context: this._model.context(this._context)});
- },
- /**
- * Performs a groups read according to the provided grouping criterion
- *
- * @param {String|Array<String>} grouping
- * @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null}
- */
- group_by: function (grouping) {
- var ctx = pyeval.eval(
- 'context', this._model.context(this._context));
-
- // undefined passed in explicitly (!)
- if (_.isUndefined(grouping)) {
- grouping = [];
- }
-
- if (!(grouping instanceof Array)) {
- grouping = _.toArray(arguments);
- }
- if (_.isEmpty(grouping) && !ctx.group_by_no_leaf) {
- return null;
- }
- var raw_fields = _.map(grouping.concat(this._fields || []), function (field) {
- return field.split(':')[0];
- });
-
- var self = this;
- return this._model.call('read_group', {
- groupby: grouping,
- fields: _.uniq(raw_fields),
- domain: this._model.domain(this._filter),
- context: ctx,
- offset: this._offset,
- lazy: this._lazy,
- limit: this._limit,
- orderby: serialize_sort(this._order_by) || false
- }).then(function (results) {
- return _(results).map(function (result) {
- // FIX: querygroup initialization
- result.__context = result.__context || {};
- result.__context.group_by = result.__context.group_by || [];
- _.defaults(result.__context, ctx);
- var grouping_fields = self._lazy ? [grouping[0]] : grouping;
- return new QueryGroup(
- self._model.name, grouping_fields, result);
- });
- });
- },
- /**
- * Creates a new query with the union of the current query's context and
- * the new context.
- *
- * @param context context data to add to the query
- * @returns {openerp.web.Query}
- */
- context: function (context) {
- if (!context) { return this; }
- return this.clone({context: context});
- },
- /**
- * Creates a new query with the union of the current query's filter and
- * the new domain.
- *
- * @param domain domain data to AND with the current query filter
- * @returns {openerp.web.Query}
- */
- filter: function (domain) {
- if (!domain) { return this; }
- return this.clone({filter: domain});
- },
- /**
- * Creates a new query with the provided parameter lazy replacing the current
- * query's own.
- *
- * @param {Boolean} lazy indicates if the read_group should return only the
- * first level of groupby records, or should return the records grouped by
- * all levels at once (so, it makes only 1 db request).
- * @returns {openerp.web.Query}
- */
- lazy: function (lazy) {
- return this.clone({lazy: lazy});
- },
- /**
- * Creates a new query with the provided limit replacing the current
- * query's own limit
- *
- * @param {Number} limit maximum number of records the query should retrieve
- * @returns {openerp.web.Query}
- */
- limit: function (limit) {
- return this.clone({limit: limit});
- },
- /**
- * Creates a new query with the provided offset replacing the current
- * query's own offset
- *
- * @param {Number} offset number of records the query should skip before starting its retrieval
- * @returns {openerp.web.Query}
- */
- offset: function (offset) {
- return this.clone({offset: offset});
- },
- /**
- * Creates a new query with the provided ordering parameters replacing
- * those of the current query
- *
- * @param {String...} fields ordering clauses
- * @returns {openerp.web.Query}
- */
- order_by: function (fields) {
- if (fields === undefined) { return this; }
- if (!(fields instanceof Array)) {
- fields = _.toArray(arguments);
- }
- if (_.isEmpty(fields)) { return this; }
- return this.clone({order_by: fields});
- }
-});
-
-var QueryGroup = Class.extend({
- init: function (model, grouping_fields, read_group_group) {
- // In cases where group_by_no_leaf and no group_by, the result of
- // read_group has aggregate fields but no __context or __domain.
- // Create default (empty) values for those so that things don't break
- var fixed_group = _.extend(
- {__context: {group_by: []}, __domain: []},
- read_group_group);
-
- var count_key = (grouping_fields[0] && grouping_fields[0].split(':')[0]) + '_count';
- var aggregates = {};
- _(fixed_group).each(function (value, key) {
- if (key.indexOf('__') === 0
- || _.contains(grouping_fields, key)
- || (key === count_key)) {
- return;
- }
- aggregates[key] = value || 0;
- });
-
- this.model = new Model(
- model, fixed_group.__context, fixed_group.__domain);
-
- var group_size = fixed_group[count_key] || fixed_group.__count || 0;
- var leaf_group = fixed_group.__context.group_by.length === 0;
-
- var value = (grouping_fields.length === 1)
- ? fixed_group[grouping_fields[0]]
- : _.map(grouping_fields, function (field) { return fixed_group[field]; });
- var grouped_on = (grouping_fields.length === 1)
- ? grouping_fields[0]
- : grouping_fields;
- this.attributes = {
- folded: !!(fixed_group.__fold),
- grouped_on: grouped_on,
- // if terminal group (or no group) and group_by_no_leaf => use group.__count
- length: group_size,
- value: value,
- // A group is open-able if it's not a leaf in group_by_no_leaf mode
- has_children: !(leaf_group && fixed_group.__context.group_by_no_leaf),
-
- aggregates: aggregates
- };
- },
- get: function (key) {
- return this.attributes[key];
- },
- subgroups: function () {
- return this.model.query().group_by(this.model.context().group_by);
- },
- query: function () {
- return this.model.query.apply(this.model, arguments);
- }
-});
-
-var DataSet = Class.extend(mixins.PropertiesMixin, {
- /**
- * Collection of OpenERP records, used to share records and the current selection between views.
- *
- * @constructs instance.web.DataSet
- *
- * @param {String} model the OpenERP model this dataset will manage
- */
- init: function(parent, model, context) {
- mixins.PropertiesMixin.init.call(this);
- this.model = model;
- this.context = context || {};
- this.index = null;
- this._sort = [];
- this._model = new Model(model, context);
- this.orderer = new utils.DropMisordered();
- },
- previous: function () {
- this.index -= 1;
- if (!this.ids.length) {
- this.index = null;
- } else if (this.index < 0) {
- this.index = this.ids.length - 1;
- }
- return this;
- },
- next: function () {
- this.index += 1;
- if (!this.ids.length) {
- this.index = null;
- } else if (this.index >= this.ids.length) {
- this.index = 0;
- }
- return this;
- },
- select_id: function(id) {
- var idx = this.get_id_index(id);
- if (idx === null) {
- return false;
- } else {
- this.index = idx;
- return true;
- }
- },
- get_id_index: function(id) {
- for (var i=0, ii=this.ids.length; i<ii; i++) {
- // Here we use type coercion because of the mess potentially caused by
- // OpenERP ids fetched from the DOM as string. (eg: dhtmlxcalendar)
- // OpenERP ids can be non-numeric too ! (eg: recursive events in calendar)
- if (id == this.ids[i]) {
- return i;
- }
- }
- return null;
- },
- /**
- * Read records.
- *
- * @param {Array} ids identifiers of the records to read
- * @param {Array} [fields] fields to read and return, by default all fields are returned
- * @param {Object} [options]
- * @returns {$.Deferred}
- */
- read_ids: function (ids, fields, options) {
- if (_.isEmpty(ids))
- return $.Deferred().resolve([]);
-
- options = options || {};
- var method = 'read';
- var ids_arg = ids;
- var context = this.get_context(options.context);
- if (options.check_access_rule === true){
- method = 'search_read';
- ids_arg = [['id', 'in', ids]];
- context = new CompoundContext(context, {active_test: false});
- }
- return this._model.call(method,
- [ids_arg, fields || false],
- {context: context})
- .then(function (records) {
- if (records.length <= 1) { return records; }
- var indexes = {};
- for (var i = 0; i < ids.length; i++) {
- indexes[ids[i]] = i;
- }
- records.sort(function (a, b) {
- return indexes[a.id] - indexes[b.id];
- });
- return records;
- });
- },
- /**
- * Read a slice of the records represented by this DataSet, based on its
- * domain and context.
- *
- * @param {Array} [fields] fields to read and return, by default all fields are returned
- * @params {Object} [options]
- * @param {Number} [options.offset=0] The index from which selected records should be returned
- * @param {Number} [options.limit=null] The maximum number of records to return
- * @returns {$.Deferred}
- */
- read_slice: function (fields, options) {
- var self = this;
- options = options || {};
- var query = this._model.query(fields)
- .limit(options.limit || false)
- .offset(options.offset || 0)
- .all();
- return this.orderer.add(query).done(function (records) {
- self.ids = _(records).pluck('id');
- });
- },
- /**
- * Reads the current dataset record (from its index)
- *
- * @params {Array} [fields] fields to read and return, by default all fields are returned
- * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
- * @returns {$.Deferred}
- */
- read_index: function (fields, options) {
- options = options || {};
- return this.read_ids([this.ids[this.index]], fields, options).then(function (records) {
- if (_.isEmpty(records)) { return $.Deferred().reject().promise(); }
- return records[0];
- });
- },
- /**
- * Reads default values for the current model
- *
- * @param {Array} [fields] fields to get default values for, by default all defaults are read
- * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
- * @returns {$.Deferred}
- */
- default_get: function(fields, options) {
- options = options || {};
- return this._model.call('default_get',
- [fields], {context: this.get_context(options.context)});
- },
- /**
- * Creates a new record in db
- *
- * @param {Object} data field values to set on the new record
- * @param {Object} options Dictionary that can contain the following keys:
- * - readonly_fields: Values from readonly fields that were updated by
- * on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
- * @returns {$.Deferred}
- */
- create: function(data, options) {
- var self = this;
- return this._model.call('create', [data], {
- context: this.get_context()
- }).done(function () {
- self.trigger('dataset_changed', data, options);
- });
- },
- /**
- * Saves the provided data in an existing db record
- *
- * @param {Number|String} id identifier for the record to alter
- * @param {Object} data field values to write into the record
- * @param {Object} options Dictionary that can contain the following keys:
- * - context: The context to use in the server-side call.
- * - readonly_fields: Values from readonly fields that were updated by
- * on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
- * @returns {$.Deferred}
- */
- write: function (id, data, options) {
- options = options || {};
- var self = this;
- return this._model.call('write', [[id], data], {
- context: this.get_context(options.context)
- }).done(function () {
- self.trigger('dataset_changed', id, data, options);
- });
- },
- /**
- * Deletes an existing record from the database
- *
- * @param {Number|String} ids identifier of the record to delete
- */
- unlink: function(ids) {
- var self = this;
- return this._model.call('unlink', [ids], {
- context: this.get_context()
- }).done(function () {
- self.trigger('dataset_changed', ids);
- });
- },
- /**
- * Calls an arbitrary RPC method
- *
- * @param {String} method name of the method (on the current model) to call
- * @param {Array} [args] arguments to pass to the method
- * @param {Function} callback
- * @param {Function} error_callback
- * @returns {$.Deferred}
- */
- call: function (method, args) {
- return this._model.call(method, args);
- },
- /**
- * Calls a button method, usually returning some sort of action
- *
- * @param {String} method
- * @param {Array} [args]
- * @returns {$.Deferred}
- */
- call_button: function (method, args) {
- return this._model.call_button(method, args);
- },
- /**
- * Fetches the "readable name" for records, based on intrinsic rules
- *
- * @param {Array} ids
- * @returns {$.Deferred}
- */
- name_get: function(ids) {
- return this._model.call('name_get', [ids], {context: this.get_context()});
- },
- /**
- *
- * @param {String} name name to perform a search for/on
- * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
- * @param {String} [operator='ilike'] matching operator to use with the provided name value
- * @param {Number} [limit=0] maximum number of matches to return
- * @param {Function} callback function to call with name_search result
- * @returns {$.Deferred}
- */
- name_search: function (name, domain, operator, limit) {
- return this._model.call('name_search', {
- name: name || '',
- args: domain || false,
- operator: operator || 'ilike',
- context: this._model.context(),
- limit: limit || 0
- });
- },
- /**
- * @param name
- */
- name_create: function(name, context) {
- return this._model.call('name_create', [name], {context: this.get_context(context)});
- },
- get_context: function(request_context) {
- return this._model.context(request_context);
- },
- /**
- * Reads or changes sort criteria on the dataset.
- *
- * If not provided with any argument, serializes the sort criteria to
- * an SQL-like form usable by OpenERP's ORM.
- *
- * If given a field, will set that field as first sorting criteria or,
- * if the field is already the first sorting criteria, will reverse it.
- *
- * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
- * @param {Boolean} [force_reverse=false] forces inserting the field as DESC
- * @returns {String|undefined}
- */
- sort: function (field, force_reverse) {
- if (!field) {
- return serialize_sort(this._sort);
- }
- var reverse = force_reverse || (this._sort[0] === field);
- this._sort.splice.apply(
- this._sort, [0, this._sort.length].concat(
- _.without(this._sort, field, '-' + field)));
-
- this._sort.unshift((reverse ? '-' : '') + field);
- return undefined;
- },
- /**
- * Set the sort criteria on the dataset.
- *
- * @param {Array} fields_list: list of fields order descriptors, as used by
- * Odoo's ORM (such as 'name desc', 'product_id', 'order_date asc')
- */
- set_sort: function (fields_list) {
- this._sort = deserialize_sort(fields_list);
- },
- size: function () {
- return this.ids.length;
- },
- alter_ids: function(n_ids) {
- this.ids = n_ids;
- },
- remove_ids: function (ids) {
- this.alter_ids(_(this.ids).difference(ids));
- },
- add_ids: function(ids, at) {
- var args = [at, 0].concat(_.difference(ids, this.ids));
- this.ids.splice.apply(this.ids, args);
- },
- /**
- * Resequence records.
- *
- * @param {Array} ids identifiers of the records to resequence
- * @returns {$.Deferred}
- */
- resequence: function (ids, options) {
- options = options || {};
- return session.rpc('/web/dataset/resequence', {
- model: this.model,
- ids: ids,
- context: pyeval.eval(
- 'context', this.get_context(options.context)),
- }).then(function (results) {
- return results;
- });
- },
-});
-
-var DataSetStatic = DataSet.extend({
- init: function(parent, model, context, ids) {
- this._super(parent, model, context);
- // all local records
- this.ids = ids || [];
- },
- read_slice: function (fields, options) {
- options = options || {};
- fields = fields || {};
- var offset = options.offset || 0,
- limit = options.limit || false;
- var end_pos = limit && limit !== -1 ? offset + limit : this.ids.length;
- return this.read_ids(this.ids.slice(offset, end_pos), fields, options);
- },
- set_ids: function (ids) {
- this.ids = ids;
- if (ids.length === 0) {
- this.index = null;
- } else if (this.index >= ids.length - 1) {
- this.index = ids.length - 1;
- }
- },
- unlink: function(ids) {
- this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
- this.trigger('unlink', ids);
- return $.Deferred().resolve({result: true});
- },
-});
-
-var DataSetSearch = DataSet.extend({
- /**
- * @constructs instance.web.DataSetSearch
- * @extends instance.web.DataSet
- *
- * @param {Object} parent
- * @param {String} model
- * @param {Object} context
- * @param {Array} domain
- */
- init: function(parent, model, context, domain) {
- this._super(parent, model, context);
- this.domain = domain || [];
- this._length = null;
- this.ids = [];
- this._model = new Model(model, context, domain);
- },
- /**
- * Read a slice of the records represented by this DataSet, based on its
- * domain and context.
- *
- * @params {Object} options
- * @param {Array} [options.fields] fields to read and return, by default all fields are returned
- * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
- * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
- * @param {Number} [options.offset=0] The index from which selected records should be returned
- * @param {Number} [options.limit=null] The maximum number of records to return
- * @returns {$.Deferred}
- */
- read_slice: function (fields, options) {
- options = options || {};
- var self = this;
- var q = this._model.query(fields || false)
- .filter(options.domain)
- .context(options.context)
- .offset(options.offset || 0)
- .limit(options.limit || false);
- q = q.order_by.apply(q, this._sort);
-
- return this.orderer.add(q.all()).done(function (records) {
- // FIXME: not sure about that one, *could* have discarded count
- q.count().done(function (count) { self._length = count; });
- self.ids = _(records).pluck('id');
- });
- },
- get_domain: function (other_domain) {
- return this._model.domain(other_domain);
- },
- alter_ids: function (ids) {
- this._super(ids);
- if (this.index !== null && this.index >= this.ids.length) {
- this.index = this.ids.length > 0 ? this.ids.length - 1 : 0;
- }
- },
- remove_ids: function (ids) {
- var before = this.ids.length;
- this._super(ids);
- if (this._length) {
- this._length -= (before - this.ids.length);
- }
- },
- add_ids: function(ids, at) {
- var before = this.ids.length;
- this._super(ids, at);
- if(this._length){
- this._length += (this.ids.length - before);
- }
- },
- unlink: function(ids, callback, error_callback) {
- var self = this;
- return this._super(ids).done(function() {
- self.remove_ids( ids);
- self.trigger("dataset_changed", ids, callback, error_callback);
- });
- },
- size: function () {
- if (this._length !== null) {
- return this._length;
- }
- return this._super();
- }
-});
-
-var BufferedDataSet = DataSetStatic.extend({
- virtual_id_prefix: "one2many_v_id_",
- debug_mode: true,
- init: function() {
- this._super.apply(this, arguments);
- this.reset_ids([]);
- this.last_default_get = {};
- this.running_reads = [];
- this.mutex = new utils.Mutex();
- },
- default_get: function(fields, options) {
- var self = this;
- return this._super(fields, options).done(function(res) {
- self.last_default_get = _.clone(res);
- });
- },
- get_cache: function (id) {
- if (!this.cache[id]) {
- this.cache[id] = {
- 'id': id,
- 'values': {},
- 'from_read': {},
- 'changes': {},
- 'readonly_fields': {},
- 'to_create': false,
- 'to_delete': false};
- }
- return this.cache[id];
- },
- _update_cache: function (id, options) {
- // One should call this method after modifying this.from_read,
- // this.to_create or this.change. It updates this.cache and
- // this.readonly_fields.
- var cached = this.get_cache(id);
- if (options) {
- _.extend(cached.from_read, options.from_read);
- _.extend(cached.changes, options.changes);
- _.extend(cached.readonly_fields, options.readonly_fields);
- if (options.to_create !== undefined) cached.to_create = options.to_create;
- if (options.to_delete !== undefined) cached.to_delete = options.to_delete;
- }
- cached.values = _.extend({'id': id}, cached.from_read, cached.changes, cached.readonly_fields);
- return cached;
- },
- create: function(data, options) {
- var changes = _.extend({}, this.last_default_get, data);
- var cached = this._update_cache(_.uniqueId(this.virtual_id_prefix), _.extend({'changes': changes, 'to_create': true}, options));
- this.trigger("dataset_changed", data, options);
- return $.Deferred().resolve(cached.id);
- },
- write: function (id, data, options) {
- var self = this;
- var cached = this.get_cache(id);
-
- // if update after a remove, it's like an add before updating
- cached.to_delete = false;
-
- // apply change
- var def = $.Deferred();
- this.mutex.exec(function () {
- var dirty = false;
- // _.each is broken if a field "length" is present
- for (var k in data) {
- if (!_.isEqual(data[k], cached.values[k])) {
- dirty = true;
- if (_.isEqual(data[k], cached.from_read[k])) { // clean changes
- delete cached.changes[k];
- } else {
- cached.changes[k] = data[k];
- }
- } else {
- delete data[k];
- }
- }
- self._update_cache(id, options);
-
- if (dirty) {
- self.trigger("dataset_changed", id, data, options);
- }
-
- return def.resolve(data).promise();
- });
-
- return def;
- },
- unlink: function(ids, callback, error_callback) {
- var self = this;
- _.each(ids, function (id) {
- self.get_cache(id).to_delete = true;
- });
- this.set_ids(_.difference(this.ids, _.pluck(_.filter(this.cache, function (c) {return c.to_delete;}), 'id')));
- this.trigger("dataset_changed", ids, callback, error_callback);
- return utils.async_when({result: true}).done(callback);
- },
- reset_ids: function(ids, options) {
- var self = this;
- this.set_ids(ids);
- if (!options || !options.keep_read_data) {
- this.cache = {};
- } else {
- _.each(this.cache, function (cache) {
- self._update_cache(cache.id, {'changes': {}, 'to_delete': false});
- });
- }
- this.delete_all = false;
- this.cancel_read();
- },
- cancel_read: function () {
- _.invoke(_.clone(this.running_reads), 'reject');
- },
- read_ids: function (ids, fields, options) {
- // read what is necessary from the server to have ids and the given
- // fields in this.from_read
- var self = this;
- var to_get = _.filter(ids, function(id) {
- var cache = self.get_cache(id);
- return !cache.to_create && _.any(fields, function(x) {return cache.from_read[x] === undefined;});
- });
- options = options || {};
-
- var return_records = function() {
- var records = _.map(ids, function(id) {
- return _.clone(self.get_cache(id).values);
- });
- if (self.debug_mode) {
- if (_.include(records, undefined)) {
- throw "Record not correctly loaded";
- }
- }
- var sort_fields = self._sort,
- compare = function (v1, v2) {
- return (v1 < v2) ? -1
- : (v1 > v2) ? 1
- : 0;
- };
- // Array.sort is not necessarily stable. We must be careful with this because
- // sorting an array where all items are considered equal is a worst-case that
- // will randomize the array with an unstable sort! Therefore we must avoid
- // sorting if there are no sort_fields (i.e. all items are considered equal)
- // See also: http://ecma262-5.com/ELS5_Section_15.htm#Section_15.4.4.11
- // http://code.google.com/p/v8/issues/detail?id=90
- if (sort_fields.length) {
- records.sort(function (a, b) {
- return _.reduce(sort_fields, function (acc, field) {
- if (acc) { return acc; }
- var sign = 1;
- if (field[0] === '-') {
- sign = -1;
- field = field.slice(1);
- }
- if(!a[field] && a[field] !== 0){ return sign; }
- if(!b[field] && b[field] !== 0){ return (sign == -1) ? 1 : -1; }
- //m2o should be searched based on value[1] not based whole value(i.e. [id, value])
- if(_.isArray(a[field]) && a[field].length == 2 && _.isString(a[field][1])){
- return sign * compare(a[field][1], b[field][1]);
- }
- return sign * compare(a[field], b[field]);
- }, 0);
- });
- }
- return $.when(records);
- };
- if(to_get.length > 0) {
- var def = $.Deferred();
- self.running_reads.push(def);
- def.always(function() {
- self.running_reads = _.without(self.running_reads, def);
- });
- var _super = this._super;
- this.mutex.exec(function () {
- if (def.state() !== "pending") return;
- _super.call(self, to_get, fields, options).then(_.bind(def.resolve, def), _.bind(def.reject, def));
- return def;
- });
- return def.then(function(records) {
- _.each(records, function(record, index) {
- // add information into from_read
- var id = to_get[index];
- self._update_cache(id, _.extend(options, {'from_read': record}));
- });
- return return_records();
- });
- } else {
- return return_records();
- }
- },
- /**
- * Invalidates caching of a record in the dataset to ensure the next read
- * of that record will hit the server.
- *
- * Of use when an action is going to remote-alter a record which will then
- * need to be reloaded, e.g. action button.
- *
- * @param {Object} id record to remove from the BDS's cache
- */
- evict_record: function (id) {
- // Don't evict records which haven't yet been saved: there is no more
- // recent data on the server (and there potentially isn't any data),
- // and this breaks the assumptions of other methods (that the data
- // for new and altered records is both in the cache and in the change
- // or to_create collection)
- this.get_cache(id).from_read = {};
- this._update_cache(id);
- },
- call_button: function (method, args) {
- this.evict_record(args[0][0]);
- return this._super(method, args);
- },
- alter_ids: function(n_ids, options) {
- var dirty = !_.isEqual(this.ids, n_ids);
- this._super(n_ids, options);
- if (dirty) {
- this.trigger("dataset_changed", n_ids, options);
- }
- },
-});
-
-BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
-
-var ProxyDataSet = DataSetSearch.extend({
- init: function() {
- this._super.apply(this, arguments);
- this.create_function = null;
- this.write_function = null;
- this.read_function = null;
- this.default_get_function = null;
- this.unlink_function = null;
- },
- read_ids: function (ids, fields, options) {
- if (this.read_function) {
- return this.read_function(ids, fields, options, this._super);
- } else {
- return this._super.apply(this, arguments);
- }
- },
- default_get: function(fields, options) {
- if (this.default_get_function) {
- return this.default_get_function(fields, options, this._super);
- } else {
- return this._super.apply(this, arguments);
- }
- },
- create: function(data, options) {
- if (this.create_function) {
- return this.create_function(data, options, this._super);
- } else {
- return this._super.apply(this, arguments);
- }
- },
- write: function (id, data, options) {
- if (this.write_function) {
- return this.write_function(id, data, options, this._super);
- } else {
- return this._super.apply(this, arguments);
- }
- },
- unlink: function(ids) {
- if (this.unlink_function) {
- return this.unlink_function(ids, this._super);
- } else {
- return this._super.apply(this, arguments);
- }
- },
-});
-
-var CompoundContext = Class.extend({
- init: function () {
- this.__ref = "compound_context";
- this.__contexts = [];
- this.__eval_context = null;
- var self = this;
- _.each(arguments, function(x) {
- self.add(x);
- });
- },
- add: function (context) {
- this.__contexts.push(context);
- return this;
- },
- set_eval_context: function (eval_context) {
- this.__eval_context = eval_context;
- return this;
- },
- get_eval_context: function () {
- return this.__eval_context;
- },
- eval: function() {
- return pyeval.eval('context', this, undefined, {no_user_context: true});
- },
-});
-
-var CompoundDomain = Class.extend({
- init: function () {
- this.__ref = "compound_domain";
- this.__domains = [];
- this.__eval_context = null;
- var self = this;
- _.each(arguments, function(x) {
- self.add(x);
- });
- },
- add: function(domain) {
- this.__domains.push(domain);
- return this;
- },
- set_eval_context: function(eval_context) {
- this.__eval_context = eval_context;
- return this;
- },
- get_eval_context: function() {
- return this.__eval_context;
- },
- eval: function() {
- return pyeval.eval('domain', this);
- },
-});
-
-function compute_domain (expr, fields) {
- if (! (expr instanceof Array))
- return !! expr;
- var stack = [];
- for (var i = expr.length - 1; i >= 0; i--) {
- var ex = expr[i];
- if (ex.length == 1) {
- var top = stack.pop();
- switch (ex) {
- case '|':
- stack.push(stack.pop() || top);
- continue;
- case '&':
- stack.push(stack.pop() && top);
- continue;
- case '!':
- stack.push(!top);
- continue;
- default:
- throw new Error(_.str.sprintf(
- _t("Unknown operator %s in domain %s"),
- ex, JSON.stringify(expr)));
- }
- }
-
- var field = fields[ex[0]];
- if (!field) {
- throw new Error(_.str.sprintf(
- _t("Unknown field %s in domain %s"),
- ex[0], JSON.stringify(expr)));
- }
- var field_value = field.get_value ? field.get_value() : field.value;
- var op = ex[1];
- var val = ex[2];
-
- switch (op.toLowerCase()) {
- case '=':
- case '==':
- stack.push(_.isEqual(field_value, val));
- break;
- case '!=':
- case '<>':
- stack.push(!_.isEqual(field_value, val));
- break;
- case '<':
- stack.push(field_value < val);
- break;
- case '>':
- stack.push(field_value > val);
- break;
- case '<=':
- stack.push(field_value <= val);
- break;
- case '>=':
- stack.push(field_value >= val);
- break;
- case 'in':
- if (!_.isArray(val)) val = [val];
- stack.push(_(val).contains(field_value));
- break;
- case 'not in':
- if (!_.isArray(val)) val = [val];
- stack.push(!_(val).contains(field_value));
- break;
- default:
- console.warn(
- _t("Unsupported operator %s in domain %s"),
- op, JSON.stringify(expr));
- }
- }
- return _.all(stack, _.identity);
-}
-
-return {
- Query: Query,
- DataSet: DataSet,
- DataSetStatic: DataSetStatic,
- DataSetSearch: DataSetSearch,
- BufferedDataSet: BufferedDataSet,
- ProxyDataSet: ProxyDataSet,
- CompoundContext: CompoundContext,
- CompoundDomain: CompoundDomain,
- compute_domain: compute_domain,
- noDisplayContent: "<em class=\"text-warning\">" + _t("Unnamed") + "</em>",
-};
-
-});