[FIX] web: can partially evaluate complex dynamic context

Have a form view with an x2many field, and a context on that field,
depending on other fields of the view. For instance:
 ```xml
   <form>
     <field name="a"/>
     <field name="b_ids" context="{'key1': a, 'key2': 4}"/>
   </form>
 ```

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
    <field name="b_ids" context="{'key1': a + b}"/>
 ```
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 <ged@odoo.com>
This commit is contained in:
Aaron Bohy
2023-07-28 14:54:02 +02:00
parent 89835ec67a
commit 7dc6958334
4 changed files with 114 additions and 55 deletions
+24 -1
View File
@@ -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;
}
@@ -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;
}
}
+36 -2
View File
@@ -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 });
});
});
@@ -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: `
<form>
<field name="int_field"/>
<field name="p" editable="bottom" context="{'static': 4, 'dynamic': int_field * 2}">
<tree>
<field name="foo"/>
</tree>
</field>
</form>`,
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 = {