diff options
Diffstat (limited to 'addons/web/static/src/js/data.js')
| -rw-r--r-- | addons/web/static/src/js/data.js | 1344 |
1 files changed, 0 insertions, 1344 deletions
diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js deleted file mode 100644 index 82edbc2f35b..00000000000 --- a/addons/web/static/src/js/data.js +++ /dev/null @@ -1,1344 +0,0 @@ - -(function() { - -var instance = openerp; -openerp.web.data = {}; - -/** - * 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 - */ -instance.web.serialize_sort = function (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. - */ - instance.web.deserialize_sort = function (criterion) { - return _.map(criterion, function (criteria) { - var split = _.without(criteria.split(' '), ''); - return (split[1] && split[1].toLowerCase() === 'desc' ? '-' : '') + split[0]; - }); -}; - -instance.web.Query = instance.web.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 instance.web.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 instance.web.CompoundDomain( - q._filter, to_set.filter); - break; - case 'context': - q._context = new instance.web.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 instance.session.rpc('/web/dataset/search_read', { - model: this._model.name, - fields: this._fields || false, - domain: instance.web.pyeval.eval('domains', - [this._model.domain(this._filter)]), - context: instance.web.pyeval.eval('contexts', - [this._model.context(this._context)]), - offset: this._offset, - limit: this._limit, - sort: instance.web.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 = instance.web.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: instance.web.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 instance.web.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}); - } -}); - -instance.web.QueryGroup = instance.web.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 instance.web.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); - } -}); - -instance.web.Model.include({ - /** - new openerp.web.Model([session,] model_name[, context[, domain]]) - - @constructs instance.web.Model - @extends instance.web.Class - - @param {openerp.web.Session} [session] The session object used to communicate with - the server. - @param {String} model_name name of the OpenERP model this object is bound to - @param {Object} [context] - @param {Array} [domain] - */ - init: function() { - var session, model_name, context, domain; - var args = _.toArray(arguments); - args.reverse(); - session = args.pop(); - if (session && ! (session instanceof openerp.web.Session)) { - model_name = session; - session = null; - } else { - model_name = args.pop(); - } - context = args.length > 0 ? args.pop() : null; - domain = args.length > 0 ? args.pop() : null; - - this._super(session, model_name, context, domain); - this._context = context || {}; - this._domain = domain || []; - }, - session: function() { - if (! this._session) - return instance.session; - return this._super(); - }, - /** - * @deprecated does not allow to specify kwargs, directly use call() instead - */ - get_func: function (method_name) { - var self = this; - return function () { - return self.call(method_name, _.toArray(arguments)); - }; - }, - /** - * Call a method (over RPC) on the bound OpenERP model. - * - * @param {String} method name of the method to call - * @param {Array} [args] positional arguments - * @param {Object} [kwargs] keyword arguments - * @param {Object} [options] additional options for the rpc() method - * @returns {jQuery.Deferred<>} call result - */ - call: function (method, args, kwargs, options) { - args = args || []; - kwargs = kwargs || {}; - if (!_.isArray(args)) { - // call(method, kwargs) - kwargs = args; - args = []; - } - instance.web.pyeval.ensure_evaluated(args, kwargs); - return this._super(method, args, kwargs, options); - }, - /** - * Fetches a Query instance bound to this model, for searching - * - * @param {Array<String>} [fields] fields to ultimately fetch during the search - * @returns {instance.web.Query} - */ - query: function (fields) { - return new instance.web.Query(this, fields); - }, - /** - * Executes a signal on the designated workflow, on the bound OpenERP model - * - * @param {Number} id workflow identifier - * @param {String} signal signal to trigger on the workflow - */ - exec_workflow: function (id, signal) { - return this.session().rpc('/web/dataset/exec_workflow', { - model: this.name, - id: id, - signal: signal - }); - }, - /** - * Fetches the model's domain, combined with the provided domain if any - * - * @param {Array} [domain] to combine with the model's internal domain - * @returns {instance.web.CompoundDomain} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain - */ - domain: function (domain) { - if (!domain) { return this._domain; } - return new instance.web.CompoundDomain( - this._domain, domain); - }, - /** - * Fetches the combination of the user's context and the domain context, - * combined with the provided context if any - * - * @param {Object} [context] to combine with the model's internal context - * @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. - */ - context: function (context) { - return new instance.web.CompoundContext( - instance.session.user_context, this._context, context || {}); - }, - /** - * Button action caller, needs to perform cleanup if an action is returned - * from the button (parsing of context and domain, and fixup of the views - * collection for act_window actions) - * - * FIXME: remove when evaluator integrated - */ - call_button: function (method, args) { - instance.web.pyeval.ensure_evaluated(args, {}); - return this.session().rpc('/web/dataset/call_button', { - model: this.name, - method: method, - // Should not be necessary anymore. Integrate remote in this? - domain_id: null, - context_id: args.length - 1, - args: args || [] - }); - }, -}); - -instance.web.DataSet = instance.web.Class.extend(instance.web.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) { - instance.web.PropertiesMixin.init.call(this); - this.model = model; - this.context = context || {}; - this.index = null; - this._sort = []; - this._model = new instance.web.Model(model, context); - }, - 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 instance.web.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 || {}; - return this._model.query(fields) - .limit(options.limit || false) - .offset(options.offset || 0) - .all().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) { - return this._model.call('name_create', [name], {context: this.get_context()}); - }, - exec_workflow: function (id, signal) { - return this._model.exec_workflow(id, signal); - }, - 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 instance.web.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 = instance.web.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 instance.session.rpc('/web/dataset/resequence', { - model: this.model, - ids: ids, - context: instance.web.pyeval.eval( - 'context', this.get_context(options.context)), - }).then(function (results) { - return results; - }); - }, -}); - -instance.web.DataSetStatic = instance.web.DataSet.extend({ - init: function(parent, model, context, ids) { - var self = this; - 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); - }, - 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}); - }, -}); - -instance.web.DataSetSearch = instance.web.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 instance.web.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 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); - } - }, - unlink: function(ids, callback, error_callback) { - var self = this; - return this._super(ids).done(function(result) { - self.remove_ids( ids); - self.trigger("dataset_changed", ids, callback, error_callback); - }); - }, - size: function () { - if (this._length !== null) { - return this._length; - } - return this._super(); - } -}); - -instance.web.BufferedDataSet = instance.web.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 = []; - }, - default_get: function(fields, options) { - var self = this; - return this._super(fields, options).done(function(res) { - self.last_default_get = res; - }); - }, - create: function(data, options) { - var cached = { - id:_.uniqueId(this.virtual_id_prefix), - values: _.extend({}, data, (options || {}).readonly_fields || {}), - defaults: this.last_default_get - }; - this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)})); - this.cache.push(cached); - return $.Deferred().resolve(cached.id).promise(); - }, - write: function (id, data, options) { - var self = this; - var record = _.detect(this.to_create, function(x) {return x.id === id;}); - record = record || _.detect(this.to_write, function(x) {return x.id === id;}); - var dirty = false; - if (record) { - for (var k in data) { - if (record.values[k] === undefined || record.values[k] !== data[k]) { - dirty = true; - break; - } - } - $.extend(record.values, data); - } else { - dirty = true; - record = {id: id, values: data}; - self.to_write.push(record); - } - var cached = _.detect(this.cache, function(x) {return x.id === id;}); - if (!cached) { - cached = {id: id, values: {}}; - this.cache.push(cached); - } - $.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {})); - if (dirty) - this.trigger("dataset_changed", id, data, options); - return $.Deferred().resolve(true).promise(); - }, - unlink: function(ids, callback, error_callback) { - var self = this; - _.each(ids, function(id) { - if (! _.detect(self.to_create, function(x) { return x.id === id; })) { - self.to_delete.push({id: id}); - } - }); - this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);}); - this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);}); - this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);}); - this.set_ids(_.without.apply(_, [this.ids].concat(ids))); - this.trigger("dataset_changed", ids, callback, error_callback); - return $.async_when({result: true}).done(callback); - }, - reset_ids: function(ids) { - this.set_ids(ids); - this.to_delete = []; - this.to_create = []; - this.to_write = []; - this.cache = []; - this.delete_all = false; - _.each(_.clone(this.running_reads), function(el) { - el.reject(); - }); - }, - read_ids: function (ids, fields, options) { - var self = this; - var to_get = []; - _.each(ids, function(id) { - var cached = _.detect(self.cache, function(x) {return x.id === id;}); - var created = _.detect(self.to_create, function(x) {return x.id === id;}); - if (created) { - _.each(fields, function(x) {if (cached.values[x] === undefined) - cached.values[x] = created.defaults[x] || false;}); - } else { - if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined;})) - to_get.push(id); - } - }); - var return_records = function() { - var records = _.map(ids, function(id) { - var c = _.find(self.cache, function(c) {return c.id === id;}); - return _.isUndefined(c) ? c : _.extend({}, c.values, {"id": id}); - }); - 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); - }); - this._super(to_get, fields, options).then(function() { - def.resolve.apply(def, arguments); - }, function() { - def.reject.apply(def, arguments); - }); - return def.then(function(records) { - _.each(records, function(record, index) { - var id = to_get[index]; - var cached = _.detect(self.cache, function(x) {return x.id === id;}); - if (!cached) { - self.cache.push({id: id, values: record}); - } else { - // I assume cache value is prioritary - cached.values = _.defaults(_.clone(cached.values), 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 to_write - // or to_create collection) - if (_(this.to_create.concat(this.to_write)).find(function (record) { - return record.id === id; })) { - return; - } - for(var i=0, len=this.cache.length; i<len; ++i) { - var record = this.cache[i]; - // if record we call the button upon is in the cache - if (record.id === id) { - // evict it so it gets reloaded from server - this.cache.splice(i, 1); - break; - } - } - }, - call_button: function (method, args) { - this.evict_record(args[0][0]); - return this._super(method, args); - }, - exec_workflow: function (id, signal) { - this.evict_record(id); - return this._super(id, signal); - }, - alter_ids: function(n_ids) { - this._super(n_ids); - this.trigger("dataset_changed", n_ids); - }, -}); -instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/; - -instance.web.ProxyDataSet = instance.web.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); - } - }, -}); - -instance.web.CompoundContext = instance.web.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 instance.web.pyeval.eval('context', this, undefined, {no_user_context: true}); - }, -}); - -instance.web.CompoundDomain = instance.web.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 instance.web.pyeval.eval('domain', this); - }, -}); - -instance.web.DropMisordered = instance.web.Class.extend({ - /** - * @constructs instance.web.DropMisordered - * @extends instance.web.Class - * - * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored - */ - init: function (failMisordered) { - // local sequence number, for requests sent - this.lsn = 0; - // remote sequence number, seqnum of last received request - this.rsn = -1; - this.failMisordered = failMisordered || false; - }, - /** - * Adds a deferred (usually an async request) to the sequencer - * - * @param {$.Deferred} deferred to ensure add - * @returns {$.Deferred} - */ - add: function (deferred) { - var res = $.Deferred(); - - var self = this, seq = this.lsn++; - deferred.done(function () { - if (seq > self.rsn) { - self.rsn = seq; - res.resolve.apply(res, arguments); - } else if (self.failMisordered) { - res.reject(); - } - }).fail(function () { - res.reject.apply(res, arguments); - }); - - return res.promise(); - } -}); - -instance.web.SimpleIndexedDB = instance.web.Class.extend({ - /** - * A simple wrapper around IndexedDB that provides an asynchronous - * localStorage-like Key-Value api that persists between browser - * restarts. - * - * It will not work if the browser doesn't support a recent version - * of IndexedDB, and it may fail if the user refuses db access. - * - * All instances of SimpleIndexedDB will by default refer to the same - * IndexedDB database, if you want to pick another one, use the 'name' - * option on instanciation. - */ - init: function(opts){ - var self = this; - var opts = opts || {}; - - this.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; - this.db = undefined; - this._ready = new $.Deferred(); - - if(this.indexedDB && this.indexedDB.open){ - var request = this.indexedDB.open( opts.name || "SimpleIndexedDB" ,1); - - request.onerror = function(event){ - self.db = null; - self._ready.reject(event.target.error); - }; - - request.onsuccess = function(event){ - self.db = request.result; - self._ready.resolve(); - }; - - request.onupgradeneeded = function(event){ - self.db = event.target.result; - - var objectStore = self.db.createObjectStore("keyvals", { keyPath:"key" }); - self._ready.resolve(); - }; - }else{ - this.db = null; - this._ready.reject({type:'UnknownError', message:'IndexedDB is not supported by your Browser'}); - } - }, - - /** - * returns true if the browser supports the necessary IndexedDB API - * (but doesn't tell if the db can be created, check ready() for that) - */ - isSupportedByBrowser: function(){ - return this.indexedDB && this.indexedDB.open; - }, - - /** - * returns a deferred that resolves when the db has been successfully - * initialized. done/failed callbacks are optional. - */ - ready: function(done,failed){ - this._ready.then(done,failed); - return this._ready.promise(); - }, - - /** - * fetches the value associated to 'key' in the db. if the key doesn't - * exists, it returns undefined. The returned value is provided as a - * deferred, or as the first parameter of the optional 'done' callback. - */ - getItem: function(key,done,failed){ - var self = this; - var def = new $.Deferred(); - def.then(done,failed); - - this._ready.then(function(){ - var request = self.db.transaction(["keyvals"],"readwrite") - .objectStore("keyvals") - .get(key); - - request.onsuccess = function(){ - def.resolve(request.result ? request.result.value : undefined); - }; - - request.onerror = function(event){ - def.reject(event.target.error); - }; - },function(){ - def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'}); - }); - - return def.promise(); - }, - - /** - * Associates a value to 'key' in the db, overwriting previous value if - * necessary. Contrary to localStorage, the value is not limited to strings and - * can be any javascript object, even with cyclic references ! - * - * Be sure to check for failure as the user may refuse to have data localy stored. - */ - setItem: function(key,value,done,failed){ - var self = this; - var def = new $.Deferred(); - def.then(done,failed); - - this._ready.then(function(){ - var request = self.db.transaction(["keyvals"],"readwrite") - .objectStore("keyvals") - .put( {key:key, value:value} ); - - request.onsuccess = function(){ - def.resolve(); - }; - - request.onerror = function(event){ - def.reject(event.target.error); - }; - },function(){ - def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'}); - }); - - return def.promise(); - }, - - /** - * Removes the value associated with 'key' from the db. - */ - removeItem: function(key,done,failed){ - var self = this; - var def = new $.Deferred(); - def.then(done,failed); - - this._ready.then(function(){ - var request = self.db.transaction(["keyvals"],"readwrite") - .objectStore("keyvals") - .delete(key); - - request.onsuccess = function(){ - def.resolve(); - }; - - request.onerror = function(event){ - def.reject(event.target.error); - }; - },function(){ - def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'}); - }); - - return def.promise(); - }, -}); - -})(); |
