From 26b9f975776cf23d0c9ccbb1dcebca9224ad5909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 27 Dec 2022 17:36:16 +0100 Subject: [PATCH] Use `&` and `|` as boolean operator in search widget Closes #11962, #8714 --- sao/CHANGELOG | 1 + sao/src/common.js | 7 +-- sao/tests/sao.js | 46 +++++++++--------- tryton/CHANGELOG | 1 + tryton/doc/usage.rst | 20 ++++---- tryton/tryton/common/domain_parser.py | 7 ++- .../tryton/tests/test_common_domain_parser.py | 48 +++++++++---------- 7 files changed, 70 insertions(+), 60 deletions(-) diff --git a/sao/CHANGELOG b/sao/CHANGELOG index cc4eeb8374..251dd2b91f 100644 --- a/sao/CHANGELOG +++ b/sao/CHANGELOG @@ -1,3 +1,4 @@ +* Use ``&`` and ``|`` as boolean operator in search widget * Use toggle button and label on translate dialog * Support border type of images * Manage domain on id in single value list as unique diff --git a/sao/src/common.js b/sao/src/common.js index 0feaad3c2b..dd66f3c71a 100644 --- a/sao/src/common.js +++ b/sao/src/common.js @@ -1306,7 +1306,7 @@ var nary = ' '; if ((domain[0] == 'AND') || (domain[0] == 'OR')) { if (domain[0] == 'OR') { - nary = ' or '; + nary = ' | '; } domain = domain.slice(1); } @@ -1729,11 +1729,12 @@ var result = []; operator = operator || 'or'; tokens = jQuery.extend([], tokens); + var notation = {'or': '|', 'and': '&'}[operator]; var test = function(value) { if (value instanceof Array) { - return Sao.common.compare(value, [operator]); + return Sao.common.compare(value, [notation]); } else { - return value == operator; + return value == notation; } }; var cur = tokens.shift(); diff --git a/sao/tests/sao.js b/sao/tests/sao.js index 46506bd6c8..5f9e88a080 100644 --- a/sao/tests/sao.js +++ b/sao/tests/sao.js @@ -1578,31 +1578,31 @@ [ [['a'], ['a']], - [['a', 'or', 'b'], [['OR', 'a', 'b']]], - [['a', 'or', 'b', 'or', 'c'], [['OR', ['OR', 'a', 'b'], 'c']]], - [['a', 'b', 'or', 'c'], ['a', ['OR', 'b', 'c']]], - [['a', 'or', 'b', 'c'], [['OR', 'a', 'b'], 'c']], + [['a', '|', 'b'], [['OR', 'a', 'b']]], + [['a', '|', 'b', '|', 'c'], [['OR', ['OR', 'a', 'b'], 'c']]], + [['a', 'b', '|', 'c'], ['a', ['OR', 'b', 'c']]], + [['a', '|', 'b', 'c'], [['OR', 'a', 'b'], 'c']], [['a', ['b', 'c']], ['a', ['b', 'c']]], [['a', ['b', 'c'], 'd'], ['a', ['b', 'c'], 'd']], - [['a', 'or', ['b', 'c']], [['OR', 'a', ['b', 'c']]]], - [['a', 'or', ['b', 'c'], 'd'], + [['a', '|', ['b', 'c']], [['OR', 'a', ['b', 'c']]]], + [['a', '|', ['b', 'c'], 'd'], [['OR', 'a', ['b', 'c']], 'd']], - [['a', ['b', 'c'], 'or', 'd'], + [['a', ['b', 'c'], '|', 'd'], ['a', ['OR', ['b', 'c'], 'd']]], - [['a', 'or', ['b', 'or', 'c']], + [['a', '|', ['b', '|', 'c']], [['OR', 'a', [['OR', 'b', 'c']]]]], - [['or'], []], - [['or', 'a'], ['a']], - [['a', ['or', 'b']], ['a', ['b']]], - [['a', 'or', 'or', 'b'], [['OR', 'a', 'b']]], - [['or', 'or', 'a'], ['a']], - [['or', 'or', 'a', 'b'], ['a', 'b']], - [['or', 'or', 'a', 'or', 'b'], [['OR', 'a', 'b']]], - [['a', ['b', 'or', 'c']], ['a', [['OR', 'b', 'c']]]], - [[a, [b, ['or'], c]], [a, [['OR', b, c]]]], - [['a', ['b', 'or']], ['a', [['OR', 'b']]]], + [['|'], []], + [['|', 'a'], ['a']], + [['a', ['|', 'b']], ['a', ['b']]], + [['a', '|', '|', 'b'], [['OR', 'a', 'b']]], + [['|', '|', 'a'], ['a']], + [['|', '|', 'a', 'b'], ['a', 'b']], + [['|', '|', 'a', '|', 'b'], [['OR', 'a', 'b']]], + [['a', ['b', '|', 'c']], ['a', [['OR', 'b', 'c']]]], + [[a, [b, ['|'], c]], [a, [['OR', b, c]]]], + [['a', ['b', '|']], ['a', [['OR', 'b']]]], [[null_], [null_]], - [[null_, 'or', double_null_], [['OR', null_, double_null_]]] + [[null_, '|', double_null_], [['OR', null_, double_null_]]] ].forEach(function(test) { var value = test[0]; var result = test[1]; @@ -2233,14 +2233,14 @@ [['OR', ['name', 'ilike', '%Doe%'], ['name', 'ilike', '%Jane%'] - ], 'Name: Doe or Name: Jane'], + ], 'Name: Doe | Name: Jane'], [[ ['name', 'ilike', '%Doe%'], ['OR', ['name', 'ilike', '%John%'], ['name', 'ilike', '%Jane%'] ] - ], 'Name: Doe (Name: John or Name: Jane)'], + ], 'Name: Doe (Name: John | Name: Jane)'], [[], ''], [[['surname', 'ilike', '%Doe%']], '"(Sur)Name": Doe'], //[[['date', '>=', new Date(2012, 10, 24)]], 'Date: >=10/24/2012'], @@ -3112,8 +3112,8 @@ ['Name: !=foo', []], ['', ["Name: "]], [' ', ["", "Name: "]], - ["Name: foo or", ["Name: foo"]], - ['Name: foo (Name: foo or N', ["Name: foo (Name: foo or Name: "]], + ["Name: foo |", ["Name: foo"]], + ['Name: foo (Name: foo | N', ["Name: foo (Name: foo | Name: "]], ].forEach(function(test) { var value = test[0]; var expected = test[1]; diff --git a/tryton/CHANGELOG b/tryton/CHANGELOG index d660d0cb3c..8572ac844c 100644 --- a/tryton/CHANGELOG +++ b/tryton/CHANGELOG @@ -1,3 +1,4 @@ +* Use ``&`` and ``|`` as boolean operator in search widget * Use toggle button and label on translate dialog * Manage domain on id in single value list as unique * Do not validate domain and enforce uniqueness for empty fields unless they're required diff --git a/tryton/doc/usage.rst b/tryton/doc/usage.rst index 5cfd69c071..130caca511 100644 --- a/tryton/doc/usage.rst +++ b/tryton/doc/usage.rst @@ -512,22 +512,26 @@ It is possible to escape special characters in values by using double quotes. Clause composition ++++++++++++++++++ -The clauses can be composed using the two boolean operators ``and`` and ``or``. -By default, there is an implicit ``and`` between each clause if no operator is +The clauses can be composed using the two boolean operators ``&`` (for `logical +conjunction`_) and ``|`` (for `logical disjunction`_). +By default, there is an implicit ``&`` between each clause if no operator is specified. For example: ``Name: Michael Amount: 100`` - is the same as ``Name: Michael and Amount: 100`` + is the same as ``Name: Michael & Amount: 100`` -The ``and`` operator has a highest precedence than ``or`` but you can change it -by using parenthesis. +The ``&`` operator has a highest precedence than ``|`` but you can change it by +using parenthesis. - For example: ``(Name: Michael or Name: Pam) and Amount: 100`` + For example: ``(Name: Michael | Name: Pam) & Amount: 100`` - is different than ``Name: Michael or Name: Pam and Amount: 100`` + is different than ``Name: Michael | Name: Pam & Amount: 100`` - which is evaluated as ``Name: Michael or (Name: Pam and Amount: 100)`` + which is evaluated as ``Name: Michael | (Name: Pam & Amount: 100)`` + +.. _logical conjunction: https://en.wikipedia.org/wiki/Logical_conjunction +.. _logical disjunction: https://en.wikipedia.org/wiki/Logical_disjunction RichText Editor ^^^^^^^^^^^^^^^ diff --git a/tryton/tryton/common/domain_parser.py b/tryton/tryton/common/domain_parser.py index 24fc800b20..52b729bc82 100644 --- a/tryton/tryton/common/domain_parser.py +++ b/tryton/tryton/common/domain_parser.py @@ -484,7 +484,10 @@ def parenthesize(tokens): def operatorize(tokens, operator='or'): "Convert operators" - test = (operator, (operator,)) + test = { + 'or': ('|', ('|',)), + 'and': ('&', ('&',)), + }[operator] try: cur = next(tokens) while cur in test: @@ -656,7 +659,7 @@ class DomainParser(object): if not domain: return '' if domain[0] in ('AND', 'OR'): - nary = ' ' if domain[0] == 'AND' else ' or ' + nary = ' ' if domain[0] == 'AND' else ' | ' domain = domain[1:] else: nary = ' ' diff --git a/tryton/tryton/tests/test_common_domain_parser.py b/tryton/tryton/tests/test_common_domain_parser.py index b9c91e72ff..6d469adade 100644 --- a/tryton/tryton/tests/test_common_domain_parser.py +++ b/tryton/tryton/tests/test_common_domain_parser.py @@ -529,31 +529,31 @@ class DomainParserTestCase(TestCase): double_null_ = ('e', None, None) for value, result in ( (['a'], ['a']), - (['a', 'or', 'b'], [['OR', 'a', 'b']]), - (['a', 'or', 'b', 'or', 'c'], [['OR', ['OR', 'a', 'b'], 'c']]), - (['a', 'b', 'or', 'c'], ['a', ['OR', 'b', 'c']]), - (['a', 'or', 'b', 'c'], [['OR', 'a', 'b'], 'c']), + (['a', '|', 'b'], [['OR', 'a', 'b']]), + (['a', '|', 'b', '|', 'c'], [['OR', ['OR', 'a', 'b'], 'c']]), + (['a', 'b', '|', 'c'], ['a', ['OR', 'b', 'c']]), + (['a', '|', 'b', 'c'], [['OR', 'a', 'b'], 'c']), (['a', iter(['b', 'c'])], ['a', ['b', 'c']]), (['a', iter(['b', 'c']), 'd'], ['a', ['b', 'c'], 'd']), - (['a', 'or', iter(['b', 'c'])], [['OR', 'a', ['b', 'c']]]), - (['a', 'or', iter(['b', 'c']), 'd'], + (['a', '|', iter(['b', 'c'])], [['OR', 'a', ['b', 'c']]]), + (['a', '|', iter(['b', 'c']), 'd'], [['OR', 'a', ['b', 'c']], 'd']), - (['a', iter(['b', 'c']), 'or', 'd'], + (['a', iter(['b', 'c']), '|', 'd'], ['a', ['OR', ['b', 'c'], 'd']]), - (['a', 'or', iter(['b', 'or', 'c'])], + (['a', '|', iter(['b', '|', 'c'])], [['OR', 'a', [['OR', 'b', 'c']]]]), - (['or'], []), - (['or', 'a'], ['a']), - (['a', iter(['or', 'b'])], ['a', ['b']]), - (['a', 'or', 'or', 'b'], [['OR', 'a', 'b']]), - (['or', 'or', 'a'], ['a']), - (['or', 'or', 'a', 'b'], ['a', 'b']), - (['or', 'or', 'a', 'or', 'b'], [['OR', 'a', 'b']]), - (['a', iter(['b', 'or', 'c'])], ['a', [['OR', 'b', 'c']]]), - ([a, iter([b, ('or',), c])], [a, [['OR', b, c]]]), - (['a', iter(['b', 'or'])], ['a', [['OR', 'b']]]), + (['|'], []), + (['|', 'a'], ['a']), + (['a', iter(['|', 'b'])], ['a', ['b']]), + (['a', '|', '|', 'b'], [['OR', 'a', 'b']]), + (['|', '|', 'a'], ['a']), + (['|', '|', 'a', 'b'], ['a', 'b']), + (['|', '|', 'a', '|', 'b'], [['OR', 'a', 'b']]), + (['a', iter(['b', '|', 'c'])], ['a', [['OR', 'b', 'c']]]), + ([a, iter([b, ('|',), c])], [a, [['OR', b, c]]]), + (['a', iter(['b', '|'])], ['a', [['OR', 'b']]]), ([null_], [null_]), - ([null_, 'or', double_null_], [['OR', null_, double_null_]]), + ([null_, '|', double_null_], [['OR', null_, double_null_]]), ): self.assertEqual( rlist(operatorize(iter(value))), result, @@ -696,14 +696,14 @@ class DomainParserTestCase(TestCase): dom.string(['OR', ('name', 'ilike', '%Doe%'), ('name', 'ilike', '%Jane%')]), - 'Name: Doe or Name: Jane') + 'Name: Doe | Name: Jane') self.assertEqual( dom.string([ ('name', 'ilike', '%Doe%'), ['OR', ('name', 'ilike', '%John%'), ('name', 'ilike', '%Jane%')]]), - 'Name: Doe (Name: John or Name: Jane)') + 'Name: Doe (Name: John | Name: Jane)') self.assertEqual(dom.string([]), '') self.assertEqual( dom.string([('surname', 'ilike', '%Doe%')]), '"(Sur)Name": Doe') @@ -1121,10 +1121,10 @@ class DomainParserTestCase(TestCase): self.assertEqual(list(dom.completion('Name: !=foo')), []) self.assertEqual(list(dom.completion('')), ['Name: ']) self.assertEqual(list(dom.completion(' ')), ['', 'Name: ']) - self.assertEqual(list(dom.completion('Name: foo or')), ['Name: foo']) + self.assertEqual(list(dom.completion('Name: foo |')), ['Name: foo']) self.assertEqual( - list(dom.completion('Name: foo (Name: foo or N')), - ['Name: foo (Name: foo or Name: ']) + list(dom.completion('Name: foo (Name: foo | N')), + ['Name: foo (Name: foo | Name: ']) def test_completion_many2one(self): "Test completion many2one"