[IMP] web: auto-context for fields model RPC calls
Some fields need to do some model RPC calls (name_search, name_create, ...). Before this commit, they did not use the performModelRPC method but an explicit trigger_up so that the BasicController (the FieldManagerMixin) catches it and ask the BasicModel to perform the RPC. This was designed to let the BasicModel handle the context. This commit makes the context be automatically binded without the need to explicitely call the BasicModel for each route in the FieldManagerMixin. It also ensures that the field own context is properly merged with the user/action/search contexts. Now, the fields use the performModelRPC method and the FieldManagerMixin catches the associated "call_service" event. Then, it can get the full context from the BasicModel instance and let the event continue its propagation. To know which field context has to be fetched, the performModelRPC method is overriden by AbstractField to automatically add the dataPointID and the field name in the options parameter.
This commit is contained in:
@@ -207,6 +207,17 @@ 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
|
||||
|
||||
@@ -224,21 +224,18 @@ var FieldMany2One = AbstractRelationalField.extend({
|
||||
*/
|
||||
_quickCreate: function (name) {
|
||||
var self = this;
|
||||
var slow_create = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
|
||||
var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
|
||||
if (this.nodeOptions.quick_create) {
|
||||
this.trigger_up('name_create', {
|
||||
model: this.field.relation,
|
||||
name: name,
|
||||
context: data.build_context(this.record, this.context),
|
||||
on_success: function (result) {
|
||||
this.performModelRPC(this.field.relation, "name_create", [name]).then(
|
||||
function (result) {
|
||||
if (self.mode === "edit") {
|
||||
self.reinitialize({id: result[0], display_name: result[1]});
|
||||
}
|
||||
},
|
||||
on_fail: slow_create,
|
||||
});
|
||||
slowCreate
|
||||
);
|
||||
} else {
|
||||
slow_create();
|
||||
slowCreate();
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -296,69 +293,66 @@ var FieldMany2One = AbstractRelationalField.extend({
|
||||
exclusion_domain.push(['id', 'not in', blacklisted_ids]);
|
||||
}
|
||||
var domain = new data.CompoundDomain(data.build_domain(this.record, this.domain), exclusion_domain);
|
||||
this.trigger_up('name_search', {
|
||||
model: this.field.relation,
|
||||
search_val: search_val,
|
||||
domain: pyeval.eval('domain', domain),
|
||||
operator: 'ilike',
|
||||
|
||||
this.performModelRPC(this.field.relation, "name_search", [], {
|
||||
name: search_val,
|
||||
args: pyeval.eval("domain", domain),
|
||||
operator: "ilike",
|
||||
limit: this.limit + 1,
|
||||
on_success: function (result) {
|
||||
// possible selections for the m2o
|
||||
var values = _.map(result, function (x) {
|
||||
x[1] = x[1].split("\n")[0];
|
||||
return {
|
||||
label: _.str.escapeHTML(x[1].trim()) || data.noDisplayContent,
|
||||
value: x[1],
|
||||
name: x[1],
|
||||
id: x[0],
|
||||
};
|
||||
}).then(function (result) {
|
||||
// possible selections for the m2o
|
||||
var values = _.map(result, function (x) {
|
||||
x[1] = x[1].split("\n")[0];
|
||||
return {
|
||||
label: _.str.escapeHTML(x[1].trim()) || data.noDisplayContent,
|
||||
value: x[1],
|
||||
name: x[1],
|
||||
id: x[0],
|
||||
};
|
||||
});
|
||||
|
||||
// search more... if more results than limit
|
||||
if (values.length > self.limit) {
|
||||
values = values.slice(0, self.limit);
|
||||
values.push({
|
||||
label: _t("Search More..."),
|
||||
action: function () {
|
||||
self.performModelRPC(self.field.relation, "name_search", {
|
||||
name: search_val,
|
||||
args: pyeval.eval('domain', domain),
|
||||
operator: "ilike",
|
||||
limit: 160,
|
||||
}).then(self._searchCreatePopup.bind(self, "search"));
|
||||
},
|
||||
classname: 'o_m2o_dropdown_option',
|
||||
});
|
||||
}
|
||||
var create_enabled = self.can_create && !self.nodeOptions.no_create;
|
||||
// quick create
|
||||
var raw_result = _.map(result, function (x) { return x[1]; });
|
||||
if (create_enabled && !self.nodeOptions.no_quick_create &&
|
||||
search_val.length > 0 && !_.contains(raw_result, search_val)) {
|
||||
values.push({
|
||||
label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
|
||||
$('<span />').text(search_val).html()),
|
||||
action: self._quickCreate.bind(self, search_val),
|
||||
classname: 'o_m2o_dropdown_option'
|
||||
});
|
||||
}
|
||||
// create and edit ...
|
||||
if (create_enabled && !self.nodeOptions.no_create_edit) {
|
||||
values.push({
|
||||
label: _t("Create and Edit..."),
|
||||
action: self._searchCreatePopup.bind(self, "form", false, self._createContext(search_val)),
|
||||
classname: 'o_m2o_dropdown_option',
|
||||
});
|
||||
} else if (values.length === 0) {
|
||||
values.push({
|
||||
label: _t("No results to show..."),
|
||||
});
|
||||
}
|
||||
|
||||
// search more... if more results than limit
|
||||
if (values.length > self.limit) {
|
||||
values = values.slice(0, self.limit);
|
||||
values.push({
|
||||
label: _t("Search More..."),
|
||||
action: function () {
|
||||
self.trigger_up('name_search', {
|
||||
model: self.field.relation,
|
||||
search_val: search_val,
|
||||
domain: pyeval.eval('domain', domain),
|
||||
operator: 'ilike',
|
||||
limit: 160,
|
||||
on_success: self._searchCreatePopup.bind(self, "search"),
|
||||
});
|
||||
},
|
||||
classname: 'o_m2o_dropdown_option',
|
||||
});
|
||||
}
|
||||
var create_enabled = self.can_create && !self.nodeOptions.no_create;
|
||||
// quick create
|
||||
var raw_result = _.map(result, function (x) { return x[1]; });
|
||||
if (create_enabled && !self.nodeOptions.no_quick_create &&
|
||||
search_val.length > 0 && !_.contains(raw_result, search_val)) {
|
||||
values.push({
|
||||
label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
|
||||
$('<span />').text(search_val).html()),
|
||||
action: self._quickCreate.bind(self, search_val),
|
||||
classname: 'o_m2o_dropdown_option'
|
||||
});
|
||||
}
|
||||
// create and edit ...
|
||||
if (create_enabled && !self.nodeOptions.no_create_edit) {
|
||||
values.push({
|
||||
label: _t("Create and Edit..."),
|
||||
action: self._searchCreatePopup.bind(self, "form", false, self._createContext(search_val)),
|
||||
classname: 'o_m2o_dropdown_option',
|
||||
});
|
||||
} else if (values.length === 0) {
|
||||
values.push({
|
||||
label: _t("No results to show..."),
|
||||
});
|
||||
}
|
||||
|
||||
def.resolve(values);
|
||||
},
|
||||
def.resolve(values);
|
||||
});
|
||||
|
||||
return def;
|
||||
|
||||
@@ -613,42 +613,6 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
return record.id;
|
||||
},
|
||||
/**
|
||||
* This method just does a name_create call to the server.
|
||||
*
|
||||
* @todo Maybe this method should also create a local record...
|
||||
*
|
||||
* @param {string} model the name of the model
|
||||
* @param {string} name the desired name of the created record
|
||||
* @param {Object} context the context to be passed to the rpc
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
nameCreate: function (model, name, context) {
|
||||
return this.performModelRPC(model, 'name_create', [name], {
|
||||
context: _.extend({}, session.user_context, context),
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Do a name_search. I think that it should be done directly by the widget
|
||||
* @todo remove this
|
||||
* @deprecated
|
||||
*
|
||||
* @param {any} model
|
||||
* @param {any} name
|
||||
* @param {any} domain
|
||||
* @param {any} operator
|
||||
* @param {any} limit
|
||||
* @returns
|
||||
*/
|
||||
nameSearch: function (model, name, domain, operator, limit) {
|
||||
// fixme: correctly handle context
|
||||
return this.performModelRPC(model, 'name_search', [], {
|
||||
name: name || '',
|
||||
args: domain || [],
|
||||
operator: operator || 'ilike',
|
||||
limit: limit || 0,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* This is an extremely important method. All changes in any field go
|
||||
* through this method. It will then apply them in the local state, check
|
||||
@@ -909,6 +873,20 @@ 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
|
||||
@@ -2039,4 +2017,3 @@ var BasicModel = AbstractModel.extend({
|
||||
|
||||
return BasicModel;
|
||||
});
|
||||
|
||||
|
||||
@@ -14,13 +14,11 @@ var core = require('web.core');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
return {
|
||||
var FieldManagerMixin = {
|
||||
custom_events: {
|
||||
field_changed: '_onFieldChanged',
|
||||
name_create: '_onNameCreate',
|
||||
name_get: '_onNameGet',
|
||||
name_search: '_onNameSearch',
|
||||
load: '_onLoad',
|
||||
call_service: '_onCallService',
|
||||
},
|
||||
/**
|
||||
* A FieldManagerMixin can be initialized with an instance of a basicModel.
|
||||
@@ -123,52 +121,36 @@ return {
|
||||
});
|
||||
},
|
||||
/**
|
||||
* When a name_create event arrives, the nameCreate method from the model
|
||||
* should be called.
|
||||
* 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} event
|
||||
* @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.
|
||||
*/
|
||||
_onNameCreate: function (event) {
|
||||
var data = event.data;
|
||||
if (!data.on_success) { return; }
|
||||
this.model
|
||||
.nameCreate(data.model, data.name)
|
||||
.then(data.on_success)
|
||||
.fail(function () {
|
||||
if (data.on_fail) {
|
||||
data.on_fail();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* When a name_get event arrives, the name_get method from the model should
|
||||
* be called.
|
||||
*
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onNameGet: function (event) {
|
||||
var data = event.data;
|
||||
if (!data.on_success) { return; }
|
||||
this.model.name_get(data.model, data.ids)
|
||||
.then(data.on_success); // fixme: handle context
|
||||
},
|
||||
/**
|
||||
* Some field widgets need to perform a namesearch. For example, a many2one
|
||||
* widget when it needs to fetch its autocompletion data.
|
||||
*
|
||||
* @todo Note: I think that this is wrong, the field widget should just perform
|
||||
* a name_search rpc on its own, no need for this.
|
||||
*
|
||||
* @deprecated Just do a regular name_search
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onNameSearch: function (event) {
|
||||
var data = event.data;
|
||||
if (!data.on_success) { return; }
|
||||
this.model
|
||||
.nameSearch(data.model, data.search_val, data.domain, data.operator, data.limit)
|
||||
.then(data.on_success);
|
||||
_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 || {}
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return FieldManagerMixin;
|
||||
});
|
||||
|
||||
@@ -299,10 +299,12 @@ var KanbanController = BasicController.extend({
|
||||
});
|
||||
}
|
||||
data.context['default_' + data.groupedBy[0]] = column.id;
|
||||
this.model
|
||||
.nameCreate(data.model, event.data.value, data.context)
|
||||
.then(add_record)
|
||||
.fail(function (event) {
|
||||
|
||||
this.performModelRPC(data.model, "name_create", [event.data.value], {
|
||||
context: data.context,
|
||||
}).then(
|
||||
add_record,
|
||||
function (event) {
|
||||
event.preventDefault();
|
||||
var popup = new form_common.SelectCreatePopup(this);
|
||||
popup.select_element(
|
||||
@@ -316,7 +318,8 @@ var KanbanController = BasicController.extend({
|
||||
{ default_name: event.data.value }
|
||||
);
|
||||
popup.on("elements_selected", null, add_record);
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @param {OdooEvent} event
|
||||
|
||||
@@ -399,6 +399,41 @@ QUnit.module('relational_fields', {
|
||||
assert.strictEqual(form.$('a').text(), '', 'the tag a should be empty');
|
||||
});
|
||||
|
||||
QUnit.test('check that the field context is used when doing a name_search in a m2o', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form string="Partners">' +
|
||||
'<field name="product_id" context="{\'hello\': \'world\', \'test\': foo}"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
session: {user_context: {hey: "ho"}},
|
||||
manualDestroy: true,
|
||||
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');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('input').first().click();
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldOne2Many');
|
||||
|
||||
QUnit.test('one2many basic properties', function (assert) {
|
||||
|
||||
Reference in New Issue
Block a user