[IMP] web: support set intersections in condition tree

Now that some field attributes like invisible are given by Python
expressions that can involve set operations, we want to be able to easily
edit those expressions in the expression editor. For this we have to
improve a bit the mapping expression <-> condition tree, in order to
have simple rewrittings like
"set(user_ids).intersection([1, 2])" <-> condition('user_ids', 'in', [1, 2]).

Part-of: odoo/odoo#139451
This commit is contained in:
Mathieu Duckerts-Antoine
2023-10-25 17:03:19 +00:00
parent febece1371
commit 39e5124170
2 changed files with 464 additions and 20 deletions
@@ -335,7 +335,7 @@ function not(ast) {
return { ...ast, value: !ast.value };
}
if (ast.type === 7 && COMPARATORS.includes(ast.op)) {
return { ...ast, op: TERM_OPERATORS_NEGATION_EXTENDED[ast.op] };
return { ...ast, op: TERM_OPERATORS_NEGATION_EXTENDED[ast.op] }; // do not use this if ast is within a domain context!
}
return { type: 6, op: "not", right: isBool(ast) ? ast.args[0] : ast };
}
@@ -366,14 +366,16 @@ function isNot(ast) {
function is(oneParamFunc, ast) {
return (
ast.type === 8 &&
ast.fn &&
ast.fn.type === 5 &&
ast.fn.value === oneParamFunc &&
ast.args &&
ast.args.length === 1
); // improve condition?
}
function isSet(ast) {
return ast.type === 8 && ast.fn.type === 5 && ast.fn.value === "set" && ast.args.length <= 1;
}
function isBool(ast) {
return is("bool", ast);
}
@@ -386,6 +388,14 @@ function isValidPath(ast, options) {
return false;
}
function isX2Many(ast, options) {
if (isValidPath(ast, options)) {
const fieldDef = options.getFieldDef(ast.value); // safe: isValidPath has not returned null;
return ["many2many", "one2many"].includes(fieldDef.type);
}
return false;
}
function _getConditionFromComparator(ast, options) {
if (["is", "is not"].includes(ast.op)) {
// we could do something smarter here
@@ -407,8 +417,9 @@ function _getConditionFromComparator(ast, options) {
if (!isValidPath(left, options)) {
if (operator in EXCHANGE) {
left = ast.right;
right = ast.left;
const temp = left;
left = right;
right = temp;
operator = EXCHANGE[operator];
} else {
return null;
@@ -418,6 +429,60 @@ function _getConditionFromComparator(ast, options) {
return condition(left.value, operator, toValue(right));
}
function isValidPath2(ast, options) {
if (!ast) {
return null;
}
if ([4, 10].includes(ast.type) && ast.value.length === 1) {
return isValidPath(ast.value[0], options);
}
return isValidPath(ast, options);
}
function _getConditionFromIntersection(ast, options, negate = false) {
let left = ast.fn.obj.args[0];
let right = ast.args[0];
if (!left) {
return condition(negate ? 1 : 0, "=", 1);
}
// left/right exchange
if (isValidPath2(left, options) == isValidPath2(right, options)) {
return null;
}
if (!isValidPath2(left, options)) {
const temp = left;
left = right;
right = temp;
}
if ([4, 10].includes(left.type) && left.value.length === 1) {
left = left.value[0];
}
if (!right) {
return condition(left.value, negate ? "=" : "!=", false);
}
// try to extract the ast of an iterable
// we only make simple conversions here
if (isSet(right)) {
if (!right.args[0]) {
right = { type: 4, value: [] };
}
if ([4, 10].includes(right.args[0].type)) {
right = right.args[0];
}
}
if (![4, 10].includes(right.type)) {
return null;
}
return condition(left.value, negate ? "not in" : "in", toValue(right));
}
/**
* @param {AST} ast
* @param {Options} options
@@ -438,6 +503,18 @@ function _leafFromAST(ast, options, negate = false) {
return condition(astValue ? 1 : 0, "=", 1);
}
if (
ast.type === 8 &&
ast.fn.type === 15 /** object lookup */ &&
isSet(ast.fn.obj) &&
ast.fn.key === "intersection"
) {
const tree = _getConditionFromIntersection(ast, options, negate);
if (tree) {
return tree;
}
}
if (ast.type === 7 && COMPARATORS.includes(ast.op)) {
if (negate) {
return _leafFromAST(not(ast), options);
@@ -448,7 +525,7 @@ function _leafFromAST(ast, options, negate = false) {
}
}
// no conclusive way to transform ast in a condition
// no conclusive/simple way to transform ast in a condition
return complexCondition(formatAST(negate ? not(ast) : ast));
}
@@ -562,6 +639,37 @@ function _expressionFromTree(tree, options, isRoot = false) {
return formatAST(operator === "=" ? not(pathAST) : pathAST);
}
if (
pathAST.type === 5 &&
isValidPath(pathAST, options) &&
["in", "not in"].includes(operator)
) {
const setIteratorAST = isX2Many(pathAST, options) ? pathAST : { type: 4, value: [pathAST] };
const valueAST = toAST(value);
const otherIteratorAST = [4, 10].includes(valueAST.type)
? valueAST
: { type: 4, value: [valueAST] };
const ast = {
type: 8,
fn: {
type: 15,
obj: {
args: [setIteratorAST],
type: 8,
fn: {
type: 5,
value: "set",
},
},
key: "intersection",
},
args: [otherIteratorAST],
};
return formatAST(operator === "not in" ? not(ast) : ast);
}
// add case true for boolean fields
return formatAST({
@@ -243,41 +243,125 @@ QUnit.test("domainFromExpression", function (assert) {
QUnit.test("expressionFromTree", function (assert) {
const options = {
getFieldDef: (name) => (name === "x" ? {} /** any field */ : null),
getFieldDef: (name) => {
if (["foo", "bar"].includes(name)) {
return {}; // any field
}
if (["foo_ids", "bar_ids"].includes(name)) {
return { type: "many2many" };
}
return null;
},
};
const toTest = [
{
expressionTree: condition("x", "=", false),
result: `not x`,
expressionTree: condition("foo", "=", false),
result: `not foo`,
},
{
expressionTree: condition("x", "=", false, true),
result: `x`,
expressionTree: condition("foo", "=", false, true),
result: `foo`,
},
{
expressionTree: condition("x", "!=", false),
result: `x`,
expressionTree: condition("foo", "!=", false),
result: `foo`,
},
{
expressionTree: condition("x", "!=", false, true),
result: `not x`,
expressionTree: condition("foo", "!=", false, true),
result: `not foo`,
},
{
expressionTree: condition("y", "=", false),
result: `not "y"`,
},
{
expressionTree: condition("x", "between", [1, 3]),
result: `x >= 1 and x <= 3`,
expressionTree: condition("foo", "between", [1, 3]),
result: `foo >= 1 and foo <= 3`,
},
{
expressionTree: condition("x", "between", [1, expression("uid")], true),
result: `not ( x >= 1 and x <= uid )`,
expressionTree: condition("foo", "between", [1, expression("uid")], true),
result: `not ( foo >= 1 and foo <= uid )`,
},
{
expressionTree: complexCondition("uid"),
result: `uid`,
},
{
expressionTree: condition("foo_ids", "in", []),
result: `set(foo_ids).intersection([])`,
},
{
expressionTree: condition("foo_ids", "in", [1]),
result: `set(foo_ids).intersection([1])`,
},
{
expressionTree: condition("foo_ids", "in", 1),
result: `set(foo_ids).intersection([1])`,
},
{
expressionTree: condition("foo", "in", []),
result: `set([foo]).intersection([])`,
},
{
expressionTree: condition(expression("expr"), "in", []),
result: `expr in []`,
},
{
expressionTree: condition("foo", "in", [1]),
result: `set([foo]).intersection([1])`,
},
{
expressionTree: condition("foo", "in", 1),
result: `set([foo]).intersection([1])`,
},
{
expressionTree: condition("y", "in", []),
result: `"y" in []`,
},
{
expressionTree: condition("y", "in", [1]),
result: `"y" in [1]`,
},
{
expressionTree: condition("y", "in", 1),
result: `"y" in 1`,
},
{
expressionTree: condition("foo_ids", "not in", []),
result: `not set(foo_ids).intersection([])`,
},
{
expressionTree: condition("foo_ids", "not in", [1]),
result: `not set(foo_ids).intersection([1])`,
},
{
expressionTree: condition("foo_ids", "not in", 1),
result: `not set(foo_ids).intersection([1])`,
},
{
expressionTree: condition("foo", "not in", []),
result: `not set([foo]).intersection([])`,
},
{
expressionTree: condition("foo", "not in", [1]),
result: `not set([foo]).intersection([1])`,
},
{
expressionTree: condition("foo", "not in", 1),
result: `not set([foo]).intersection([1])`,
},
{
expressionTree: condition("y", "not in", []),
result: `"y" not in []`,
},
{
expressionTree: condition("y", "not in", [1]),
result: `"y" not in [1]`,
},
{
expressionTree: condition("y", "not in", 1),
result: `"y" not in 1`,
},
];
for (const { expressionTree, result, extraOptions } of toTest) {
const o = { ...options, ...extraOptions };
@@ -291,7 +375,7 @@ QUnit.test("treeFromExpression", function (assert) {
if (["foo", "bar"].includes(name)) {
return {}; // any field
}
if (name === "foo_ids") {
if (["foo_ids", "bar_ids"].includes(name)) {
return { type: "many2many" };
}
return null;
@@ -365,6 +449,122 @@ QUnit.test("treeFromExpression", function (assert) {
]),
]),
},
{
expression: `set()`,
result: complexCondition(`set()`),
},
{
expression: `set([1, 2])`,
result: complexCondition(`set([1, 2])`),
},
{
expression: `set(foo_ids).intersection([1, 2])`,
result: condition("foo_ids", "in", [1, 2]),
},
{
expression: `set(foo_ids).intersection(set([1, 2]))`,
result: condition("foo_ids", "in", [1, 2]),
},
{
expression: `set(foo_ids).intersection(set((1, 2)))`,
result: condition("foo_ids", "in", [1, 2]),
},
{
expression: `set(foo_ids).intersection("ab")`,
result: complexCondition(`set(foo_ids).intersection("ab")`),
},
{
expression: `set([1, 2]).intersection(foo_ids)`,
result: condition("foo_ids", "in", [1, 2]),
},
{
expression: `set(set([1, 2])).intersection(foo_ids)`,
result: condition("foo_ids", "in", [1, 2]),
},
{
expression: `set((1, 2)).intersection(foo_ids)`,
result: condition("foo_ids", "in", [1, 2]),
},
{
expression: `set("ab").intersection(foo_ids)`,
result: complexCondition(`set("ab").intersection(foo_ids)`),
},
{
expression: `set([2, 3]).intersection([1, 2])`,
result: complexCondition(`set([2, 3]).intersection([1, 2])`),
},
{
expression: `set(foo_ids).intersection(bar_ids)`,
result: complexCondition(`set(foo_ids).intersection(bar_ids)`),
},
{
expression: `set().intersection(foo_ids)`,
result: condition(0, "=", 1),
},
{
expression: `set(foo_ids).intersection()`,
result: condition("foo_ids", "set", false),
},
{
expression: `not set().intersection(foo_ids)`,
result: condition(1, "=", 1),
},
{
expression: `not set(foo_ids).intersection()`,
result: condition("foo_ids", "not_set", false),
},
{
expression: `not set(foo_ids).intersection([1, 2])`,
result: condition("foo_ids", "not in", [1, 2]),
},
{
expression: `not set(foo_ids).intersection(set([1, 2]))`,
result: condition("foo_ids", "not in", [1, 2]),
},
{
expression: `not set(foo_ids).intersection(set((1, 2)))`,
result: condition("foo_ids", "not in", [1, 2]),
},
{
expression: `not set(foo_ids).intersection("ab")`,
result: complexCondition(`not set(foo_ids).intersection("ab")`),
},
{
expression: `not set([1, 2]).intersection(foo_ids)`,
result: condition("foo_ids", "not in", [1, 2]),
},
{
expression: `not set(set([1, 2])).intersection(foo_ids)`,
result: condition("foo_ids", "not in", [1, 2]),
},
{
expression: `not set((1, 2)).intersection(foo_ids)`,
result: condition("foo_ids", "not in", [1, 2]),
},
{
expression: `not set("ab").intersection(foo_ids)`,
result: complexCondition(`not set("ab").intersection(foo_ids)`),
},
{
expression: `not set([2, 3]).intersection([1, 2])`,
result: complexCondition(`not set([2, 3]).intersection([1, 2])`),
},
{
expression: `not set(foo_ids).intersection(bar_ids)`,
result: complexCondition(`not set(foo_ids).intersection(bar_ids)`),
},
{
expression: `set(foo_ids).difference([1, 2])`,
result: complexCondition(`set(foo_ids).difference([1, 2])`),
},
{
expression: `set(foo_ids).union([1, 2])`,
result: complexCondition(`set(foo_ids).union([1, 2])`),
},
{
expression: `expr in []`,
result: complexCondition(`expr in []`),
},
];
for (const { expression, result, extraOptions } of toTest) {
const o = { ...options, ...extraOptions };
@@ -445,6 +645,142 @@ QUnit.test("expressionFromTree . treeFromExpression", function (assert) {
expression: `not context.get("toto")`,
result: `not context.get("toto")`,
},
{
expression: `set()`,
result: `set()`,
},
{
expression: `set([1, 2])`,
result: `set([1, 2])`,
},
{
expression: `set(foo_ids).intersection([1, 2])`,
result: `set(foo_ids).intersection([1, 2])`,
},
{
expression: `set(foo_ids).intersection(set([1, 2]))`,
result: `set(foo_ids).intersection([1, 2])`,
},
{
expression: `set(foo_ids).intersection(set((1, 2)))`,
result: `set(foo_ids).intersection([1, 2])`,
},
{
expression: `set(foo_ids).intersection("ab")`,
result: `set(foo_ids).intersection("ab")`,
},
{
expression: `set([1, 2]).intersection(foo_ids)`,
result: `set(foo_ids).intersection([1, 2])`,
},
{
expression: `set(set([1, 2])).intersection(foo_ids)`,
result: `set(foo_ids).intersection([1, 2])`,
},
{
expression: `set((1, 2)).intersection(foo_ids)`,
result: `set(foo_ids).intersection([1, 2])`,
},
{
expression: `set("ab").intersection(foo_ids)`,
result: `set("ab").intersection(foo_ids)`,
},
{
expression: `set([2, 3]).intersection([1, 2])`,
result: `set([2, 3]).intersection([1, 2])`,
},
{
expression: `set(foo_ids).intersection(bar_ids)`,
result: `set(foo_ids).intersection(bar_ids)`,
},
{
expression: `set().intersection(foo_ids)`,
result: `False`,
},
{
expression: `set(foo_ids).intersection()`,
result: `foo_ids`,
},
{
expression: `not set(foo_ids).intersection([1, 2])`,
result: `not set(foo_ids).intersection([1, 2])`,
},
{
expression: `not set(foo_ids).intersection(set([1, 2]))`,
result: `not set(foo_ids).intersection([1, 2])`,
},
{
expression: `not set(foo_ids).intersection(set((1, 2)))`,
result: `not set(foo_ids).intersection([1, 2])`,
},
{
expression: `not set(foo_ids).intersection("ab")`,
result: `not set(foo_ids).intersection("ab")`,
},
{
expression: `not set([1, 2]).intersection(foo_ids)`,
result: `not set(foo_ids).intersection([1, 2])`,
},
{
expression: `not set(set([1, 2])).intersection(foo_ids)`,
result: `not set(foo_ids).intersection([1, 2])`,
},
{
expression: `not set((1, 2)).intersection(foo_ids)`,
result: `not set(foo_ids).intersection([1, 2])`,
},
{
expression: `not set("ab").intersection(foo_ids)`,
result: `not set("ab").intersection(foo_ids)`,
},
{
expression: `not set([2, 3]).intersection([1, 2])`,
result: `not set([2, 3]).intersection([1, 2])`,
},
{
expression: `not set(foo_ids).intersection(bar_ids)`,
result: `not set(foo_ids).intersection(bar_ids)`,
},
{
expression: `set(foo_ids).difference([1, 2])`,
result: `set(foo_ids).difference([1, 2])`,
},
{
expression: `set(foo_ids).intersection([1, 2])`,
result: `set(foo_ids).intersection([1, 2])`,
},
{
expression: `set([foo]).intersection()`,
result: `foo`,
},
{
expression: `set([foo]).intersection([1, 2])`,
result: `set([foo]).intersection([1, 2])`,
},
{
expression: `set().intersection([foo])`,
result: `False`,
},
{
expression: `set([1, 2]).intersection([foo])`,
result: `set([foo]).intersection([1, 2])`,
},
{
expression: `not set([foo]).intersection()`,
result: `not foo`,
},
{
expression: `not set([foo]).intersection([1, 2])`,
result: `not set([foo]).intersection([1, 2])`,
},
{
expression: `not set().intersection([foo])`,
result: `True`,
},
{
expression: `not set([1, 2]).intersection([foo])`,
result: `not set([foo]).intersection([1, 2])`,
},
];
for (const { expression, result, extraOptions } of toTest) {
const o = { ...options, ...extraOptions };