From e2d8b6b2958620e38d6e15010afa5f8d3bc5cfbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Mon, 30 Dec 2019 14:26:14 +0100 Subject: [PATCH] Add MultiSelection entry to Dict field issue8903 review284571003 --- CHANGELOG | 1 + trytond/ir/message.xml | 3 + trytond/model/dictschema.py | 13 +-- trytond/model/fields/dict.py | 44 +++++++--- trytond/tests/test_field_dict.py | 141 +++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+), 16 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 82992ac1bd..c082756c06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add MultiSelection entry to Dict field * Allow empty order clause * Change editable tree attribute into boolean * Send default order to clients diff --git a/trytond/ir/message.xml b/trytond/ir/message.xml index 53e6f94d59..b252a7357b 100644 --- a/trytond/ir/message.xml +++ b/trytond/ir/message.xml @@ -90,6 +90,9 @@ this repository contains the full copyright notices and license terms. --> Selection + + MultiSelection + Digits diff --git a/trytond/model/dictschema.py b/trytond/model/dictschema.py index 0e2c570db1..f03e865abf 100644 --- a/trytond/model/dictschema.py +++ b/trytond/model/dictschema.py @@ -42,6 +42,8 @@ class DictSchemaMixin(object): ('date', lazy_gettext('ir.msg_dict_schema_date')), ('datetime', lazy_gettext('ir.msg_dict_schema_datetime')), ('selection', lazy_gettext('ir.msg_dict_schema_selection')), + ('multiselection', + lazy_gettext('ir.msg_dict_schema_multiselection')), ], lazy_gettext('ir.msg_dict_schema_type'), required=True) digits = fields.Integer( lazy_gettext('ir.msg_dict_schema_digits'), @@ -52,19 +54,20 @@ class DictSchemaMixin(object): selection = fields.Text( lazy_gettext('ir.msg_dict_schema_selection'), states={ - 'invisible': Eval('type_') != 'selection', + 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), }, translate=True, depends=['type_'], help=lazy_gettext('ir.msg_dict_schema_selection_help')) selection_sorted = fields.Boolean( lazy_gettext('ir.msg_dict_schema_selection_sorted'), states={ - 'invisible': Eval('type_') != 'selection', + 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), }, depends=['type_'], help=lazy_gettext('ir.msg_dict_schema_selection_sorted_help')) selection_json = fields.Function(fields.Char( lazy_gettext('ir.msg_dict_schema_selection_json'), states={ - 'invisible': Eval('type_') != 'selection', + 'invisible': ~Eval('type_').in_( + ['selection', 'multiselection']), }, depends=['type_']), 'get_selection_json') @@ -117,7 +120,7 @@ class DictSchemaMixin(object): @classmethod def check_selection(cls, schemas): for schema in schemas: - if schema.type_ != 'selection': + if schema.type_ not in {'selection', 'multiselection'}: continue try: dict(json.loads(schema.get_selection_json())) @@ -147,7 +150,7 @@ class DictSchemaMixin(object): 'domain': record.domain, 'sequence': getattr(record, 'sequence', record.name), } - if record.type_ == 'selection': + if record.type_ in {'selection', 'multiselection'}: with Transaction().set_context(language=Config.get_language()): english_key = cls(record.id) selection = OrderedDict(json.loads( diff --git a/trytond/model/fields/dict.py b/trytond/model/fields/dict.py index 58172682c3..b34c92019c 100644 --- a/trytond/model/fields/dict.py +++ b/trytond/model/fields/dict.py @@ -47,7 +47,14 @@ class Dict(Field): def sql_format(self, value): value = super().sql_format(value) if isinstance(value, dict): - value = dumps({k: v for k, v in value.items() if v is not None}) + d = {} + for k, v in value.items(): + if v is None: + continue + if isinstance(v, list): + v = list(sorted(set(v))) + d[k] = v + value = dumps(d) return value def translated(self, name=None, type_='values'): @@ -74,6 +81,8 @@ class Dict(Field): value = int(value) if isinstance(value, (Select, CombiningQuery)): return value + if isinstance(value, (list, tuple)): + value = sorted(set(value)) if operator.endswith('in'): return [dumps(v) for v in value] else: @@ -101,33 +110,46 @@ class Dict(Field): table, _ = tables[None] name, key = name.split('.', 1) Operator = SQL_OPERATORS[operator] - column = self.sql_column(table) - column = self._domain_column(operator, column, key) + raw_column = self.sql_column(table) + column = self._domain_column(operator, raw_column, key) expression = Operator(column, self._domain_value(operator, value)) if operator in {'=', '!='}: # Try to use custom operators in case there is indexes - raw_column = self.sql_column(table) try: if value is None: expression = database.json_key_exists( raw_column, key) if operator == '=': expression = operators.Not(expression) - return expression - else: + # we compare on multi-selection by doing an equality check and + # not a contain check + elif not isinstance(value, (list, tuple)): expression = database.json_contains( raw_column, dumps({key: value})) if operator == '!=': expression = operators.Not(expression) expression &= database.json_key_exists( raw_column, key) - return expression + return expression except NotImplementedError: pass - if isinstance(expression, operators.In) and not expression.right: - expression = Literal(False) - elif isinstance(expression, operators.NotIn) and not expression.right: - expression = Literal(True) + elif operator.endswith('in'): + # Try to use custom operators in case there is indexes + if not value: + expression = Literal(operator.startswith('not')) + else: + op = '!=' if operator.startswith('not') else '=' + try: + in_expr = Literal(False) + for v in value: + in_expr |= database.json_contains( + self._domain_column(op, raw_column, key), + dumps(v)) + if operator.startswith('not'): + in_expr = ~in_expr + expression = in_expr + except NotImplementedError: + pass expression = self._domain_add_null(column, operator, value, expression) return expression diff --git a/trytond/tests/test_field_dict.py b/trytond/tests/test_field_dict.py index d8c28e46d4..37e2b103da 100644 --- a/trytond/tests/test_field_dict.py +++ b/trytond/tests/test_field_dict.py @@ -36,6 +36,17 @@ class FieldDictTestCase(unittest.TestCase): 'type_': 'selection', 'selection': ('arabic: Arabic\n' 'hexa: Hexadecimal'), + }, { + 'name': 'countries', + 'string': 'Countries', + 'type_': 'multiselection', + 'selection': ( + 'au: Australia\n' + 'be: Belgium\n' + 'ca: Canada\n' + 'de: Germany\n' + 'es: Spain\n' + 'fr: France'), }]) def set_jsonb(self, table): @@ -130,6 +141,18 @@ class FieldDictTestCase(unittest.TestCase): self.assertDictEqual(dict_.dico, {'type': 'arabic'}) + @with_transaction() + def test_create_multiselection(self): + "Test create dict with multi-selection" + Dict = Pool().get('test.dict') + self.create_schema() + + dict_, = Dict.create([{ + 'dico': {'countries': ['fr', 'be']}, + }]) + + self.assertDictEqual(dict_.dico, {'countries': ['be', 'fr']}) + @with_transaction() def test_invalid_selection_schema(self): "Test invalid selection schema" @@ -234,6 +257,31 @@ class FieldDictTestCase(unittest.TestCase): self.assertListEqual(dicts_bar, []) self.assertListEqual(dicts_foo_b, []) + @with_transaction() + def test_search_multiselection_equals(self): + "Test search dict multi-selection equals" + pool = Pool() + Dict = pool.get('test.dict') + self.create_schema() + + dict_, = Dict.create([{ + 'dico': {'countries': ['fr', 'be']}, + }]) + + france_belgium = Dict.search([ + ('dico.countries', '=', ['be', 'fr']), + ]) + belgium = Dict.search([ + ('dico.countries', '=', ['be']), + ]) + germany = Dict.search([ + ('dico.countries', '=', ['de']), + ]) + + self.assertEqual(france_belgium, [dict_]) + self.assertEqual(belgium, []) + self.assertEqual(germany, []) + @with_transaction() def test_search_element_equals_none(self): "Test search dict element equals None" @@ -290,6 +338,35 @@ class FieldDictTestCase(unittest.TestCase): self.assertListEqual(dicts_bar, [dict_]) self.assertListEqual(dicts_foo_b, []) + @with_transaction() + def test_search_multiselection_not_equals(self): + "Test search dict multi-selection not equals" + pool = Pool() + Dict = pool.get('test.dict') + self.create_schema() + + dict_, = Dict.create([{ + 'dico': {'countries': ['fr', 'be']}, + }]) + + not_france_belgium = Dict.search([ + ('dico.countries', '!=', ['be', 'fr']), + ]) + not_belgium = Dict.search([ + ('dico.countries', '!=', ['be']), + ]) + not_germany = Dict.search([ + ('dico.countries', '!=', ['de']), + ]) + not_empty = Dict.search([ + ('dico.countries', '!=', []), + ]) + + self.assertEqual(not_france_belgium, []) + self.assertEqual(not_belgium, [dict_]) + self.assertEqual(not_germany, [dict_]) + self.assertEqual(not_empty, [dict_]) + @with_transaction() def test_search_element_non_equals_none(self): "Test search dict element non equals None" @@ -402,6 +479,38 @@ class FieldDictTestCase(unittest.TestCase): self.assertListEqual(dicts_empty, []) self.assertListEqual(dicts_foo_b, []) + @with_transaction() + @unittest.skipIf( + backend.name != 'postgresql', + 'in use the contain check specific to postgresql') + def test_search_multiselection_in(self): + "Test search dict multi-selection with in" + pool = Pool() + Dict = pool.get('test.dict') + self.create_schema() + + dict_, = Dict.create([{ + 'dico': {'countries': ['fr', 'be']}, + }]) + + belgium = Dict.search([ + ('dico.countries', 'in', ['be']), + ]) + germany = Dict.search([ + ('dico.countries', 'in', ['de']), + ]) + belgium_germany = Dict.search([ + ('dico.countries', 'in', ['be', 'de']), + ]) + empty = Dict.search([ + ('dico.countries', 'in', []), + ]) + + self.assertEqual(belgium, [dict_]) + self.assertEqual(germany, []) + self.assertEqual(belgium_germany, [dict_]) + self.assertEqual(empty, []) + @with_transaction() def test_search_element_in_none(self): "Test search dict element in [None]" @@ -446,6 +555,38 @@ class FieldDictTestCase(unittest.TestCase): self.assertListEqual(dicts_empty, [dict_]) self.assertListEqual(dicts_foo_b, []) + @with_transaction() + @unittest.skipIf( + backend.name != 'postgresql', + 'in use the contain check specific to postgresql') + def test_search_multiselection_not_in(self): + "Test search dict multi-selection with not in" + pool = Pool() + Dict = pool.get('test.dict') + self.create_schema() + + dict_, = Dict.create([{ + 'dico': {'countries': ['fr', 'be']}, + }]) + + not_belgium = Dict.search([ + ('dico.countries', 'not in', ['be']), + ]) + not_germany = Dict.search([ + ('dico.countries', 'not in', ['de']), + ]) + not_belgium_germany = Dict.search([ + ('dico.countries', 'not in', ['de', 'be']), + ]) + not_empty = Dict.search([ + ('dico.countries', 'not in', []), + ]) + + self.assertEqual(not_belgium, []) + self.assertEqual(not_germany, [dict_]) + self.assertEqual(not_belgium_germany, []) + self.assertEqual(not_empty, [dict_]) + @with_transaction() def test_search_element_not_in_none(self): "Test search dict element not in [None]"