diff --git a/CHANGELOG b/CHANGELOG index f8a84a6c35..affbf0b64a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Make depends on methods generic to any method * Add width/height attribute to notebook * Allow to call set_lang with None and language instance * Add hostname configuration to list database diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst index a884b1a640..589f4fcc0d 100644 --- a/doc/ref/models/fields.rst +++ b/doc/ref/models/fields.rst @@ -227,7 +227,7 @@ Depends A decorator to define the field names on which the decorated method depends. The `methods` argument can be used to duplicate the field names from other -fields. This is usefull if the decorated method calls another method. +decorated methods. This is useful if the decorated method calls another method. Field types =========== diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py index b5afcb3a2d..af5dfb5194 100644 --- a/trytond/model/modelview.py +++ b/trytond/model/modelview.py @@ -119,57 +119,79 @@ class ModelView(Model): cls.__rpc__['on_change_with'] = RPC(instantiate=0) cls._buttons = {} - if hasattr(cls, '__depend_methods'): - cls.__depend_methods = cls.__depend_methods.copy() - else: - cls.__depend_methods = collections.defaultdict(set) - - if hasattr(cls, '__change_buttons'): - cls.__change_buttons = cls.__change_buttons.copy() - else: - cls.__change_buttons = collections.defaultdict(set) - - def setup_field(field, field_name): - for attribute in ('on_change', 'on_change_with', 'autocomplete', - 'selection_change_with'): - if attribute == 'selection_change_with': - if isinstance( - getattr(field, 'selection', None), basestring): - function_name = field.selection - else: - continue - else: - function_name = '%s_%s' % (attribute, field_name) - if not getattr(cls, function_name, None): - continue - # Search depends on all parent class because field has been - # copied with the original definition - for parent_cls in cls.__mro__: - function = getattr(parent_cls, function_name, None) - if not function: - continue - if getattr(function, 'depends', None): - setattr(field, attribute, - getattr(field, attribute) | function.depends) - if getattr(function, 'depend_methods', None): - cls.__depend_methods[(field_name, attribute)] |= \ - function.depend_methods - function = getattr(cls, function_name, None) - if (attribute == 'on_change' - and not getattr(function, 'on_change', None)): - # Decorate on_change to always return self - setattr(cls, function_name, on_change(function)) - - def setup_callable(function, name): - if hasattr(function, 'change'): - cls.__change_buttons[name] |= function.change - + fields_ = {} + callables = {} for name in dir(cls): + if name.startswith('__'): + continue attr = getattr(cls, name) if isinstance(attr, fields.Field): - setup_field(attr, name) + fields_[name] = attr elif isinstance(attr, collections.Callable): - setup_callable(attr, name) + callables[name] = attr + + methods = { + 'depends': collections.defaultdict(set), + 'depend_methods': collections.defaultdict(set), + 'change': collections.defaultdict(set), + } + cls.__change_buttons = methods['change'] + + def get_callable_attributes(name, method): + for parent_cls in cls.__mro__: + parent_meth = getattr(parent_cls, name, None) + if not parent_meth: + continue + for attr in ['depends', 'depend_methods', 'change']: + parent_value = getattr(parent_meth, attr, None) + if parent_value: + methods[attr][name] |= parent_value + + for name, method in callables.iteritems(): + get_callable_attributes(name, method) + + def setup_field(field_name, field, attribute): + if attribute == 'selection_change_with': + if isinstance( + getattr(field, 'selection', None), basestring): + function_name = field.selection + else: + return + else: + function_name = '%s_%s' % (attribute, field_name) + if not getattr(cls, function_name, None): + return + + function = getattr(cls, function_name, None) + setattr(field, attribute, + getattr(field, attribute) | methods['depends'][function_name]) + + meth_names = list(methods['depend_methods'][function_name]) + meth_done = set() + while meth_names: + meth_name = meth_names.pop() + assert isinstance( + getattr(cls, meth_name), collections.Callable), \ + "%s.%s not callable" % (cls, meth_name) + setattr(field, attribute, + getattr(field, attribute) | methods['depends'][meth_name]) + meth_names += list( + methods['depend_methods'][meth_name] - meth_done) + meth_done.add(meth_name) + + if (attribute == 'on_change' + and not getattr(function, 'on_change', None)): + # Decorate on_change to always return self + setattr(cls, function_name, on_change(function)) + + for name, field in fields_.iteritems(): + for attribute in [ + 'on_change', + 'on_change_with', + 'autocomplete', + 'selection_change_with', + ]: + setup_field(name, field, attribute) @classmethod def __post_setup__(cls): @@ -187,16 +209,6 @@ class ModelView(Model): cls.__rpc__.setdefault(button, RPC(instantiate=0, result=on_change_result)) - # Update depend on methods - for (field_name, attribute), others in ( - cls.__depend_methods.iteritems()): - field = getattr(cls, field_name) - for other in others: - other_field = getattr(cls, other) - setattr(field, attribute, - getattr(field, attribute) - | getattr(other_field, attribute)) - @classmethod def fields_view_get(cls, view_id=None, view_type='form'): ''' diff --git a/trytond/tests/test_field_depends.py b/trytond/tests/test_field_depends.py index 42f608a331..73a178293c 100644 --- a/trytond/tests/test_field_depends.py +++ b/trytond/tests/test_field_depends.py @@ -2,7 +2,7 @@ # repository contains the full copyright notices and license terms. import unittest -from trytond.model import fields +from trytond.model import ModelView, fields class FieldDependsTestCase(unittest.TestCase): @@ -77,6 +77,72 @@ class FieldDependsTestCase(unittest.TestCase): self.assertIsNone(record.parent.parent.name) self.assertEqual(record.parent.parent.description, "Description") + def test_inherit(self): + "Tests inherited depends" + + class Parent(ModelView): + name = fields.Char("Name") + foo = fields.Char("Foo") + + @fields.depends('foo') + def on_change_name(self): + pass + + class Model(Parent): + bar = fields.Char("Bar") + + @fields.depends('bar') + def on_change_name(self): + super(Model, self).on_change_name() + + Model.__setup__() + + self.assertEqual(Model.name.on_change, {'foo', 'bar'}) + + def test_methods(self): + "Tests depends on method" + + class Model(ModelView): + name = fields.Char("Name") + foo = fields.Char("Foo") + bar = fields.Char("Bar") + + @fields.depends('foo', methods=['other_method']) + def on_change_name(self): + self.other_method() + + @fields.depends('bar') + def other_method(self): + pass + + Model.__setup__() + + self.assertEqual(Model.name.on_change, {'foo', 'bar'}) + + def test_methods_2(self): + "Tests depends on method on method" + + class Model(ModelView): + name = fields.Char("Name") + foo = fields.Char("Foo") + bar = fields.Char("Bar") + + @fields.depends('foo', methods=['other_method']) + def on_change_name(self): + self.other_method() + + @fields.depends(methods=['another_method']) + def other_method(self): + self.another_method() + + @fields.depends('bar') + def another_method(self): + pass + + Model.__setup__() + + self.assertEqual(Model.name.on_change, {'foo', 'bar'}) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(FieldDependsTestCase)