diff options
| author | Géry Debongnie <ged@odoo.com> | 2015-03-09 16:34:27 +0100 |
|---|---|---|
| committer | Géry Debongnie <ged@odoo.com> | 2015-03-18 09:01:49 +0100 |
| commit | e8a00bc50da435d3859118ca127d20a2eb9a0cab (patch) | |
| tree | 039f6ec40b37392e76c8830a8e518ac40f7e7fec /addons/web/static/src/js/data.js | |
| parent | 9036084f065c7aca3b2199903f4da67db3501ba7 (diff) | |
[REF] web: add module system to the web client
This commit introduces a new module system for the javascript code.
Instead of using global (namespaced) variables everywhere (like
openerp.web.whatever, ...), each file declare one (or more) module
with its dependencies. Each module will be loaded after its
dependencies, and will be given a reference to the return values.
This change is a massive departure from the way the client worked
previously. It will sadly break some/most history, but I am sure that
the future git ecosystem will allow you to easily browse the history
past this revision (one can dream).
This commit feature:
* main namespace is now odoo instead of openerp
* a way to define modules and dependencies (inspired by requirejs)
* many files have been splitted in simpler subfiles
* basic support for debugging in odoo.__DEBUG__
* no global variable (i.e. nothing in window.openerp)
* many bug fixes, many new bugs
Future vision: this modularization should simplify future code
development, simplify debugging and will allow a serious testing effort
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(); - }, -}); - -})(); |
