Add dynamic size limit on the One2Many, Many2Many and Char
review396001
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user