[IMP] web, *: compute contexts and domains from datapoint

This commit tries to refactore the management of contexts and domains
in the webclient.

- Get rid of CompoundDomain: use standard array manipulation to
join multiple domains or use the Domain class (to be improved)
- Get rid of data.js build_domain and build_context methods
- Link this.record to the data point from the basic model for all
fields (not only relational fields)
- Add a getContext and a getDomain function to the basic model
data point (localData element) so that AbstractField implementations
can use them thanks to their this.record element.
- Remove the autocontext binding for field RPCs (see FieldManagerMixin)
This commit is contained in:
qsm-odoo
2017-04-11 19:44:38 +02:00
committed by Aaron Bohy
parent e574027056
commit 5804ed100e
18 changed files with 289 additions and 297 deletions
+7 -6
View File
@@ -4,8 +4,8 @@ odoo.define('board.dashboard', function (require) {
var ActionManager = require('web.ActionManager');
var Context = require('web.Context');
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
var Domain = require('web.Domain');
var FavoriteMenu = require('web.FavoriteMenu');
var form_common = require('web.view_dialogs');
var pyeval = require('web.pyeval');
@@ -396,9 +396,11 @@ FavoriteMenu.include({
var search_data = this.searchview.build_search_data(),
context = new Context(this.searchview.dataset.get_context() || []),
domain = new data.CompoundDomain(this.searchview.dataset.get_domain() || []);
domain = this.searchview.dataset.get_domain() || [];
_.each(search_data.contexts, context.add, context);
_.each(search_data.domains, domain.add, domain);
_.each(search_data.domains, function (d) {
domain.push(Domain.prototype.stringToArray(d));
});
context.add({
group_by: pyeval.eval('groupbys', search_data.groupbys || [])
@@ -412,13 +414,12 @@ FavoriteMenu.include({
}
this.toggle_dashboard_menu(false);
c.dashboard_merge_domains_contexts = false;
var d = pyeval.eval('domain', domain);
var name = self.$add_dashboard_input.val();
return self.rpc('/board/add_to_dashboard', {
action_id: self.action_id || false,
context_to_save: c,
domain: d,
domain: domain,
view_mode: self.view_manager.active_view.type,
name: name,
}).then(function (r) {
@@ -2,9 +2,9 @@ odoo.define('google_spreadsheet.google.spreadsheet', function (require) {
"use strict";
var ActionManager = require('web.ActionManager');
var Context = require('web.Context');
var core = require('web.core');
var data = require('web.data');
var Domain = require('web.Domain');
var FavoriteMenu = require('web.FavoriteMenu');
var pyeval = require('web.pyeval');
var ViewManager = require('web.ViewManager');
@@ -14,7 +14,7 @@ var QWeb = core.qweb;
FavoriteMenu.include({
start: function () {
this._super();
var am = this.findAncestor(function(a) {
var am = this.findAncestor(function (a) {
return a instanceof ActionManager;
});
if (am && am.get_inner_widget() instanceof ViewManager) {
@@ -28,17 +28,14 @@ FavoriteMenu.include({
model = this.searchview.dataset.model,
list_view = this.view_manager.views.list,
list_view_id = list_view ? list_view.view_id : false,
context = this.searchview.dataset.get_context() || [],
compound_context = new Context(context),
compound_domain = new data.CompoundDomain(context),
domain = [],
groupbys = pyeval.eval('groupbys', sv_data.groupbys).join(" "),
ds = new data.DataSet(this, 'google.drive.config');
_.each(sv_data.contexts, compound_context.add, compound_context);
_.each(sv_data.domains, compound_domain.add, compound_domain);
compound_domain = JSON.stringify(compound_domain.eval());
ds.call('set_spreadsheet', [model, compound_domain, groupbys, list_view_id])
_.each(sv_data.domains, function (d) {
domain.push(Domain.prototype.stringToArray(d));
});
ds.call('set_spreadsheet', [model, Domain.prototype.arrayToString(domain), groupbys, list_view_id])
.done(function (res) {
if (res.url){
window.open(res.url, '_blank');
+2 -3
View File
@@ -6,7 +6,6 @@ var utils = require('mail.utils');
var config = require('web.config');
var Bus = require('web.Bus');
var core = require('web.core');
var data = require('web.data');
var session = require('web.session');
var time = require('web.time');
var web_client = require('web.web_client');
@@ -669,11 +668,11 @@ var ChatManager = Class.extend(Mixins.EventDispatcherMixin, Mixins.ServicesMixi
var cache = get_channel_cache(channel, options.domain);
if (options.domain) {
domain = new data.CompoundDomain(domain, options.domain || []).eval();
domain = domain.concat(options.domain || []);
}
if (options.load_more) {
var min_message_id = cache.messages[0].id;
domain = new data.CompoundDomain([['id', '<', min_message_id]], domain).eval();
domain = [['id', '<', min_message_id]].concat(domain);
}
return this.rpc('mail.message', 'message_fetch')
+3 -3
View File
@@ -217,7 +217,7 @@ var Followers = AbstractField.extend({
_follow: function () {
var kwargs = {
partner_ids: [this.partnerID],
context: {}, // new Context(this.build_context(), {}) // fixme
context: {}, // FIXME
};
this.rpc(this.model, 'message_subscribe')
.args([[this.res_id]])
@@ -240,7 +240,7 @@ var Followers = AbstractField.extend({
[self.res_id],
ids.partner_ids,
ids.channel_ids,
{}, // new Context(self.build_context(), {}) // fixme
{}, // FIXME
];
self.rpc(self.model, 'message_unsubscribe')
.args(args)
@@ -285,7 +285,7 @@ var Followers = AbstractField.extend({
} else {
var kwargs = _.extend({}, ids);
kwargs.subtype_ids = checklist;
kwargs.context = {}; // new Context(this.build_context(), {}); // fixme
kwargs.context = {}; // FIXME
this.rpc(this.model, 'message_subscribe')
.args([[this.res_id]])
.kwargs(kwargs)
+3 -69
View File
@@ -64,8 +64,7 @@ var Query = Class.extend({
if (!to_set.hasOwnProperty(key)) { continue; }
switch(key) {
case 'filter':
q._filter = new CompoundDomain(
q._filter, to_set.filter);
q._filter = (q._filter || []).concat(to_set.filter || []);
break;
case 'context':
q._context = new Context(
@@ -729,77 +728,12 @@ var DataSetSearch = DataSet.extend({
}
});
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 rawify (data) {
// replaces the value of relational fields by their raw value (the id, if
// any, for many2ones and an array of ids for x2manys), in turn
return _.mapObject(data, function (value) {
if (value instanceof moment) {
return moment;
}
if (_.isObject(value)) {
return value.type === 'record' ? value.res_id : value.res_ids;
}
return value;
});
}
function build_eval_context (record) {
var field_values = rawify(_.extend({}, record.data, { active_model: record.model }));
if ('id' in record.data) {
field_values.active_id = record.data.id;
field_values.active_ids = [record.data.id];
}
// FIXME: parent?
return new Context(field_values);
}
function build_context(record, context) {
context = context || {};
var eval_context = build_eval_context(record);
return new Context(context).set_eval_context(eval_context);
}
function build_domain(record, domain) {
domain = domain || [];
var eval_context = build_eval_context(record);
return new CompoundDomain(domain).set_eval_context(eval_context);
}
var data = {
Query: Query,
DataSet: DataSet,
DataSetStatic: DataSetStatic,
DataSetSearch: DataSetSearch,
CompoundDomain: CompoundDomain,
noDisplayContent: "<em class=\"text-warning\">" + _t("Unnamed") + "</em>",
build_context: build_context,
build_domain: build_domain,
};
@@ -841,11 +775,11 @@ var Model = Class.extend({
* 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
* @returns {Array} 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 data.CompoundDomain(this._domain, domain);
return this._domain.concat(domain);
},
/**
* Fetches the combination of the user's context and the domain context,
@@ -3,7 +3,7 @@ odoo.define('web.search_inputs', function (require) {
var Context = require('web.Context');
var core = require('web.core');
var data = require('web.data');
var Domain = require('web.Domain');
var field_utils = require('web.field_utils');
var pyeval = require('web.pyeval');
var time = require('web.time');
@@ -151,27 +151,27 @@ var Field = Input.extend( /** @lends instance.web.search.Field# */ {
var domain = this.attrs.filter_domain;
if (domain) {
value_to_domain = function (facetValue) {
return new data.CompoundDomain(domain)
.set_eval_context({self: self.value_from(facetValue)});
return Domain.prototype.stringToArray(
domain,
{self: self.value_from(facetValue)}
);
};
} else {
value_to_domain = function (facetValue) {
return self.make_domain(
self.attrs.name,
self.attrs.operator || self.default_operator,
facetValue);
facetValue
);
};
}
var domains = facet.values.map(value_to_domain);
if (domains.length === 1) { return domains[0]; }
for (var i = domains.length; --i;) {
domains.unshift(['|']);
if (domains.length === 1) {
return domains[0];
}
return _.extend(new data.CompoundDomain(), {
__domains: domains
});
var ors = _.times(domains.length - 1, _.constant("|"));
return ors.concat.apply(ors, domains);
}
});
@@ -421,7 +421,7 @@ var ManyToOneField = CharField.extend({
var args = this.attrs.domain;
if (typeof args === 'string') {
try {
args = pyeval.eval('domain', args);
args = Domain.prototype.stringToArray(args);
} catch(e) {
args = [];
}
@@ -625,14 +625,15 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{
.reject(_.isEmpty)
.value();
if (!domains.length) { return; }
if (domains.length === 1) { return domains[0]; }
for (var i=domains.length; --i;) {
domains.unshift(['|']);
if (!domains.length) {
return;
}
return _.extend(new data.CompoundDomain(), {
__domains: domains
});
if (domains.length === 1) {
return domains[0];
}
var ors = _.times(domains.length - 1, _.constant("|"));
return ors.concat.apply(ors, domains);
},
toggle_filter: function (e) {
e.preventDefault();
+5 -3
View File
@@ -14,6 +14,11 @@ var Context = Class.extend({
self.add(x);
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
add: function (context) {
this.__contexts.push(context);
return this;
@@ -22,9 +27,6 @@ var Context = Class.extend({
this.__eval_context = eval_context;
return this;
},
get_eval_context: function () {
return this.__eval_context;
},
eval: function () {
return pyeval.eval('context', this);
},
+35 -7
View File
@@ -12,20 +12,22 @@ var Domain = collections.Tree.extend({
/**
* @constructor
* @param {string|Array|boolean|undefined} domain
* - The given domain can be:
* The given domain can be:
* * a string representation of the Python prefix-array
* representation of the domain.
* * a JS prefix-array representation of the domain.
* * a boolean where the "true" domain match all records and the
* "false" domain does not match any records.
* * undefined, considered as the false boolean.
* @param {Object} [evalContext] - in case the given domain is a string, an
* evaluation context might be needed
*/
init: function (domain) {
init: function (domain, evalContext) {
this._super.apply(this, arguments);
if (domain === true || domain === false || domain === undefined) {
this._data = domain || false;
} else {
this._parse(this.normalizeArray(_.clone(this.stringToArray(domain))));
this._parse(this.normalizeArray(_.clone(this.stringToArray(domain, evalContext))));
}
},
@@ -107,6 +109,30 @@ var Domain = collections.Tree.extend({
}
},
/**
* @returns {Array} JS prefix-array representation of this domain
*/
toArray: function () {
if (this._data === false) {
throw new Error("'false' domain cannot be converted to array");
} else if (this._data === true) {
return [];
} else {
var arr = [this._data];
return arr.concat.apply(arr, _.map(this._children, function (child) {
return child.toArray();
}));
}
},
/**
* @returns {string} representation of the Python prefix-array
* representation of the domain
*/
toString: function () {
return Domain.prototype.arrayToString(this.toArray());
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -179,8 +205,9 @@ var Domain = collections.Tree.extend({
arrayToString: function (domain) {
if (_.isString(domain)) return domain;
return JSON.stringify(domain || [])
.replace(/false/g, "False")
.replace(/true/g, "True");
.replace(/null/g, "None")
.replace(/false/g, "False")
.replace(/true/g, "True");
},
/**
* Converts a string representation of the Python prefix-array
@@ -189,11 +216,12 @@ var Domain = collections.Tree.extend({
*
* @static
* @param {string|Array} domain
* @param {Object} [evalContext]
* @returns {Array}
*/
stringToArray: function (domain) {
stringToArray: function (domain, evalContext) {
if (!_.isString(domain)) return domain;
return pyeval.eval("domain", domain || "[]");
return pyeval.eval("domain", domain || "[]", evalContext);
},
/**
* Makes implicit "&" operators explicit in the given JS prefix-array
+1 -7
View File
@@ -867,12 +867,6 @@ function eval_domains(domains, evaluation_context) {
evaluation_context.context = evaluation_context;
domain_array_to_combine = py.eval(domain.__debug, wrap_context(evaluation_context));
break;
case 'compound_domain':
var eval_context = eval_contexts([domain.__eval_context]);
domain_array_to_combine = eval_domains(
domain.__domains, _.extend({}, evaluation_context, eval_context)
);
break;
default:
domain_array_to_combine = domain;
}
@@ -983,7 +977,7 @@ function pyeval(type, object, context) {
function eval_arg(arg) {
if (typeof arg !== 'object' || !arg.__ref) { return arg; }
switch(arg.__ref) {
case 'domain': case 'compound_domain':
case 'domain':
return pyeval('domains', [arg]);
case 'context': case 'compound_context':
return pyeval('contexts', [arg]);
@@ -81,6 +81,9 @@ var AbstractField = Widget.extend({
// 'name' is the field name displayed by this widget
this.name = name;
// the datapoint fetched from the model
this.record = record;
// the 'field' property is a description of all the various field properties,
// such as the type, the comodel (relation), ...
this.field = record.fields[name];
@@ -208,17 +211,6 @@ var AbstractField = Widget.extend({
this._reset(record, event);
return this._render();
},
/**
* @override performModelRPC from ServicesMixin
* Adds the dataPointID to the option parameter so that the BasicController
* (the FieldManagerMixin) knows the context to bind to the rpc call.
*/
performModelRPC: function (model, method, args, kwargs, options) {
return this._super(model, method, args, kwargs, _.extend({
dataPointID: this.dataPointID,
fieldName: this.name,
}, options || {}));
},
//--------------------------------------------------------------------------
// Private
@@ -289,6 +281,7 @@ var AbstractField = Widget.extend({
* @param {OdooEvent} event the event that triggered the change
*/
_reset: function (record, event) {
this.record = record;
this.value = record.data[this.name];
this.recordData = record.data;
},
@@ -1298,9 +1298,9 @@ var FieldDomain = AbstractField.extend({
// ... then query the model to get the number of matched record if valid
var self = this;
return def.then(function () {
var context = {}; // FIXME ?
return self.rpc(self.domainModel, "search_count")
.args([domain,context])
.args([domain])
.kwargs({context: self.record.getContext({fieldName: self.name})})
.exec()
.then(function (data) {
self._isValidForModel = true;
@@ -16,7 +16,6 @@ odoo.define('web.relational_fields', function (require) {
var AbstractField = require('web.AbstractField');
var concurrency = require('web.concurrency');
var config = require('web.config');
var Context = require('web.Context');
var ControlPanel = require('web.ControlPanel');
var dialogs = require('web.view_dialogs');
var core = require('web.core');
@@ -26,39 +25,10 @@ var field_utils = require('web.field_utils');
var KanbanRenderer = require('web.KanbanRenderer');
var ListRenderer = require('web.ListRenderer');
var Pager = require('web.Pager');
var pyeval = require('web.pyeval');
var _t = core._t;
var qweb = core.qweb;
/**
* Abstract widget to handle relational fields
*/
var AbstractRelationalField = AbstractField.extend({
init: function (parent, name, record) {
this._super.apply(this, arguments);
// 'domain' and 'context' that should be used by the field widget. It
// can come from the attrs, and is mostly useful for rpcs
this.domain = this.attrs.domain || this.field.domain || [];
this.context = this.attrs.context || this.field.context || {};
// todo: remove this
this.record = record;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_reset: function (record) {
this.record = record;
this._super.apply(this, arguments);
},
});
//------------------------------------------------------------------------------
// Many2one widgets
//------------------------------------------------------------------------------
@@ -104,7 +74,7 @@ var M2ODialog = Dialog.extend({
},
});
var FieldMany2One = AbstractRelationalField.extend({
var FieldMany2One = AbstractField.extend({
template: 'FieldMany2One',
custom_events: {
'quick_create': '_onQuickCreate',
@@ -229,6 +199,7 @@ var FieldMany2One = AbstractRelationalField.extend({
if (this.nodeOptions.quick_create) {
this.rpc(this.field.relation, "name_create")
.args([name])
.kwargs({context: this.record.getContext({fieldName: this.name})})
.exec()
.then(function (result) {
if (self.mode === "edit") {
@@ -288,18 +259,20 @@ var FieldMany2One = AbstractRelationalField.extend({
var def = $.Deferred();
this.orderer.add(def);
var exclusion_domain = [];
var context = this.record.getContext({fieldName: this.name});
var domain = this.record.getDomain({fieldName: this.name});
var blacklisted_ids = this._getSearchBlacklist();
if (blacklisted_ids.length > 0) {
exclusion_domain.push(['id', 'not in', blacklisted_ids]);
domain.push(['id', 'not in', blacklisted_ids]);
}
var domain = new data.CompoundDomain(data.build_domain(this.record, this.domain), exclusion_domain);
this.performModelRPC(this.field.relation, "name_search", [], {
name: search_val,
args: pyeval.eval("domain", domain),
args: domain,
operator: "ilike",
limit: this.limit + 1,
context: context,
}).then(function (result) {
// possible selections for the m2o
var values = _.map(result, function (x) {
@@ -320,9 +293,10 @@ var FieldMany2One = AbstractRelationalField.extend({
action: function () {
self.performModelRPC(self.field.relation, "name_search", {
name: search_val,
args: pyeval.eval('domain', domain),
args: domain,
operator: "ilike",
limit: 160,
context: context,
}).then(self._searchCreatePopup.bind(self, "search"));
},
classname: 'o_m2o_dropdown_option',
@@ -370,8 +344,8 @@ var FieldMany2One = AbstractRelationalField.extend({
var self = this;
new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, {
res_model: this.field.relation,
domain: data.build_domain(this.record, this.domain),
context: new Context(data.build_context(this.record, this.context), context || {}),
domain: this.record.getDomain({fieldName: this.name}),
context: _.extend({}, this.record.getContext({fieldName: this.name}), context || {}),
title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
initial_ids: ids ? _.map(ids, function (x) { return x[0]; }) : undefined,
initial_view: view,
@@ -405,12 +379,13 @@ var FieldMany2One = AbstractRelationalField.extend({
if (this.mode === 'readonly' && !this.nodeOptions.no_open) {
event.preventDefault();
event.stopPropagation();
var context = data.build_context(this.record, this.context);
var args = [[this.value.res_id], context.eval()];
this.performModelRPC(this.field.relation, 'get_formview_action', args)
.then(function (action) {
self.trigger_up('do_action', {action: action});
});
this.performModelRPC(this.field.relation, 'get_formview_action', [
[this.value.res_id]
], {
context: this.record.getContext({fieldName: this.name}),
}).then(function (action) {
self.trigger_up('do_action', {action: action});
});
}
},
/**
@@ -422,11 +397,14 @@ var FieldMany2One = AbstractRelationalField.extend({
return;
}
var self = this;
var context = data.build_context(this.record, this.context);
var model = this.field.relation;
var method = 'get_formview_id';
var args = [[this.value.res_id], context.eval()];
this.performModelRPC(model, method, args).then(function (view_id) {
var context = this.record.getContext({fieldName: this.name});
this.performModelRPC(model, method, [
[this.value.res_id]
], {
context: context,
}).then(function (view_id) {
new dialogs.FormViewDialog(self, {
res_model: self.field.relation,
res_id: self.value.res_id,
@@ -492,7 +470,7 @@ var ListFieldMany2One = FieldMany2One.extend({
},
});
var KanbanFieldMany2One = AbstractRelationalField.extend({
var KanbanFieldMany2One = AbstractField.extend({
tagName: 'span',
init: function () {
this._super.apply(this, arguments);
@@ -515,9 +493,9 @@ var KanbanFieldMany2One = AbstractRelationalField.extend({
// X2Many widgets
//------------------------------------------------------------------------------
var FieldX2Many = AbstractRelationalField.extend({
var FieldX2Many = AbstractField.extend({
tagName: 'div',
custom_events: _.extend({}, AbstractRelationalField.prototype.custom_events, {
custom_events: _.extend({}, AbstractField.prototype.custom_events, {
field_changed: '_onFieldChanged',
kanban_record_delete: '_onDeleteRecord',
list_record_delete: '_onDeleteRecord',
@@ -716,8 +694,8 @@ var FieldX2Many = AbstractRelationalField.extend({
*/
_onOpenRecord: function (event) {
_.extend(event.data, {
context: data.build_context(this.record, this.context),
domain: data.build_context(this.record, this.domain),
context: this.record.getContext({fieldName: this.name}),
domain: this.record.getDomain({fieldName: this.name}),
form_view: this.field.views && this.field.views.form,
readonly: this.mode === 'readonly',
string: this.string,
@@ -810,8 +788,8 @@ var FieldOne2Many = FieldX2Many.extend({
});
} else {
this.trigger_up('add_one2many_record', {
domain: data.build_domain(this.record, this.domain),
context: data.build_context(this.record, this.context),
domain: this.record.getDomain({fieldName: this.name}),
context: this.record.getContext({fieldName: this.name}),
field: this.field,
on_save: function (record) {
this._setValue({
@@ -872,11 +850,13 @@ var FieldMany2Many = FieldX2Many.extend({
*/
_onAddRecord: function () {
var self = this;
var domain = data.build_domain(this.record, this.domain);
var domain = this.record.getDomain({fieldName: this.name});
new dialogs.SelectCreateDialog(this, {
res_model: this.field.relation,
domain: new data.CompoundDomain(domain, ["!", ["id", "in", this.value.res_ids]]),
context: data.build_context(this.record, this.context),
domain: domain.concat(["!", ["id", "in", this.value.res_ids]]),
context: this.record.getContext({fieldName: this.name}),
title: _t("Add: ") + this.string,
no_create: this.nodeOptions.no_create || !this.activeActions.create,
on_selected: function (res_ids) {
@@ -908,7 +888,7 @@ var FieldMany2Many = FieldX2Many.extend({
},
});
var FieldMany2ManyTags = AbstractRelationalField.extend({
var FieldMany2ManyTags = AbstractField.extend({
tag_template: "FieldMany2ManyTag",
className: "o_form_field o_form_field_many2manytags",
replace_element: true,
@@ -1118,9 +1098,9 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
},
});
var FieldMany2ManyCheckBoxes = AbstractRelationalField.extend({
var FieldMany2ManyCheckBoxes = AbstractField.extend({
template: 'FieldMany2ManyCheckBoxes',
events: _.extend({}, AbstractRelationalField.prototype.events, {
events: _.extend({}, AbstractField.prototype.events, {
change: '_onChange',
}),
supportedFieldTypes: ['many2many'],
@@ -79,7 +79,7 @@ odoo.define('web.BasicModel', function (require) {
var AbstractModel = require('web.AbstractModel');
var concurrency = require('web.concurrency');
var Context = require('web.Context');
var data = require('web.data'); // TODO: remove dependency to data.js
var Domain = require('web.Domain');
var fieldUtils = require('web.field_utils');
var pyeval = require('web.pyeval');
var session = require('web.session');
@@ -365,6 +365,10 @@ var BasicModel = AbstractModel.extend({
delete element.parentID;
delete element.relation_field;
delete element.rawContext;
element.getContext = this._getContext.bind(this, record);
element.getDomain = this._getDomain.bind(this, record);
return element;
},
/**
@@ -929,20 +933,6 @@ var BasicModel = AbstractModel.extend({
});
}
},
/**
* Every RPC done by the model need to add some context, which is a
* combination of the context of the session, of the record/list, and/or of
* the concerned field. This method combines all these contexts and evaluate
* them with the proper evalcontext.
*
* @param {string} recordID - the id of an element from the localData
* @see _getContext for the options parameter
* @returns {Object} the evaluated context
*/
getContext: function (recordID, options) {
var record = this.localData[recordID];
return this._getContext(record, options);
},
//--------------------------------------------------------------------------
// Private
@@ -1626,7 +1616,77 @@ var BasicModel = AbstractModel.extend({
*/
_getContext: function (element, options) {
var context = new Context(session.user_context, element.context);
context.set_eval_context(this._getEvalContext(element));
if (options && options.fieldName) {
var attrs = element.fieldAttrs[options.fieldName];
if (attrs && attrs.context) {
context.add(attrs.context);
} else {
var fieldParams = element.fields[options.fieldName];
if (fieldParams.context) {
context.add(fieldParams.context);
}
}
}
if (element.rawContext) {
var rawContext = new Context(element.rawContext);
var evalContext = this._getEvalContext(this.localData[element.parentID]);
evalContext.id = evalContext.id || false;
rawContext.set_eval_context(evalContext);
context.add(rawContext);
}
return context.eval();
},
/**
* Some records are associated to a/some domain(s). This method allows to
* retrieve them, evaluated.
*
* @param {Object} element an element from the localData
* @param {Object} [options]
* @param {string} [options.fieldName]
* the name of the field whose domain needs to be returned
* @returns {Array} the evaluated domain
*/
_getDomain: function (element, options) {
if (options && options.fieldName) {
var attrs = element.fieldAttrs[options.fieldName];
if (attrs && attrs.domain) {
return Domain.prototype.stringToArray(
attrs.domain,
this._getEvalContext(element)
);
}
var fieldParams = element.fields[options.fieldName];
if (fieldParams.domain) {
return Domain.prototype.stringToArray(
fieldParams.domain,
this._getEvalContext(element)
);
}
return [];
}
return Domain.prototype.stringToArray(
element.domain,
this._getEvalContext(element)
);
},
/**
* Returns the evaluation context that should be used when evaluating the
* context/domain associated to a given element from the localData.
*
* @param {Object} element - an element from the localData
* @returns {Object}
*/
_getEvalContext: function (element) {
var evalContext = this.get(element.id, {raw: true}).data;
evalContext.active_model = element.model;
if (evalContext.id) {
evalContext.active_id = evalContext.id;
evalContext.active_ids = [evalContext.id];
}
if (element.parentID) {
var parent = this.get(element.parentID, {raw: true});
if (parent.type === 'list' && this.localData[element.parentID].parentID) {
@@ -1634,23 +1694,7 @@ var BasicModel = AbstractModel.extend({
}
_.extend(evalContext, {parent: parent.data});
}
context.set_eval_context(evalContext);
if (options && options.fieldName) {
var attrs = element.fieldAttrs[options.fieldName];
if (attrs && attrs.context) {
context.add(attrs.context);
}
}
if (element.rawContext) {
var rawContext = new Context(element.rawContext);
var evalContext = this.get(element.parentID, {raw: true}).data;
evalContext.id = evalContext.id || false;
rawContext.set_eval_context(evalContext);
context.add(rawContext);
}
return context.eval();
return evalContext;
},
/**
* Helper method for the load entry point.
@@ -1759,8 +1803,7 @@ var BasicModel = AbstractModel.extend({
}
return val;
});
var _domain = new data.CompoundDomain(field.domain || {}).set_eval_context(field_values);
var domain = pyeval.eval('domain', _domain);
var domain = Domain.prototype.stringToArray(field.domain || [], field_values);
var fold_field = pyeval.py_eval(attrs.options || '{}').fold_field;
var fetch_status_information = self.rpc(field.relation, 'search_read')
.withFields(['id'].concat(fold_field ? [fold_field] : []))
@@ -18,7 +18,6 @@ var FieldManagerMixin = {
custom_events: {
field_changed: '_onFieldChanged',
load: '_onLoad',
call_service: '_onCallService',
},
/**
* A FieldManagerMixin can be initialized with an instance of a basicModel.
@@ -114,40 +113,6 @@ var FieldManagerMixin = {
data.on_success(self.model.get(db_id));
});
},
/**
* Some widgets perform model RPCs. These are intercepted so that the
* context can be automatically added according to the given dataPointID
* option the AbstractField implementation automatically adds.
*
* @param {OdooEvent} e
* @param {string} e.data.service - the service called, here we are only
* interested in the "ajax" service
* @param {string} e.data.method - the service method called, here we are
* only interested by the "rpc" method
* @param {Array} e.data.args
* the args parameters contains the route, the route arguments and
* the RPC call options. This method purpose is to check the RPC
* call options for the dataPointID (and field name) and merge the
* appropriate context with the route arguments' one.
*/
_onCallService: function (e) {
if (e.data.service !== "ajax" || e.data.method !== "rpc") {
return;
}
var args = e.data.args[1];
var options = e.data.args[2];
if (args.kwargs && options.dataPointID) {
args.kwargs.context = _.extend(
this.model.getContext(options.dataPointID, {
fieldName: options.fieldName || false,
}),
args.kwargs.context || {}
);
// to prevent doing this work again, in some other widget with FMM.
delete options.dataPointID;
}
},
};
return FieldManagerMixin;
@@ -243,7 +243,7 @@ var FormController = BasicController.extend({
var recordID = record.data.id;
this.trigger_up('execute_action', {
action_data: _.extend({}, attrs, {
context: data.build_context(record, attrs.context)
context: _.extend(record.getContext(), attrs.context),
}),
model: record.model,
record_id: recordID,
@@ -532,7 +532,7 @@ var FormController = BasicController.extend({
res_model: model,
res_id: res_id,
fields_view: form_fields_view,
context: data.build_context(record),
context: record.getContext(),
readonly: event.data.readonly,
title: _t("Open: ") + event.data.string,
on_save: event.data.on_save,
@@ -605,7 +605,7 @@ return FormController;
// record = record || this.model.get(this.db_id);
// var record_id = record.data.id;
// this.trigger_up('execute_action', {
// action_data: _.extend({}, attrs, { context: data.build_context(record, attrs.context) }),
// action_data: _.extend({}, attrs, { context: _.extend(record.getContext(), attrs.context) }),
// dataset: this.dataset,
// record_id: record_id,
// on_close: function (reason) {
+7 -14
View File
@@ -281,13 +281,13 @@ QUnit.module('core', function () {
}, /^Error: TypeError:/); //20
assert.throws(function () {
py.eval('a - 1', ctx);
}, /^Error: TypeError:/);
}, /^Error: TypeError:/);
assert.throws(function () {
py.eval('1 + a', ctx);
}, /^Error: TypeError:/);
}, /^Error: TypeError:/);
assert.throws(function () {
py.eval('1 - a', ctx);
}, /^Error: TypeError:/);
}, /^Error: TypeError:/);
// delta - date is senseless.
assert.throws(function () {
@@ -451,7 +451,7 @@ QUnit.module('core', function () {
{ "active_id": 9, "active_ids": [ 9 ], "active_model": "mail.compose.message" }
];
var result = pyEval.eval_domains_and_contexts({
contexts: ctx,
contexts: ctx,
domins: [],
});
@@ -522,10 +522,7 @@ QUnit.module('core', function () {
var result = pyEval.eval_domains_and_contexts({
domains: [
[['type', '=', 'contract']],
{ "__domains": [["|"], [["state", "in", ["open", "draft"]]], [["type", "=", "contract"], ["state", "=", "pending"]]],
"__eval_context": null,
"__ref": "compound_domain"
},
["|", ["state", "in", ["open", "draft"]], [["type", "=", "contract"], ["state", "=", "pending"]]],
"['|', '&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')), ('is_overdue_quantity', '=', True)]",
[['user_id', '=', 1]]
],
@@ -562,14 +559,11 @@ QUnit.module('core', function () {
});
assert.deepEqual(result1.domain, [['company_id', '=', false]]);
var compound_domain = new data.CompoundDomain(d);
compound_domain.set_eval_context({company_id: 42});
var result2 = pyEval.eval_domains_and_contexts({
domains: [compound_domain],
domains: [d],
contexts: [],
eval_context: {company_id: 42},
});
assert.deepEqual(result2.domain, [['company_id', '=', 42]]);
});
@@ -1014,4 +1008,3 @@ QUnit.module('core', function () {
});
});
@@ -437,8 +437,8 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('check that the field context is used when doing a name_search in a m2o', function (assert) {
assert.expect(3);
QUnit.test('domain and context are correctly used when doing a name_search in a m2o', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
@@ -446,20 +446,24 @@ QUnit.module('relational_fields', {
data: this.data,
arch:
'<form string="Partners">' +
'<field name="product_id" context="{\'hello\': \'world\', \'test\': foo}"/>' +
'<field name="product_id" ' +
'domain="[[\'foo\', \'=\', \'bar\'], [\'foo\', \'=\', foo]]" ' +
'context="{\'hello\': \'world\', \'test\': foo}"/>' +
'<field name="foo"/>' +
'</form>',
res_id: 1,
session: {user_context: {hey: "ho"}},
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/product/name_search') {
var context = args.kwargs.context;
assert.strictEqual(context.hey, "ho",
'the session context should have been used for the rpc');
assert.strictEqual(context.hello, "world",
'the field context should have been used for the RPC');
assert.strictEqual(context.test, "yop",
'the field context should have been properly evaluated');
assert.deepEqual(
args.kwargs.args,
[['foo', '=', 'bar'], ['foo', '=', 'yop']],
'the field attr domain should have been used for the RPC (and evaluated)');
assert.deepEqual(
args.kwargs.context,
{hey: "ho", hello: "world", test: "yop"},
'the field attr context should have been used for the '
+ 'RPC (evaluated and merged with the session one)');
}
return this._super.apply(this, arguments);
},
@@ -638,7 +638,6 @@ QUnit.module('Views', {
model.destroy();
});
QUnit.test('write commands on a one2many', function (assert) {
assert.expect(4);
@@ -835,5 +834,64 @@ QUnit.module('Views', {
model.destroy();
});
QUnit.test('contexts and domains can be properly fetched', function (assert) {
assert.expect(8);
this.data.partner.fields.product_id.context = "{'hello': 'world', 'test': foo}";
this.data.partner.fields.product_id.domain = "[['hello', 'like', 'world'], ['test', 'like', foo]]";
var model = createModel({
Model: BasicModel,
data: this.data,
});
this.params.fieldNames = ['product_id', 'foo'];
model.load(this.params).then(function (resultID) {
var recordPartner = model.get(resultID);
assert.strictEqual(typeof recordPartner.getContext, "function",
"partner record should have a getContext function");
assert.strictEqual(typeof recordPartner.getDomain, "function",
"partner record should have a getDomain function");
assert.deepEqual(recordPartner.getContext(), {},
"asking for a context without a field name should fetch the session/user/view context");
assert.deepEqual(recordPartner.getDomain(), [],
"asking for a domain without a field name should fetch the session/user/view domain");
assert.deepEqual(
recordPartner.getContext({fieldName: "product_id"}),
{hello: "world", test: "gnap"},
"asking for a context with a field name should fetch the field context (evaluated)");
assert.deepEqual(
recordPartner.getDomain({fieldName: "product_id"}),
[["hello", "like", "world"], ["test", "like", "gnap"]],
"asking for a domain with a field name should fetch the field domain (evaluated)");
});
model.destroy();
// Try again with xml override of field domain and context
model = createModel({
Model: BasicModel,
data: this.data,
});
this.params.fieldAttrs = {
product_id: {
context: "{'hello2': 'world', 'test2': foo}",
domain: "[['hello2', 'like', 'world'], ['test2', 'like', foo]]",
},
};
model.load(this.params).then(function (resultID) {
var recordPartner = model.get(resultID);
assert.deepEqual(
recordPartner.getContext({fieldName: "product_id"}),
{hello2: "world", test2: "gnap"},
"field context should have been overriden by xml attribute");
assert.deepEqual(
recordPartner.getDomain({fieldName: "product_id"}),
[["hello2", "like", "world"], ["test2", "like", "gnap"]],
"field domain should have been overriden by xml attribute");
});
model.destroy();
});
});});