Move field definition from Model to Field

issue8037
review74441002
This commit is contained in:
Cédric Krier
2019-02-13 10:37:40 +01:00
parent 246c672073
commit 98bb26a386
15 changed files with 221 additions and 178 deletions
+1
View File
@@ -1,3 +1,4 @@
* Move field definition from Model to Field
* Use number of verbose flag as log level
* Add coroutine concurrency option
* Add increasing delay on database operational error retry
+8
View File
@@ -192,6 +192,14 @@ Instance methods:
Adds to `model` the default RPC instances required by the field.
.. method:: Field.definition(model, language)
Returns a dictionary with the definition of the field.
.. method:: Field.definition_translations(model, language)
Returns a list of translation sources used by :meth:`~Field.definition`.
Default value
=============
+7 -1
View File
@@ -20,8 +20,8 @@ class Binary(Field):
domain=None, states=None, select=False, on_change=None,
on_change_with=None, depends=None, context=None, loading='lazy',
filename=None, file_id=None, store_prefix=None):
self.filename = filename
if filename is not None:
self.filename = filename
if depends is None:
depends = [filename]
else:
@@ -116,3 +116,9 @@ class Binary(Field):
values = [self.sql_format(value)]
cursor.execute(*table.update(columns, values,
where=reduce_ids(table.id, ids)))
def definition(self, model, language):
definition = super().definition(model, language)
definition['searchable'] = False
definition['filename'] = self.filename
return definition
+6
View File
@@ -78,3 +78,9 @@ class Char(FieldTranslate):
database = Transaction().database
value = database.unaccent(value)
return value
def definition(self, model, language):
definition = super().definition(model, language)
definition['autocomplete'] = list(self.autocomplete)
definition['size'] = self.size
return definition
+16 -1
View File
@@ -3,6 +3,7 @@
import datetime
from sql.functions import Function
from trytond.pyson import PYSONEncoder
from ... import backend
from .field import Field
@@ -52,7 +53,16 @@ class Date(Field):
return super(Date, self).sql_cast(expression)
class Timestamp(Field):
class FormatMixin(Field):
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
definition['format'] = encoder.encode(self.format)
return definition
class Timestamp(FormatMixin, Field):
'''
Define a timestamp field (``datetime``).
'''
@@ -184,3 +194,8 @@ class TimeDelta(Field):
else:
result[row['id']] = value
return result
def definition(self, model, language):
definition = super().definition(model, language)
definition['converter'] = self.converter
return definition
+5
View File
@@ -140,6 +140,11 @@ class Dict(Field):
column = self.sql_column(table)
return [database.json_get(column, key)]
def definition(self, model, language):
definition = super().definition(model, language)
definition['schema_model'] = self.schema_model
return definition
class TranslatedDict(object):
'A descriptor for translated values of Dict field'
+39
View File
@@ -384,6 +384,40 @@ class Field(object):
model.__rpc__.setdefault(
func_name, RPC(instantiate=0, result=result))
def definition(self, model, language):
pool = Pool()
Translation = pool.get('ir.translation')
encoder = PYSONEncoder()
definition = {
'context': encoder.encode(self.context),
'loading': self.loading,
'name': self.name,
'on_change': list(self.on_change),
'on_change_with': list(self.on_change_with),
'readonly': self.readonly,
'required': self.required,
'states': encoder.encode(self.states),
'type': self._type,
'domain': encoder.encode(self.domain),
'searchable': hasattr(model, 'search'),
'sortable': hasattr(model, 'search'),
}
name = '%s,%s' % (model.__class__, self.name)
trans = Translation.get_source(name, 'field', language)
definition['string'] = trans or self.string
trans = Translation.get_source(name, 'help', language)
definition['help'] = trans or self.help
return definition
def definition_translations(self, model, language):
"Returns sources used for definition"
name = '%s,%s' % (model.__class__, self.name)
return [
(name, 'field', language, None),
(name, 'help', language, None),
]
class FieldTranslate(Field):
@@ -493,3 +527,8 @@ class FieldTranslate(Field):
language = get_parent_language(language)
return [Coalesce(column, self.sql_column(table))]
def definition(self, model, language):
definition = super().definition(model, language)
definition['translate'] = self.translate
return definition
+7
View File
@@ -1,5 +1,6 @@
# 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.pyson import PYSONEncoder
from .field import Field
from ...pyson import PYSON
@@ -52,3 +53,9 @@ class Float(Field):
if value is None:
return None
return float(value)
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
definition['digits'] = encoder.encode(self.digits)
return definition
+8
View File
@@ -123,6 +123,14 @@ class Function(Field):
def __set__(self, inst, value):
self._field.__set__(inst, value)
def definition(self, model, language):
definition = self._field.definition(model, language)
definition.update(super().definition(model, language))
definition['searchable'] &= (
bool(self.searcher) or hasattr(model, 'domain_' + self.name))
definition['sortable'] &= hasattr(model, 'order_' + self.name)
return definition
class MultiValue(Function):
+16
View File
@@ -6,6 +6,7 @@ from sql import Cast, Literal, Null
from sql.functions import Substring, Position
from sql.conditionals import Coalesce
from trytond.pyson import PYSONEncoder
from .field import (Field, size_validate, instanciate_values, domain_validate,
search_order_validate, context_validate)
from ...pool import Pool
@@ -437,3 +438,18 @@ class Many2Many(Field):
query_table = convert_from(None, relation_tables)
query = query_table.select(origin, where=expression)
return table.id.in_(query)
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
definition['add_remove'] = encoder.encode(self.add_remove)
definition['datetime_field'] = self.datetime_field
if self.filter:
definition['domain'] = encoder.encode(
['AND', self.domain, self.filter])
definition['relation'] = self.get_target().__name__
definition['search_context'] = encoder.encode(self.search_context)
definition['search_order'] = encoder.encode(self.search_order)
definition['sortable'] &= hasattr(model, 'order_' + self.name)
definition['size'] = encoder.encode(self.size)
return definition
+19
View File
@@ -5,6 +5,7 @@ from sql.aggregate import Max
from sql.conditionals import Coalesce
from sql.operators import Or
from trytond.pyson import PYSONEncoder
from .field import (Field, search_order_validate, context_validate,
with_inactive_records)
from ...pool import Pool
@@ -289,3 +290,21 @@ class Many2One(Field):
}
tables[self.name] = target_tables
return target_tables
def definition(self, model, language):
encoder = PYSONEncoder()
target = self.get_target()
relation_fields = [fname for fname, field in target._fields.items()
if field._type == 'one2many'
and field.model_name == model.__name__
and field.field == self.name]
definition = super().definition(model, language)
definition['datetime_field'] = self.datetime_field
definition['relation'] = target.__name__
if len(relation_fields) == 1:
definition['relation_field'], = relation_fields
definition['search_context'] = encoder.encode(self.search_context)
definition['search_order'] = encoder.encode(self.search_order)
return definition
+17
View File
@@ -5,6 +5,7 @@ from sql import Cast, Literal
from sql.functions import Substring, Position
from sql.conditionals import Coalesce
from trytond.pyson import PYSONEncoder
from .field import (Field, size_validate, instanciate_values, domain_validate,
search_order_validate, context_validate)
from ...pool import Pool
@@ -327,3 +328,19 @@ class One2Many(Field):
if operator == 'not where':
expression = ~expression
return expression
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
definition['add_remove'] = encoder.encode(self.add_remove)
definition['datetime_field'] = self.datetime_field
if self.filter:
definition['domain'] = encoder.encode(
['AND', self.domain, self.filter])
definition['relation'] = self.model_name
definition['relation_field'] = self.field
definition['search_context'] = encoder.encode(self.search_context)
definition['search_order'] = encoder.encode(self.search_order)
definition['size'] = encoder.encode(self.size)
definition['sortable'] &= hasattr(model, 'order_' + self.name)
return definition
+10 -1
View File
@@ -5,6 +5,7 @@ import warnings
from sql import Cast, Literal, Query, Expression
from sql.functions import Substring, Position
from trytond.pyson import PYSONEncoder
from .field import (Field, search_order_validate, context_validate,
with_inactive_records)
from .selection import SelectionMixin
@@ -13,7 +14,7 @@ from ...pool import Pool
from ...rpc import RPC
class Reference(Field, SelectionMixin):
class Reference(SelectionMixin, Field):
'''
Define a reference field (``str``).
'''
@@ -177,3 +178,11 @@ class Reference(Field, SelectionMixin):
Position(',', column) + Literal(1)),
Model.id.sql_type().base).in_(query)
& column.ilike(target + ',%'))
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
definition['datetime_field'] = self.datetime_field
definition['search_context'] = encoder.encode(self.search_context)
definition['search_order'] = encoder.encode(self.search_order)
return definition
+37 -2
View File
@@ -4,13 +4,15 @@ import warnings
from sql.conditionals import Case
from trytond.pool import Pool
from ...transaction import Transaction
from ...tools import is_instance_method
from .field import Field
from ...rpc import RPC
class SelectionMixin:
class SelectionMixin(Field):
translate_selection = True
def translated(self, name=None):
"Return a descriptor for the translated value of the field"
@@ -20,8 +22,36 @@ class SelectionMixin:
raise ValueError('Missing name argument')
return TranslatedSelection(name)
def definition(self, model, language):
pool = Pool()
Translation = pool.get('ir.translation')
definition = super().definition(model, language)
if not isinstance(self.selection, str) and self.translate_selection:
name = '%s,%s' % (model.__class__, self.name)
selection = []
for key, source in self.selection:
trans = Translation.get_source(
name, 'selection', language, source)
selection.append((key, trans or source))
elif hasattr(self.selection, 'copy'):
selection = self.selection.copy()
else:
selection = self.selection
definition['selection'] = selection
definition['selection_change_with'] = list(self.selection_change_with)
return definition
class Selection(Field, SelectionMixin):
def definition_translations(self, model, language):
name = '%s,%s' % (model.__class__, self.name)
selection = []
if not isinstance(self.selection, str) and self.translate_selection:
for key, source in self.selection:
selection.append(
(name, 'selection', language, source))
return super().definition_translations(model, language) + selection
class Selection(SelectionMixin, Field):
'''
Define a selection field (``str``).
'''
@@ -87,6 +117,11 @@ class Selection(Field, SelectionMixin):
else:
return [column]
def definition(self, model, language):
definition = super().definition(model, language)
definition['sort'] = self.sort
return definition
class TranslatedSelection(object):
'A descriptor for translated value of Selection field'
+25 -173
View File
@@ -6,7 +6,7 @@ from functools import total_ordering
from trytond.model import fields
from trytond.pool import Pool, PoolBase, PoolMeta
from trytond.pyson import PYSONEncoder
from trytond.pyson import PYSONEncoder, PYSONDecoder
from trytond.transaction import Transaction
from trytond.url import URLMixin
from trytond.rpc import RPC
@@ -158,7 +158,7 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
"""
Return the definition of each field on the model.
"""
res = {}
definition = {}
pool = Pool()
Translation = pool.get('ir.translation')
FieldAccess = pool.get('ir.model.field.access')
@@ -167,186 +167,38 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
# Add translation to cache
language = Transaction().language
trans_args = []
for field in (x for x in cls._fields.keys()
if ((not fields_names) or x in fields_names)):
trans_args.append((cls.__name__ + ',' + field, 'field', language,
None))
trans_args.append((cls.__name__ + ',' + field, 'help', language,
None))
if hasattr(cls._fields[field], 'selection'):
if (isinstance(cls._fields[field].selection, (tuple, list))
and ((hasattr(cls._fields[field],
'translate_selection')
and cls._fields[field].translate_selection)
or not hasattr(cls._fields[field],
'translate_selection'))):
sel = cls._fields[field].selection
for (key, val) in sel:
trans_args.append((cls.__name__ + ',' + field,
'selection', language, val))
for fname, field in cls._fields.items():
if fields_names and fname not in fields_names:
continue
trans_args.extend(field.definition_translations(cls, language))
Translation.get_sources(trans_args)
encoder = PYSONEncoder()
decoder = PYSONDecoder(noeval=True)
accesses = FieldAccess.get_access([cls.__name__])[cls.__name__]
for field in (x for x in cls._fields.keys()
if ((not fields_names) or x in fields_names)):
res[field] = {
'type': cls._fields[field]._type,
'name': field,
}
for arg in (
'string',
'readonly',
'states',
'size',
'required',
'translate',
'help',
'select',
'on_change',
'add_remove',
'on_change_with',
'autocomplete',
'sort',
'datetime_field',
'loading',
'filename',
'selection_change_with',
'domain',
'converter',
'search_order',
'search_context',
):
if getattr(cls._fields[field], arg, None) is not None:
value = getattr(cls._fields[field], arg)
if isinstance(value, set):
value = list(value)
else:
value = copy.copy(value)
res[field][arg] = value
for fname, field in cls._fields.items():
if fields_names and fname not in fields_names:
continue
definition[fname] = field.definition(cls, language)
if not accesses.get(field, {}).get('write', True):
res[field]['readonly'] = True
if res[field].get('states') and \
'readonly' in res[field]['states']:
del res[field]['states']['readonly']
for arg in ('digits', 'invisible'):
if hasattr(cls._fields[field], arg) \
and getattr(cls._fields[field], arg):
res[field][arg] = copy.copy(getattr(cls._fields[field],
arg))
if ((isinstance(cls._fields[field],
(fields.Function, fields.One2Many,
fields.Many2Many))
and not getattr(cls, 'order_%s' % field, None))
or not hasattr(cls, 'search')):
res[field]['sortable'] = False
if ((isinstance(cls._fields[field], fields.Function)
and not (cls._fields[field].searcher
or getattr(cls, 'domain_%s' % field, None)))
or (cls._fields[field]._type in ('binary', 'sha'))
or not hasattr(cls, 'search')):
res[field]['searchable'] = False
else:
res[field]['searchable'] = True
definition[fname]['readonly'] = True
states = decoder.decode(definition[fname]['states'])
states.pop('readonly', None)
definition[fname]['states'] = encoder.encode(states)
for right in ['create', 'delete']:
definition[fname][right] = accesses.get(
fname, {}).get(right, True)
if Transaction().context.get('language'):
# translate the field label
res_trans = Translation.get_source(
cls.__name__ + ',' + field, 'field',
Transaction().context['language'])
if res_trans:
res[field]['string'] = res_trans
help_trans = Translation.get_source(
cls.__name__ + ',' + field, 'help',
Transaction().context['language'])
if help_trans:
res[field]['help'] = help_trans
if hasattr(cls._fields[field], 'selection'):
if isinstance(cls._fields[field].selection, (tuple, list)):
sel = copy.copy(cls._fields[field].selection)
if (Transaction().context.get('language')
and ((hasattr(cls._fields[field],
'translate_selection')
and cls._fields[field].translate_selection)
or not hasattr(cls._fields[field],
'translate_selection'))):
# translate each selection option
sel2 = []
for (key, val) in sel:
val2 = Translation.get_source(
cls.__name__ + ',' + field, 'selection',
language, val)
sel2.append((key, val2 or val))
sel = sel2
res[field]['selection'] = sel
else:
# call the 'dynamic selection' function
res[field]['selection'] = copy.copy(
cls._fields[field].selection)
if res[field]['type'] in (
'one2many',
'many2many',
'many2one',
'one2one',
):
if hasattr(cls._fields[field], 'model_name'):
relation = copy.copy(cls._fields[field].model_name)
else:
relation = copy.copy(
cls._fields[field].get_target().__name__)
res[field]['relation'] = relation
res[field]['context'] = copy.copy(cls._fields[field].context)
res[field]['create'] = accesses.get(field, {}).get('create',
True)
res[field]['delete'] = accesses.get(field, {}).get('delete',
True)
if res[field]['type'] == 'one2many' \
and getattr(cls._fields[field], 'field', None):
res[field]['relation_field'] = copy.copy(
cls._fields[field].field)
if res[field]['type'] == 'many2one':
target = cls._fields[field].get_target()
relation_fields = []
for target_name, target_field in target._fields.items():
if (target_field._type == 'one2many'
and target_field.model_name == cls.__name__
and target_field.field == field):
relation_fields.append(target_name)
# Set relation_field only if there is no ambiguity
if len(relation_fields) == 1:
res[field]['relation_field'], = relation_fields
if res[field]['type'] in ('datetime', 'time', 'timestamp'):
res[field]['format'] = copy.copy(cls._fields[field].format)
if res[field]['type'] == 'selection':
res[field]['context'] = copy.copy(cls._fields[field].context)
if res[field]['type'] == 'dict':
res[field]['schema_model'] = cls._fields[field].schema_model
res[field]['domain'] = copy.copy(cls._fields[field].domain)
res[field]['context'] = copy.copy(cls._fields[field].context)
res[field]['create'] = accesses.get(field, {}).get('create',
True)
res[field]['delete'] = accesses.get(field, {}).get('delete',
True)
filter_ = getattr(cls._fields[field], 'filter', None)
if filter_:
res[field]['domain'] = ['AND', res[field]['domain'], filter_]
# convert attributes into pyson
for attr in ('states', 'domain', 'context', 'digits', 'size',
'add_remove', 'format', 'search_order', 'search_context'):
if attr in res[field]:
res[field][attr] = encoder.encode(res[field][attr])
for i in list(res.keys()):
for fname in list(definition.keys()):
# filter out fields which aren't in the fields_names list
if fields_names:
if i not in fields_names:
del res[i]
elif not ModelAccess.check_relation(cls.__name__, i, mode='read'):
del res[i]
return res
if fname not in fields_names:
del definition[fname]
elif not ModelAccess.check_relation(
cls.__name__, fname, mode='read'):
del definition[fname]
return definition
def pre_validate(self):
pass