From 6858187fc8cc875662d251a4b51ca176c93b50ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 20 Jun 2012 18:12:40 +0200 Subject: [PATCH] Add dynamic size limit on the One2Many, Many2Many and Char review396001 --- CHANGELOG | 1 + doc/ref/models/fields.rst | 14 +++++- trytond/backend/mysql/fields.py | 2 +- trytond/backend/postgresql/fields.py | 2 +- trytond/model/fields/char.py | 12 ++++- trytond/model/fields/field.py | 8 ++++ trytond/model/fields/many2many.py | 21 +++++++-- trytond/model/fields/one2many.py | 21 +++++++-- trytond/model/model.py | 2 +- trytond/model/modelsql.py | 7 ++- trytond/model/modelstorage.py | 15 +++++- trytond/test/test.py | 68 ++++++++++++++++++++++++++++ trytond/tests/test_fields.py | 31 +++++++++++++ 13 files changed, 185 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8e91dfddc9..2df5ab9b90 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add dynamic size limit on the One2Many, Many2Many and Char * Replace __tryton__.py by tryton.cfg * Allow to use Reference field in One2Many & Many2Many * Remove hexmd5 from ModelView.fields_view_get diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst index 7a196f3a42..e72d43a202 100644 --- a/doc/ref/models/fields.rst +++ b/doc/ref/models/fields.rst @@ -415,7 +415,7 @@ A many-to-one relation field. One2Many -------- -.. class:: One2Many(model_name, field, string[, add_remove[, order[, datetime_field[, \**options]]]]) +.. class:: One2Many(model_name, field, string[, add_remove[, order[, datetime_field[, size[, \**options]]]]]) A one-to-many relation field. It requires to have the opposite :class:`Many2One` field or a :class:`Reference` field defined on the target @@ -470,10 +470,15 @@ This field accepts as written value a list of tuples like this: Same as :attr:`Many2One.datetime_field` +.. attribute:: One2Many.size + + An integer or a PYSON expression denoting the maximum number of records + allowed in the relation. + Many2Many --------- -.. class:: Many2Many(relation_name, origin, target, string[, order[, datetime_field[, \**options]]]) +.. class:: Many2Many(relation_name, origin, target, string[, order[, datetime_field[, size[, \**options]]]]) A many-to-many relation field. @@ -510,6 +515,11 @@ A many-to-many relation field. Same as :attr:`Many2One.datetime_field` +.. attribute:: Many2Many.size + + An integer or a PYSON expression denoting the maximum number of records + allowed in the relation. + Instance methods: .. method:: Many2Many.get_target() diff --git a/trytond/backend/mysql/fields.py b/trytond/backend/mysql/fields.py index a120c7be75..5cc01e5eb2 100644 --- a/trytond/backend/mysql/fields.py +++ b/trytond/backend/mysql/fields.py @@ -32,7 +32,7 @@ class Char(fields.Char): @staticmethod def sql_type(field): - if field.size: + if isinstance(field.size, int): return ('varchar', 'varchar(%d)' % (field.size,)) return ('varchar', 'varchar(255)') diff --git a/trytond/backend/postgresql/fields.py b/trytond/backend/postgresql/fields.py index e3feee5407..1bd069306c 100644 --- a/trytond/backend/postgresql/fields.py +++ b/trytond/backend/postgresql/fields.py @@ -29,7 +29,7 @@ class Char(fields.Char): @staticmethod def sql_type(field): - if field.size: + if isinstance(field.size, int): return ('varchar', 'varchar(%d)' % (field.size,)) return ('varchar', 'varchar') diff --git a/trytond/model/fields/char.py b/trytond/model/fields/char.py index c511a393fb..3e2d56eb22 100644 --- a/trytond/model/fields/char.py +++ b/trytond/model/fields/char.py @@ -1,7 +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. -from trytond.model.fields.field import Field +from trytond.model.fields.field import Field, size_validate def autocomplete_validate(value): @@ -32,6 +32,7 @@ class Char(Field): self.__autocomplete = None self.autocomplete = autocomplete if autocomplete else None self.translate = translate + self.__size = None self.size = size __init__.__doc__ += Field.__init__.__doc__ @@ -43,3 +44,12 @@ class Char(Field): self.__autocomplete = value autocomplete = property(_get_autocomplete, _set_autocomplete) + + def _get_size(self): + return self.__size + + def _set_size(self, value): + size_validate(value) + self.__size = value + + size = property(_get_size, _set_size) diff --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py index eaf3d36e0e..bafa8ec597 100644 --- a/trytond/model/fields/field.py +++ b/trytond/model/fields/field.py @@ -57,6 +57,14 @@ def context_validate(value): assert isinstance(value, dict), 'context must be a dict' +def size_validate(value): + if value is not None: + assert isinstance(value, (int, PYSON)), 'size must be PYSON' + if hasattr(value, 'types'): + assert value.types() == set([int]), \ + 'size must return integer' + + class Field(object): _type = None diff --git a/trytond/model/fields/many2many.py b/trytond/model/fields/many2many.py index b65fadafed..0756ddd12f 100644 --- a/trytond/model/fields/many2many.py +++ b/trytond/model/fields/many2many.py @@ -1,7 +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. from itertools import chain -from trytond.model.fields.field import Field +from trytond.model.fields.field import Field, size_validate from trytond.transaction import Transaction from trytond.pool import Pool @@ -13,9 +13,10 @@ class Many2Many(Field): _type = 'many2many' def __init__(self, relation_name, origin, target, string='', order=None, - datetime_field=None, help='', required=False, readonly=False, - domain=None, states=None, on_change=None, on_change_with=None, - depends=None, order_field=None, context=None, loading='lazy'): + datetime_field=None, size=None, help='', required=False, + readonly=False, domain=None, states=None, on_change=None, + on_change_with=None, depends=None, order_field=None, context=None, + loading='lazy'): ''' :param relation_name: The name of the relation model or the name of the target model for ModelView only. @@ -42,8 +43,20 @@ class Many2Many(Field): self.target = target self.order = order self.datetime_field = datetime_field + self.__size = None + self.size = size + __init__.__doc__ += Field.__init__.__doc__ + def _get_size(self): + return self.__size + + def _set_size(self, value): + size_validate(value) + self.__size = value + + size = property(_get_size, _set_size) + def get(self, ids, model, name, values=None): ''' Return target records ordered. diff --git a/trytond/model/fields/one2many.py b/trytond/model/fields/one2many.py index 9318842e23..0bcbd8aaf7 100644 --- a/trytond/model/fields/one2many.py +++ b/trytond/model/fields/one2many.py @@ -1,7 +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. from itertools import chain -from trytond.model.fields.field import Field +from trytond.model.fields.field import Field, size_validate from trytond.transaction import Transaction from trytond.pool import Pool @@ -18,10 +18,10 @@ class One2Many(Field): _type = 'one2many' def __init__(self, model_name, field, string='', add_remove=None, - order=None, datetime_field=None, help='', required=False, - readonly=False, domain=None, states=None, on_change=None, - on_change_with=None, depends=None, order_field=None, context=None, - loading='lazy'): + order=None, datetime_field=None, size=None, help='', + required=False, readonly=False, domain=None, states=None, + on_change=None, on_change_with=None, depends=None, + order_field=None, context=None, loading='lazy'): ''' :param model_name: The name of the target model. :param field: The name of the field that handle the reverse many2one or @@ -50,6 +50,8 @@ class One2Many(Field): self.add_remove = add_remove self.order = order self.datetime_field = datetime_field + self.__size = None + self.size = size __init__.__doc__ += Field.__init__.__doc__ @@ -62,6 +64,15 @@ class One2Many(Field): add_remove = property(_get_add_remove, _set_add_remove) + def _get_size(self): + return self.__size + + def _set_size(self, value): + size_validate(value) + self.__size = value + + size = property(_get_size, _set_size) + def get(self, ids, model, name, values=None): ''' Return target records ordered. diff --git a/trytond/model/model.py b/trytond/model/model.py index 6e03887b31..1253bc4745 100644 --- a/trytond/model/model.py +++ b/trytond/model/model.py @@ -555,7 +555,7 @@ class Model(WarningErrorMixin, URLMixin): res[field]['format'] = copy.copy(self._columns[field].format) # convert attributes into pyson - for attr in ('states', 'domain', 'context', 'digits', + for attr in ('states', 'domain', 'context', 'digits', 'size', 'add_remove', 'format'): if attr in res[field]: res[field][attr] = encoder.encode(res[field][attr]) diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 8b9125118f..071242024c 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -121,11 +121,14 @@ class ModelSQL(ModelStorage): return unpack_result default_fun = unpack_wrapper(default_fun) + if hasattr(field, 'size') and isinstance(field.size, int): + field_size = field.size + else: + field_size = None table.add_raw_column(field_name, FIELDS[field._type].sql_type(field), FIELDS[field._type].sql_format, default_fun, - hasattr(field, 'size') and field.size or None, - string=field.string) + field_size, string=field.string) if self._history: history_table.add_raw_column(field_name, FIELDS[field._type].sql_type(field), None, diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index fe5e09ef8f..9aa0c3cd5c 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -1090,9 +1090,20 @@ class ModelStorage(Model): for record in records: required_test(record[field_name], field_name) # validate size - if hasattr(field, 'size') and field.size: + if hasattr(field, 'size') and field.size is not None: for record in records: - if len(record[field_name] or '') > field.size: + if isinstance(field.size, PYSON): + pyson_size = PYSONEncoder().encode(field.size) + env = EvalEnvironment(record, self) + env.update(Transaction().context) + env['current_date'] = datetime.datetime.today() + env['time'] = time + env['context'] = Transaction().context + env['active_id'] = record.id + field_size = PYSONDecoder(env).decode(pyson_size) + else: + field_size = field.size + if (len(record[field_name] or '') > field_size >= 0): self.raise_user_error( 'size_validation_record', error_args=self._get_error_args(field_name)) diff --git a/trytond/test/test.py b/trytond/test/test.py index 7dbf6c4281..38cd740515 100644 --- a/trytond/test/test.py +++ b/trytond/test/test.py @@ -536,6 +536,45 @@ class One2ManyReferenceTarget(ModelSQL): One2ManyReferenceTarget() +class One2ManySize(ModelSQL): + 'One2Many Size Relation' + _name = 'test.one2many_size' + _description = __doc__ + targets = fields.One2Many('test.one2many_size.target', 'origin', 'Targets', + size=3) + +One2ManySize() + + +class One2ManySizeTarget(ModelSQL): + 'One2Many Size Target' + _name = 'test.one2many_size.target' + _description = __doc__ + origin = fields.Many2One('test.one2many_size', 'Origin') + +One2ManySizeTarget() + + +class One2ManySizePYSON(ModelSQL): + 'One2Many Size PYSON Relation' + _name = 'test.one2many_size_pyson' + _description = __doc__ + limit = fields.Integer('Limit') + targets = fields.One2Many('test.one2many_size_pyson.target', 'origin', + 'Targets', size=Eval('limit', 0)) + +One2ManySizePYSON() + + +class One2ManySizePYSONTarget(ModelSQL): + 'One2Many Size PYSON Target' + _name = 'test.one2many_size_pyson.target' + _description = __doc__ + origin = fields.Many2One('test.one2many_size_pyson', 'Origin') + +One2ManySizePYSONTarget() + + class Many2Many(ModelSQL): 'Many2Many' _name = 'test.many2many' @@ -632,6 +671,35 @@ class Many2ManyReferenceRelation(ModelSQL): Many2ManyReferenceRelation() +class Many2ManySize(ModelSQL): + 'Many2Many Size Relation' + _name = 'test.many2many_size' + _description = __doc__ + targets = fields.Many2Many('test.many2many_size.relation', 'origin', + 'target', 'Targets', size=5) + +Many2ManySize() + + +class Many2ManySizeTarget(ModelSQL): + 'Many2Many Size Target' + _name = 'test.many2many_size.target' + _description = __doc__ + name = fields.Char('Name') + +Many2ManySizeTarget() + + +class Many2ManySizeRelation(ModelSQL): + 'Many2Many Size Relation' + _name = 'test.many2many_size.relation' + _description = __doc__ + origin = fields.Many2One('test.many2many_size', 'Origin') + target = fields.Many2One('test.many2many_size.target', 'Target') + +Many2ManySizeRelation() + + class Property(ModelSQL): 'Property' _name = 'test.property' diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py index 81b083d9cb..1f6d118aed 100644 --- a/trytond/tests/test_fields.py +++ b/trytond/tests/test_fields.py @@ -82,6 +82,8 @@ class FieldsTestCase(unittest.TestCase): self.one2many_reference = POOL.get('test.one2many_reference') self.one2many_reference_target = POOL.get( 'test.one2many_reference.target') + self.one2many_size = POOL.get('test.one2many_size') + self.one2many_size_pyson = POOL.get('test.one2many_size_pyson') self.many2many = POOL.get('test.many2many') self.many2many_target = POOL.get('test.many2many.target') @@ -89,6 +91,8 @@ class FieldsTestCase(unittest.TestCase): self.many2many_reference = POOL.get('test.many2many_reference') self.many2many_reference_target = POOL.get( 'test.many2many_reference.target') + self.many2many_size = POOL.get('test.many2many_size') + self.many2many_size_target = POOL.get('test.many2many_size.target') self.property_ = POOL.get('test.property') @@ -2644,6 +2648,21 @@ class FieldsTestCase(unittest.TestCase): }) self.assert_(origin3_id) + self.one2many_size.create({ + 'targets': [('create', {})] * 3, + }) + self.assertRaises(Exception, self.one2many_size.create, { + 'targets': [('create', {})] * 4, + }) + self.one2many_size_pyson.create({ + 'limit': 4, + 'targets': [('create', {})] * 4, + }) + self.assertRaises(Exception, self.one2many_size_pyson.create, { + 'limit': 2, + 'targets': [('create', {})] * 4, + }) + transaction.cursor.rollback() def test0130many2many(self): @@ -2820,6 +2839,18 @@ class FieldsTestCase(unittest.TestCase): }) self.assert_(origin3_id) + size_targets = [ + self.many2many_size_target.create({ + 'name': str(i), + }) for i in range(6)] + + self.many2many_size.create({ + 'targets': [('set', size_targets[:5])], + }) + self.assertRaises(Exception, self.many2many_size.create, { + 'targets': [('set', size_targets)], + }) + transaction.cursor.rollback() def test0140property(self):