changeset e77b88fe6393 in tryton:4.8 details: https://hg.tryton.org/tryton?cmd=changeset;node=e77b88fe6393 description: Fix domain inversion of child_of mixing M2O and Reference fields
Domain inversion on Reference field does not work when using the dotted notation with a M2O field using child_of or parent_of. To solve this we correctly compute the localization of child_of leaves when using the dotted notation (which renders the inverse_leaf function useless). But we must also ensure that the selection used by reference fields restrict the choice to the right models. issue7869 review60441002 (grafted from 0655117211707b03fc496c0e30a9ec6c7af26e4c) diffstat: tryton/common/__init__.py | 5 +- tryton/common/domain_inversion.py | 71 ++++++++++++++++++++++++++++++ tryton/common/selection.py | 25 +++++++--- tryton/gui/window/view_form/model/field.py | 12 +++- 4 files changed, 100 insertions(+), 13 deletions(-) diffs (202 lines): diff -r 7c9b40df5b5d -r e77b88fe6393 tryton/common/__init__.py --- a/tryton/common/__init__.py Mon Dec 03 00:44:51 2018 +0100 +++ b/tryton/common/__init__.py Thu Dec 20 18:44:31 2018 +0100 @@ -2,7 +2,8 @@ # this repository contains the full copyright notices and license terms. from common import * from datetime_strftime import * -from domain_inversion import domain_inversion, eval_domain, localize_domain, \ - merge, inverse_leaf, filter_leaf, concat, simplify, unique_value +from .domain_inversion import domain_inversion, eval_domain, localize_domain, \ + merge, inverse_leaf, filter_leaf, prepare_reference_domain, \ + extract_reference_models, concat, simplify, unique_value from environment import EvalEnvironment import timedelta diff -r 7c9b40df5b5d -r e77b88fe6393 tryton/common/domain_inversion.py --- a/tryton/common/domain_inversion.py Mon Dec 03 00:44:51 2018 +0100 +++ b/tryton/common/domain_inversion.py Thu Dec 20 18:44:31 2018 +0100 @@ -127,6 +127,36 @@ return [filter_leaf(d, field, model) for d in domain] +def prepare_reference_domain(domain, reference): + "convert domain to replace reference fields by their local part" + if domain in ('AND', 'OR'): + return domain + elif is_leaf(domain): + # When a Reference field is using the dotted notation the model + # specified must be removed from the clause + if domain[0].count('.') and len(domain) > 3: + local_name, target_name = domain[0].split('.', 1) + if local_name == reference: + return [target_name] + list(domain[1:3] + domain[4:]) + return domain + else: + return [prepare_reference_domain(d, reference) for d in domain] + + +def extract_reference_models(domain, field_name): + "returns the set of the models available for field_name" + if domain in ('AND', 'OR'): + return set() + elif is_leaf(domain): + local_part = domain[0].split('.', 1)[0] + if local_part == field_name and len(domain) > 3: + return {domain[3]} + return set() + else: + return reduce(operator.or_, + (extract_reference_models(d, field_name) for d in domain)) + + def eval_domain(domain, context, boolop=operator.and_): "compute domain boolean value according to the context" if is_leaf(domain): @@ -150,6 +180,9 @@ return domain elif is_leaf(domain): if 'child_of' in domain[1]: + if domain[0].count('.'): + _, target_part = domain[0].split('.', 1) + return [target_part] + list(domain[1:]) if len(domain) == 3: return domain else: @@ -628,6 +661,13 @@ domain = [['x', 'child_of', [1], 'y']] assert localize_domain(domain, 'x') == [['y', 'child_of', [1]]] + domain = [['x.y', 'child_of', [1], 'parent']] + assert localize_domain(domain, 'x') == [['y', 'child_of', [1], 'parent']] + + domain = [['x.y.z', 'child_of', [1], 'parent', 'model']] + assert localize_domain(domain, 'x') == \ + [['y.z', 'child_of', [1], 'parent', 'model']] + domain = [['x.id', '=', 1, 'y']] assert localize_domain(domain, 'x', False) == [['id', '=', 1, 'y']] assert localize_domain(domain, 'x', True) == [['id', '=', 1]] @@ -636,6 +676,35 @@ assert localize_domain(domain, 'x', False) == [['b.c', '=', 1, 'y', 'z']] assert localize_domain(domain, 'x', True) == [['b.c', '=', 1, 'z']] + +def test_prepare_reference_domain(): + domain = [['x', 'like', 'A%']] + assert prepare_reference_domain(domain, 'x') == [['x', 'like', 'A%']] + + domain = [['x.y', 'like', 'A%', 'model']] + assert prepare_reference_domain(domain, 'x') == [['y', 'like', 'A%']] + + domain = [['x.y', 'child_of', [1], 'model', 'parent']] + assert prepare_reference_domain(domain, 'x') == \ + [['y', 'child_of', [1], 'parent']] + + +def test_extract_models(): + domain = [['x', 'like', 'A%']] + assert extract_reference_models(domain, 'x') == set() + assert extract_reference_models(domain, 'y') == set() + + domain = [['x', 'like', 'A%', 'model']] + assert extract_reference_models(domain, 'x') == {'model'} + assert extract_reference_models(domain, 'y') == set() + + domain = ['OR', + ['x.y', 'like', 'A%', 'model_A'], + ['x.z', 'like', 'B%', 'model_B']] + assert extract_reference_models(domain, 'x') == {'model_A', 'model_B'} + assert extract_reference_models(domain, 'y') == set() + + if __name__ == '__main__': test_simple_inversion() test_and_inversion() @@ -648,3 +717,5 @@ test_simplify() test_evaldomain() test_localize() + test_prepare_reference_domain() + test_extract_models() diff -r 7c9b40df5b5d -r e77b88fe6393 tryton/common/selection.py --- a/tryton/common/selection.py Mon Dec 03 00:44:51 2018 +0100 +++ b/tryton/common/selection.py Thu Dec 20 18:44:31 2018 +0100 @@ -50,10 +50,6 @@ return domain = field.domain_get(record) - if field.attrs['type'] == 'reference': - # The domain on reference field is not only based on the selection - # so the selection can not be filtered. - domain = [] if 'relation' not in self.attrs: change_with = self.attrs.get('selection_change_with') or [] value = record._get_on_change_args(change_with) @@ -90,10 +86,23 @@ def filter_selection(self, domain, record, field): if not domain: return - test = lambda value: eval_domain(domain, { - self.field_name: value[0], - }) - self.selection = filter(test, self.selection) + + def _value_evaluator(value): + return eval_domain(domain, { + self.field_name: value[0], + }) + + def _model_evaluator(allowed_models): + def test(value): + return value[0] in allowed_models + return test + + if field.attrs['type'] == 'reference': + allowed_models = field.get_models(record) + evaluator = _model_evaluator(allowed_models) + else: + evaluator = _value_evaluator + self.selection = list(filter(evaluator, self.selection)) def get_inactive_selection(self, value): if 'relation' not in self.attrs: diff -r 7c9b40df5b5d -r e77b88fe6393 tryton/gui/window/view_form/model/field.py --- a/tryton/gui/window/view_form/model/field.py Mon Dec 03 00:44:51 2018 +0100 +++ b/tryton/gui/window/view_form/model/field.py Thu Dec 20 18:44:31 2018 +0100 @@ -6,7 +6,8 @@ import locale from tryton.common import \ domain_inversion, eval_domain, localize_domain, \ - merge, inverse_leaf, filter_leaf, concat, simplify, unique_value, \ + merge, inverse_leaf, filter_leaf, prepare_reference_domain, \ + extract_reference_models, concat, simplify, unique_value, \ EvalEnvironment import tryton.common as common import datetime @@ -874,10 +875,16 @@ else: model = None screen_domain, attr_domain = self.domains_get(record) + screen_domain = prepare_reference_domain(screen_domain, self.name) return concat(localize_domain( filter_leaf(screen_domain, self.name, model), strip_target=True), attr_domain) + def get_models(self, record): + screen_domain, attr_domain = self.domains_get(record) + return extract_reference_models( + concat(screen_domain, attr_domain), self.name) + class _FileCache(object): def __init__(self, path): @@ -955,8 +962,7 @@ def domain_get(self, record): screen_domain, attr_domain = self.domains_get(record) - return concat(localize_domain(inverse_leaf(screen_domain)), - attr_domain) + return concat(localize_domain(screen_domain), attr_domain) def date_format(self, record): context = self.get_context(record)