From 7dc695833466b9ef8aac24d76554412a6a8f5879 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 27 Jul 2023 13:36:25 +0000 Subject: [PATCH] [FIX] web: can partially evaluate complex dynamic context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Have a form view with an x2many field, and a context on that field, depending on other fields of the view. For instance: ```xml
``` As we read records in a single rpc (unity read), we need to be able to (at least partially) evaluate the context set on b_ids to build the field specification for the web_read call. The problem is that the evaluation context is incomplete when we want to evalute it, as we don't know the value of `a`. Before this commit, we evaluated that context by adding "sentinel" values in the evaluation context (a sentinel being a Symbol), and we removed all keys whose values had been evaluated to the sentinel afterwards. This worked for the example above (we were able to generate the context `{ key2: 4 }`). However, this wasn't flawless. For instance, with ```xml ``` our sentinel strategy didn't work, because you can't perform such operations on a Symbol. This commit comes with another strategy, which is actually easier, and which handles the above case. We now parse the context, and evaluate each value individually (if it needs to be evaluated). If the evaluation crashes, we swallow the error and do not add that particular key to the evaluated context. Part of task~3179751 closes odoo/odoo#129925 Signed-off-by: Géry Debongnie --- addons/web/static/src/core/context.js | 25 +++++++- .../src/model/relational_model/utils.js | 60 +++---------------- addons/web/static/tests/core/context_tests.js | 38 +++++++++++- .../tests/views/form/form_view_tests.js | 46 ++++++++++++++ 4 files changed, 114 insertions(+), 55 deletions(-) diff --git a/addons/web/static/src/core/context.js b/addons/web/static/src/core/context.js index aba96fc5897..8be6cc9f308 100644 --- a/addons/web/static/src/core/context.js +++ b/addons/web/static/src/core/context.js @@ -1,6 +1,7 @@ /** @odoo-module **/ -import { evaluateExpr } from "./py_js/py"; +import { evaluateExpr, parseExpr } from "./py_js/py"; +import { evaluate } from "./py_js/py_interpreter"; /** * @typedef {{[key: string]: any}} Context @@ -27,3 +28,25 @@ export function makeContext(contexts, initialEvaluationContext) { } return context; } + +/** + * Allow to evaluate a context with an incomplete evaluation context. The evaluated context only + * contains keys whose values are static or can be evaluated with the given evaluation context. + * + * @param {string} context + * @param {Object} [evaluationContext={}] + * @returns {Context} + */ +export function evalPartialContext(_context, evaluationContext = {}) { + const ast = parseExpr(_context); + const context = {}; + for (const key in ast.value) { + const value = ast.value[key]; + try { + context[key] = evaluate(value, evaluationContext); + } catch { + // ignore this key as we can't evaluate its value + } + } + return context; +} diff --git a/addons/web/static/src/model/relational_model/utils.js b/addons/web/static/src/model/relational_model/utils.js index 1115737ebeb..554c0d1b23b 100644 --- a/addons/web/static/src/model/relational_model/utils.js +++ b/addons/web/static/src/model/relational_model/utils.js @@ -1,7 +1,7 @@ /* @odoo-module */ import { markup, onWillDestroy, onWillStart, onWillUpdateProps, useComponent } from "@odoo/owl"; -import { makeContext } from "@web/core/context"; +import { evalPartialContext, makeContext } from "@web/core/context"; import { deserializeDate, deserializeDateTime } from "@web/core/l10n/dates"; import { x2ManyCommands } from "@web/core/orm_service"; import { Deferred } from "@web/core/utils/concurrency"; @@ -204,49 +204,6 @@ export function getFieldContext( }; } -const SENTINEL = Symbol("sentinel"); -function _getFieldContextSpec( - fieldName, - activeFields, - fields, - evalContext, - parentActiveFields = null -) { - const rawContext = activeFields[fieldName].context; - if (!rawContext || rawContext === "{}") { - return fields[fieldName].context; - } - - evalContext = { - ids: SENTINEL, - active_id: SENTINEL, - active_ids: SENTINEL, - active_model: SENTINEL, - ...evalContext, - id: SENTINEL, - }; - for (const fieldName in activeFields) { - evalContext[fieldName] = SENTINEL; - } - if (parentActiveFields) { - evalContext.parent = {}; - for (const fieldName in parentActiveFields) { - evalContext.parent[fieldName] = SENTINEL; - } - } - const evaluatedContext = makeContext([fields[fieldName].context, rawContext], evalContext); - for (const key in evaluatedContext) { - if (evaluatedContext[key] === SENTINEL || key.startsWith("default_")) { - // FIXME: this isn't perfect, a value might be evaluted to something else - // than the symbol because of the symbol - delete evaluatedContext[key]; - } - } - if (Object.keys(evaluatedContext).length > 0) { - return evaluatedContext; - } -} - export function getFieldsSpec( activeFields, fields, @@ -283,14 +240,13 @@ export function getFieldsSpec( fieldsSpec[fieldName].fields = { display_name: {} }; } if (["many2one", "one2many", "many2many"].includes(fields[fieldName].type)) { - const context = _getFieldContextSpec( - fieldName, - activeFields, - fields, - evalContext, - parentActiveFields - ); - if (context) { + let context = activeFields[fieldName].context; + if (!context || context === "{}") { + context = fields[fieldName].context || {}; + } else { + context = evalPartialContext(context, evalContext); + } + if (Object.keys(context).length > 0) { fieldsSpec[fieldName].context = context; } } diff --git a/addons/web/static/tests/core/context_tests.js b/addons/web/static/tests/core/context_tests.js index b09a4f8cd3a..75fe0ca6982 100644 --- a/addons/web/static/tests/core/context_tests.js +++ b/addons/web/static/tests/core/context_tests.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { makeContext } from "@web/core/context"; +import { evalPartialContext, makeContext } from "@web/core/context"; -QUnit.module("utils", {}, () => { +QUnit.module("Context", {}, () => { QUnit.module("makeContext"); QUnit.test("return empty context", (assert) => { @@ -42,4 +42,38 @@ QUnit.module("utils", {}, () => { assert.deepEqual(makeContext(["{'a': a + 1}"], { a: 1 }), { a: 2 }); assert.deepEqual(makeContext(["{'b': a + 1}"], { a: 1 }), { b: 2 }); }); + + QUnit.module("evalPartialContext"); + + QUnit.test("static contexts", (assert) => { + assert.deepEqual(evalPartialContext("{}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': 1}", {}), { a: 1 }); + assert.deepEqual(evalPartialContext("{'a': 'b'}", {}), { a: "b" }); + assert.deepEqual(evalPartialContext("{'a': true}", {}), { a: true }); + assert.deepEqual(evalPartialContext("{'a': None}", {}), { a: null }); + }); + + QUnit.test("complete dynamic contexts", (assert) => { + assert.deepEqual(evalPartialContext("{'a': a, 'b': 1}", { a: 2 }), { a: 2, b: 1 }); + }); + + QUnit.test("partial dynamic contexts", (assert) => { + assert.deepEqual(evalPartialContext("{'a': a}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': a, 'b': 1}", {}), { b: 1 }); + assert.deepEqual(evalPartialContext("{'a': a, 'b': b}", { a: 2 }), { a: 2 }); + }); + + QUnit.test("value of type obj (15)", (assert) => { + assert.deepEqual(evalPartialContext("{'a': a.b.c}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': a.b.c}", { a: {} }), {}); + assert.deepEqual(evalPartialContext("{'a': a.b.c}", { a: { b: { c: 2 } } }), { a: 2 }); + }); + + QUnit.test("value of type op (14)", (assert) => { + assert.deepEqual(evalPartialContext("{'a': a + 1}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': a + b}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': a + b}", { a: 2 }), {}); + assert.deepEqual(evalPartialContext("{'a': a + 1}", { a: 2 }), { a: 3 }); + assert.deepEqual(evalPartialContext("{'a': a + b}", { a: 2, b: 3 }), { a: 5 }); + }); }); diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index eb27925d1c6..9987c3aeea3 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -3833,6 +3833,52 @@ QUnit.module("Views", (hooks) => { await click(target.querySelector(".o_field_x2many_list_row_add a")); }); + QUnit.test("form with one2many with dynamic context", async (assert) => { + assert.expect(2); + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
+ + + + + + + `, + resId: 1, + mockRPC(route, args) { + if (args.method === "web_read") { + assert.deepEqual(args.kwargs.specification, { + display_name: {}, + int_field: {}, + p: { + fields: { + foo: {}, + }, + context: { static: 4 }, + limit: 40, + order: "", + }, + }); + } + if (args.method === "onchange2") { + assert.deepEqual(args.kwargs.context, { + dynamic: 20, + lang: "en", + static: 4, + tz: "taht", + uid: 7, + }); + } + }, + }); + + await addRow(target); + }); + QUnit.test("reference field in one2many list", async function (assert) { serverData.models.partner.records[0].reference = "partner,2"; serverData.views = {