From 98bb26a386dfa5af0e19ecbc980542cc51bdee48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 13 Feb 2019 10:37:40 +0100 Subject: [PATCH] Move field definition from Model to Field issue8037 review74441002 --- CHANGELOG | 1 + doc/ref/models/fields.rst | 8 ++ trytond/model/fields/binary.py | 8 +- trytond/model/fields/char.py | 6 + trytond/model/fields/date.py | 17 ++- trytond/model/fields/dict.py | 5 + trytond/model/fields/field.py | 39 ++++++ trytond/model/fields/float.py | 7 ++ trytond/model/fields/function.py | 8 ++ trytond/model/fields/many2many.py | 16 +++ trytond/model/fields/many2one.py | 19 +++ trytond/model/fields/one2many.py | 17 +++ trytond/model/fields/reference.py | 11 +- trytond/model/fields/selection.py | 39 +++++- trytond/model/model.py | 198 ++++-------------------------- 15 files changed, 221 insertions(+), 178 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cdfbc155e2..a454fef974 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst index fc898fa3c1..3c85a054e2 100644 --- a/doc/ref/models/fields.rst +++ b/doc/ref/models/fields.rst @@ -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 ============= diff --git a/trytond/model/fields/binary.py b/trytond/model/fields/binary.py index 91a77d29ee..f6db1c89bc 100644 --- a/trytond/model/fields/binary.py +++ b/trytond/model/fields/binary.py @@ -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 diff --git a/trytond/model/fields/char.py b/trytond/model/fields/char.py index 1231558d53..301c4c8ed5 100644 --- a/trytond/model/fields/char.py +++ b/trytond/model/fields/char.py @@ -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 diff --git a/trytond/model/fields/date.py b/trytond/model/fields/date.py index f59eff8737..1718f1e800 100644 --- a/trytond/model/fields/date.py +++ b/trytond/model/fields/date.py @@ -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 diff --git a/trytond/model/fields/dict.py b/trytond/model/fields/dict.py index 0d2d9f79f2..0502081a0b 100644 --- a/trytond/model/fields/dict.py +++ b/trytond/model/fields/dict.py @@ -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' diff --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py index 5c5ebfa85b..0a7f2adc27 100644 --- a/trytond/model/fields/field.py +++ b/trytond/model/fields/field.py @@ -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 diff --git a/trytond/model/fields/float.py b/trytond/model/fields/float.py index cf85d885b3..4ae2ce3ce7 100644 --- a/trytond/model/fields/float.py +++ b/trytond/model/fields/float.py @@ -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 diff --git a/trytond/model/fields/function.py b/trytond/model/fields/function.py index e8bc6e9c93..abafb5630b 100644 --- a/trytond/model/fields/function.py +++ b/trytond/model/fields/function.py @@ -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): diff --git a/trytond/model/fields/many2many.py b/trytond/model/fields/many2many.py index 4f905dbded..4c2d5621f2 100644 --- a/trytond/model/fields/many2many.py +++ b/trytond/model/fields/many2many.py @@ -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 diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py index a2987ee306..744c5f3c0b 100644 --- a/trytond/model/fields/many2one.py +++ b/trytond/model/fields/many2one.py @@ -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 diff --git a/trytond/model/fields/one2many.py b/trytond/model/fields/one2many.py index 87737808e1..d63855d7d8 100644 --- a/trytond/model/fields/one2many.py +++ b/trytond/model/fields/one2many.py @@ -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 diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index 858845df58..4cec96cb7c 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -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 diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py index ec354ac9ec..12ce9448e1 100644 --- a/trytond/model/fields/selection.py +++ b/trytond/model/fields/selection.py @@ -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' diff --git a/trytond/model/model.py b/trytond/model/model.py index 242d285e70..15d70e1d14 100644 --- a/trytond/model/model.py +++ b/trytond/model/model.py @@ -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