Add fields.depends decorator

issue3487
review2301002
This commit is contained in:
Cédric Krier
2014-01-26 14:27:24 +01:00
parent e60274bd27
commit 905343c900
10 changed files with 142 additions and 72 deletions
+3
View File
@@ -1,3 +1,6 @@
* Deprecate on_change, on_change_with, selection_change_with and autocomplete
field arguments
* Add fields.depends decorator
* Add run-tests
* Validate only modified and dependant fields on model write
* Improve error messages by showing the failing value
+20 -5
View File
@@ -77,7 +77,7 @@ If true, the content of the field will be indexed.
.. attribute:: Field.on_change
A list of field names. If this attribute is set, the client will call the
A set of field names. If this attribute is set, the client will call the
method ``on_change_<field name>`` of the model when the user changes the
current field value and will give the values of each fields in this list. The
method signature is::
@@ -88,15 +88,17 @@ This method must return a dictionary with the values of fields to be updated.
.. note::
The on_change_<field name> methods are runnin in a rollbacked transaction.
The on_change_<field name> methods are running in a rollbacked transaction.
..
The set of field names could be filled by using the decorator :meth:`depends`.
``on_change_with``
------------------
.. attribute:: Field.on_change_with
A list of field names. Same like :attr:`on_change`, but defined the other way
A set of field names. Same like :attr:`on_change`, but defined the other way
around. If this attribute is set, the client will call the method
``on_change_with_<field name>`` of the model when the user changes one of the
fields defined in the list and will give the values of each fields in this
@@ -112,6 +114,8 @@ This method must return the new value of the field.
..
The set of field names could be filled by using the decorator :meth:`depends`.
``depends``
-----------
@@ -189,6 +193,15 @@ The method signature is::
Where ``tables`` is a nested dictionary, see :meth:`~Field.convert_domain`.
Depends
=======
.. method:: depends([\*fields[, methods]])
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.
Field types
===========
@@ -234,7 +247,7 @@ A single line string field.
.. attribute:: Char.autocomplete
A list of field names. If this attribute is set, the client will call the
A set of field names. If this attribute is set, the client will call the
method ``autocomplete_<field name>`` of the model when the user changes one
of those field value. The method signature is::
@@ -242,6 +255,7 @@ A single line string field.
This method must return a list of string that will populate the
ComboboxEntry in the client.
The set of field names could be filled by using the decorator :meth:`depends`.
Sha
---
@@ -395,10 +409,11 @@ A string field with limited values to choice.
.. attribute:: Selection.selection_change_with
A list of field names. If this attribute is set, the client will call the
A set of field names. If this attribute is set, the client will call the
``selection`` method of the model when the user changes on of the fields
defined in the list and will give the values of each fields in the list.
The ``selection`` method should be an instance method.
The set of field names could be filled by using the decorator :meth:`depends`.
.. attribute:: Selection.translate_selection
+2 -2
View File
@@ -39,8 +39,7 @@ class Attachment(ModelSQL, ModelView):
'invisible': Eval('type') != 'data',
}, depends=['type']), 'get_data', setter='set_data')
description = fields.Text('Description')
summary = fields.Function(fields.Char('Summary',
on_change_with=['description']), 'on_change_with_summary')
summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary')
resource = fields.Reference('Resource', selection='models_get',
select=True)
link = fields.Char('Link', states={
@@ -191,6 +190,7 @@ class Attachment(ModelSQL, ModelView):
'collision': collision,
})
@fields.depends('description')
def on_change_with_summary(self, name=None):
return firstline(self.description or '')
+8 -8
View File
@@ -29,20 +29,16 @@ class Trigger(ModelSQL, ModelView):
'invisible': (Eval('on_create', False)
or Eval('on_write', False)
or Eval('on_delete', False)),
}, depends=['on_create', 'on_write', 'on_delete'],
on_change=['on_time'])
}, depends=['on_create', 'on_write', 'on_delete'])
on_create = fields.Boolean('On Create', select=True, states={
'invisible': Eval('on_time', False),
}, depends=['on_time'],
on_change=['on_create'])
}, depends=['on_time'])
on_write = fields.Boolean('On Write', select=True, states={
'invisible': Eval('on_time', False),
}, depends=['on_time'],
on_change=['on_write'])
}, depends=['on_time'])
on_delete = fields.Boolean('On Delete', select=True, states={
'invisible': Eval('on_time', False),
}, depends=['on_time'],
on_change=['on_delete'])
}, depends=['on_time'])
condition = fields.Char('Condition', required=True,
help='A Python statement evaluated with record represented by '
'"self"\nIt triggers the action if true.')
@@ -97,6 +93,7 @@ class Trigger(ModelSQL, ModelView):
def default_limit_number():
return 0
@fields.depends('on_time')
def on_change_on_time(self):
if self.on_time:
return {
@@ -106,6 +103,7 @@ class Trigger(ModelSQL, ModelView):
}
return {}
@fields.depends('on_create')
def on_change_on_create(self):
if self.on_create:
return {
@@ -113,6 +111,7 @@ class Trigger(ModelSQL, ModelView):
}
return {}
@fields.depends('on_write')
def on_change_on_write(self):
if self.on_write:
return {
@@ -120,6 +119,7 @@ class Trigger(ModelSQL, ModelView):
}
return {}
@fields.depends('on_delete')
def on_change_on_delete(self):
if self.on_delete:
return {
+7 -16
View File
@@ -1,16 +1,13 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import warnings
from sql import Query, Expression
from ...config import CONFIG
from .field import Field, FieldTranslate, size_validate, SQLType
def autocomplete_validate(value):
if value:
assert isinstance(value, list), 'autocomplete must be a list'
class Char(FieldTranslate):
'''
Define a char field (``unicode``).
@@ -31,22 +28,16 @@ class Char(FieldTranslate):
readonly=readonly, domain=domain, states=states, select=select,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.__autocomplete = None
self.autocomplete = autocomplete if autocomplete else None
self.autocomplete = set()
if autocomplete:
warnings.warn('autocomplete argument is deprecated, use the '
'depends decorator', DeprecationWarning, stacklevel=2)
self.autocomplete |= set(autocomplete)
self.translate = translate
self.__size = None
self.size = size
__init__.__doc__ += Field.__init__.__doc__
def _get_autocomplete(self):
return self.__autocomplete
def _set_autocomplete(self, value):
autocomplete_validate(value)
self.__autocomplete = value
autocomplete = property(_get_autocomplete, _set_autocomplete)
def _get_size(self):
return self.__size
+40 -31
View File
@@ -1,6 +1,9 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from collections import namedtuple
import warnings
from functools import wraps
from sql import operators, Column, Literal, Select, CombiningQuery
from sql.conditionals import Coalesce, NullIf
from sql.operators import Concat
@@ -45,16 +48,6 @@ def states_validate(value):
'values of states must return boolean'
def on_change_validate(value):
if value:
assert isinstance(value, list), 'on_change must be a list'
def on_change_with_validate(value):
if value:
assert isinstance(value, list), 'on_change_with must be a list'
def depends_validate(value):
assert isinstance(value, list), 'depends must be a list'
@@ -70,6 +63,31 @@ def size_validate(value):
assert value.types() == set([int]), \
'size must return integer'
def depends(*fields, **kwargs):
methods = kwargs.pop('methods', None)
assert not kwargs
def decorator(func):
depends = getattr(func, 'depends', set())
depends |= set(fields)
setattr(func, 'depends', depends)
if methods:
depend_methods = getattr(func, 'depend_methods', set())
depend_methods |= set(methods)
setattr(func, 'depend_methods', depend_methods)
@wraps(func)
def wrapper(self, *args, **kwargs):
for field in fields:
if not hasattr(self, field):
setattr(self, field, None)
return func(self, *args, **kwargs)
return wrapper
return decorator
SQL_OPERATORS = {
'=': operators.Equal,
'!=': operators.NotEqual,
@@ -128,10 +146,18 @@ class Field(object):
self.__states = None
self.states = states or {}
self.select = bool(select)
self.__on_change = None
self.on_change = on_change
self.__on_change_with = None
self.on_change_with = on_change_with
self.on_change = set()
if on_change:
warnings.warn('on_change argument is deprecated, '
'use the depends decorator',
DeprecationWarning, stacklevel=3)
self.on_change |= set(on_change)
self.on_change_with = set()
if on_change_with:
warnings.warn('on_change_with argument is deprecated, '
'use the depends decorator',
DeprecationWarning, stacklevel=3)
self.on_change_with |= set(on_change_with)
self.__depends = None
self.depends = depends or []
self.__context = None
@@ -159,23 +185,6 @@ class Field(object):
states = property(_get_states, _set_states)
def _get_on_change(self):
return self.__on_change
def _set_on_change(self, value):
on_change_validate(value)
self.__on_change = value
on_change = property(_get_on_change, _set_on_change)
def _get_on_change_with(self):
return self.__on_change_with
def _set_on_change_with(self, value):
on_change_with_validate(value)
self.__on_change_with = value
on_change_with = property(_get_on_change_with, _set_on_change_with)
def _get_depends(self):
return self.__depends
+8 -1
View File
@@ -1,5 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import warnings
from sql import Column
from sql.conditionals import Case
@@ -32,7 +34,12 @@ class Selection(Field):
self.selection = selection.copy()
else:
self.selection = selection
self.selection_change_with = selection_change_with
self.selection_change_with = set()
if selection_change_with:
warnings.warn('selection_change_with argument is deprecated, '
'use the depends decorator',
DeprecationWarning, stacklevel=2)
self.selection_change_with |= set(selection_change_with)
self.sort = sort
self.translate_selection = translate
__init__.__doc__ += Field.__init__.__doc__
+47 -6
View File
@@ -35,12 +35,39 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
}
cls._error_messages = {}
# Copy fields
if hasattr(cls, '__depend_methods'):
cls.__depend_methods = cls.__depend_methods.copy()
else:
cls.__depend_methods = collections.defaultdict(set)
# Copy fields and update depends
for attr in dir(cls):
if attr.startswith('_'):
continue
if isinstance(getattr(cls, attr), fields.Field):
setattr(cls, attr, copy.deepcopy(getattr(cls, attr)))
if not isinstance(getattr(cls, attr), fields.Field):
continue
field_name = attr
field = copy.deepcopy(getattr(cls, field_name))
setattr(cls, field_name, field)
for attribute in ('on_change', 'on_change_with', 'autocomplete',
'selection_change_with'):
if attribute == 'selection_change_with':
if (isinstance(field, fields.Selection)
and isinstance(field.selection, basestring)):
function_name = field.selection
else:
continue
else:
function_name = '%s_%s' % (attribute, field_name)
function = getattr(cls, function_name, None)
if function:
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
@classmethod
def __post_setup__(cls):
@@ -78,9 +105,19 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
for attribute in ('on_change', 'on_change_with', 'autocomplete'):
function_name = '%s_%s' % (attribute, field_name)
if getattr(field, attribute, False):
if getattr(cls, function_name, None):
cls.__rpc__.setdefault(function_name, RPC(instantiate=0))
# 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))
# Set name to fields
for name, field in cls._fields.iteritems():
if field.name is None:
@@ -238,8 +275,12 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
'selection_change_with',
):
if getattr(cls._fields[field], arg, None) is not None:
res[field][arg] = copy.copy(getattr(cls._fields[field],
arg))
value = getattr(cls._fields[field], arg)
if isinstance(value, set):
value = list(value)
else:
value = copy.copy(value)
res[field][arg] = value
if not accesses.get(field, {}).get('write', True):
res[field]['readonly'] = True
if res[field].get('states') and \
+5 -2
View File
@@ -1167,15 +1167,16 @@ class ModelStorage(Model):
def __init__(self, id=None, **kwargs):
_ids = kwargs.pop('_ids', None)
_local_cache = kwargs.pop('_local_cache', None)
super(ModelStorage, self).__init__(id, **kwargs)
self._cursor = Transaction().cursor
self._user = Transaction().user
self._context = Transaction().context
if id is not None:
id = int(id)
if _ids is not None:
self._ids = _ids
assert id in _ids
else:
self._ids = [self.id]
self._ids = [id]
self._cursor_cache = self._cursor.get_cache(self._context)
@@ -1185,6 +1186,8 @@ class ModelStorage(Model):
self._local_cache = LRUDict(RECORD_CACHE_SIZE)
self._local_cache.counter = Transaction().counter
super(ModelStorage, self).__init__(id, **kwargs)
@property
def _cache(self):
cache = self._cursor_cache
+2 -1
View File
@@ -625,10 +625,11 @@ class Selection(ModelSQL):
('', ''), ('arabic', 'Arabic'), ('hexa', 'Hexadecimal')],
'Selection')
dyn_select = fields.Selection('get_selection',
'Instance Dynamic Selection', selection_change_with=['select'])
'Instance Dynamic Selection')
dyn_select_static = fields.Selection('static_selection',
'Static Selection')
@fields.depends('select')
def get_selection(self):
if self.select == 'arabic':
return [('', '')] + [(str(i), str(i)) for i in range(1, 11)]