From 3e9784c8d7176459e126a1395b4fae02a14b02bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Thu, 4 Apr 2019 19:06:09 +0200 Subject: [PATCH] Set context on record instantiated in Field.__set__ and read related Field may have a context defined so when it instantiate a relational record, the field should set this context like when the record is read from the database in ModelStorage.__getattr__. And when such field is read as related field, the context should also be set. issue8225 review287171002 --- CHANGELOG | 2 + trytond/model/fields/field.py | 17 ++++++- trytond/model/fields/many2many.py | 7 ++- trytond/model/fields/many2one.py | 13 +++-- trytond/model/fields/one2many.py | 7 ++- trytond/model/fields/reference.py | 12 +++-- trytond/model/modelsql.py | 72 +++++++++++++-------------- trytond/tests/field_many2many.py | 29 +++++++++++ trytond/tests/field_many2one.py | 21 ++++++++ trytond/tests/field_one2many.py | 22 ++++++++ trytond/tests/field_reference.py | 21 ++++++++ trytond/tests/test_field_many2many.py | 37 ++++++++++++++ trytond/tests/test_field_many2one.py | 41 +++++++++++++++ trytond/tests/test_field_one2many.py | 37 ++++++++++++++ trytond/tests/test_field_reference.py | 41 +++++++++++++++ 15 files changed, 327 insertions(+), 52 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8ae1b2edc5..51b01ecccd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Set context on record instantiated by Field.__set__ +* Set context when reading related fields * Check read access on field in search order (issue8189) * Add base64 converter to URL map * Add HTML widget diff --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py index 0116cf5da9..a2809916bd 100644 --- a/trytond/model/fields/field.py +++ b/trytond/model/fields/field.py @@ -9,7 +9,7 @@ from sql.conditionals import Coalesce, NullIf from sql.operators import Concat from trytond import backend -from trytond.pyson import PYSON, PYSONEncoder, Eval +from trytond.pyson import PYSON, PYSONEncoder, PYSONDecoder, Eval from trytond.const import OPERATORS from trytond.tools.string_ import StringPartitioned from trytond.transaction import Transaction @@ -162,6 +162,21 @@ def instanciate_values(Target, value): return tuple(instance(x) for x in (value or [])) +def instantiate_context(field, record): + from ..modelstorage import EvalEnvironment + ctx = {} + if field.context: + pyson_context = PYSONEncoder().encode(field.context) + ctx.update(PYSONDecoder( + EvalEnvironment(record, record.__class__)).decode( + pyson_context)) + datetime_ = None + if getattr(field, 'datetime_field', None): + datetime_ = getattr(record, field.datetime_field, None) + ctx = {'_datetime': datetime_} + return ctx + + def on_change_result(record): return record._changed_values diff --git a/trytond/model/fields/many2many.py b/trytond/model/fields/many2many.py index 18d27d56ba..0c68be0d4a 100644 --- a/trytond/model/fields/many2many.py +++ b/trytond/model/fields/many2many.py @@ -8,7 +8,7 @@ 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) + search_order_validate, context_validate, instantiate_context) from ...pool import Pool from ...tools import grouped_slice from ...transaction import Transaction @@ -278,7 +278,10 @@ class Many2Many(Field): def __set__(self, inst, value): Target = self.get_target() - super(Many2Many, self).__set__(inst, instanciate_values(Target, value)) + ctx = instantiate_context(self, inst) + with Transaction().set_context(ctx): + records = instanciate_values(Target, value) + super(Many2Many, self).__set__(inst, records) def convert_domain_tree(self, domain, tables): Target = self.get_target() diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py index 744c5f3c0b..92ecf69779 100644 --- a/trytond/model/fields/many2one.py +++ b/trytond/model/fields/many2one.py @@ -7,7 +7,7 @@ from sql.operators import Or from trytond.pyson import PYSONEncoder from .field import (Field, search_order_validate, context_validate, - with_inactive_records) + with_inactive_records, instantiate_context) from ...pool import Pool from ...tools import reduce_ids from ...transaction import Transaction @@ -100,10 +100,13 @@ class Many2One(Field): def __set__(self, inst, value): Target = self.get_target() - if isinstance(value, dict): - value = Target(**value) - elif isinstance(value, int): - value = Target(value) + if isinstance(value, (dict, int)): + ctx = instantiate_context(self, inst) + with Transaction().set_context(ctx): + if isinstance(value, dict): + value = Target(**value) + elif isinstance(value, int): + value = Target(value) assert isinstance(value, (Target, type(None))) super(Many2One, self).__set__(inst, value) diff --git a/trytond/model/fields/one2many.py b/trytond/model/fields/one2many.py index e28b395c49..24d67e0923 100644 --- a/trytond/model/fields/one2many.py +++ b/trytond/model/fields/one2many.py @@ -7,7 +7,7 @@ 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) + search_order_validate, context_validate, instantiate_context) from ...pool import Pool from ...tools import grouped_slice from ...transaction import Transaction @@ -253,7 +253,10 @@ class One2Many(Field): def __set__(self, inst, value): Target = self.get_target() - super(One2Many, self).__set__(inst, instanciate_values(Target, value)) + ctx = instantiate_context(self, inst) + with Transaction().set_context(ctx): + records = instanciate_values(Target, value) + super(One2Many, self).__set__(inst, records) def convert_domain(self, domain, tables, Model): from ..modelsql import convert_from diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index 4cec96cb7c..3b71d453ff 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -7,7 +7,7 @@ from sql.functions import Substring, Position from trytond.pyson import PYSONEncoder from .field import (Field, search_order_validate, context_validate, - with_inactive_records) + with_inactive_records, instantiate_context) from .selection import SelectionMixin from ...transaction import Transaction from ...pool import Pool @@ -144,10 +144,12 @@ class Reference(SelectionMixin, Field): else: target, value = value Target = Pool().get(target) - if isinstance(value, dict): - value = Target(**value) - else: - value = Target(value) + ctx = instantiate_context(self, inst) + with Transaction().set_context(ctx): + if isinstance(value, dict): + value = Target(**value) + else: + value = Target(value) super(Reference, self).__set__(inst, value) def sql_format(self, value): diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 6ab59ca2ec..0a6f9a6a11 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.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. import datetime -from itertools import islice, chain, product +from itertools import islice, chain, product, groupby from collections import OrderedDict, defaultdict from functools import wraps @@ -19,6 +19,7 @@ from trytond import backend from trytond.tools import reduce_ids, grouped_slice, cursor_dict from trytond.transaction import Transaction from trytond.pool import Pool +from trytond.pyson import PYSONEncoder, PYSONDecoder from trytond.cache import LRUDict from trytond.exceptions import ConcurrencyException from trytond.rpc import RPC @@ -690,19 +691,21 @@ class ModelSQL(ModelStorage): # construct a clause for the rules : domain = Rule.domain_get(cls.__name__, mode='read') - fields_related = {} - datetime_fields = [] + fields_related = defaultdict(set) + extra_fields = set() for field_name in fields_names: if field_name == '_timestamp': continue if '.' in field_name: - field, field_related = field_name.split('.', 1) - fields_related.setdefault(field, []) - fields_related[field].append(field_related) - else: - field = cls._fields[field_name] + field_name, field_related = field_name.split('.', 1) + fields_related[field_name].add(field_related) + field = cls._fields[field_name] if hasattr(field, 'datetime_field') and field.datetime_field: - datetime_fields.append(field.datetime_field) + extra_fields.add(field.datetime_field) + if field.context: + extra_fields.update(fields.get_eval_fields(field.context)) + all_fields = ( + set(fields_names) | set(fields_related.keys()) | extra_fields) result = [] table = cls.__table__() @@ -722,7 +725,7 @@ class ModelSQL(ModelStorage): history_limit = 1 columns = [] - for f in fields_names + list(fields_related.keys()) + datetime_fields: + for f in all_fields: field = cls._fields.get(f) if field and field.sql_type(): columns.append(field.sql_column(table).as_(f)) @@ -779,8 +782,7 @@ class ModelSQL(ModelStorage): cachable_fields.append(fname) # all fields for which there is a get attribute - getter_fields = [f for f in - fields_names + list(fields_related.keys()) + datetime_fields + getter_fields = [f for f in all_fields if f in cls._fields and hasattr(cls._fields[f], 'get')] if getter_fields and cachable_fields: @@ -833,15 +835,6 @@ class ModelSQL(ModelStorage): for row in result: row[fname] = getter_result[row['id']] - def group_by(rows, keyfunc=None): - result = defaultdict(list) - if keyfunc is None: - result[None] = rows - else: - for row in rows: - result[keyfunc(row)].append(row) - return result - def read_related(field, Target, rows, fields): name = field.name target_ids = [] @@ -880,7 +873,7 @@ class ModelSQL(ModelStorage): row[key] = None to_del = set() - for fname in chain(fields_related.keys(), datetime_fields): + for fname in set(fields_related.keys()) | extra_fields: if fname not in fields_names: to_del.add(fname) if fname not in cls._fields: @@ -889,27 +882,32 @@ class ModelSQL(ModelStorage): continue field = cls._fields[fname] datetime_field = getattr(field, 'datetime_field', None) - if field._type == 'reference': - def keyfunc(row): + + def keyfunc(row): + ctx = {} + if field.context: + pyson_context = PYSONEncoder().encode(field.context) + ctx.update(PYSONDecoder(row).decode(pyson_context)) + if datetime_field: + ctx['_datetime'] = row.get(datetime_field) + if field._type == 'reference': value = row[fname] if not value: - return (None, None) - model, _ = value.split(',', 1) - return (pool.get(model), row.get(datetime_field)) - else: - Target = field.get_target() + Target = None + else: + model, _ = value.split(',', 1) + Target = pool.get(model) + else: + Target = field.get_target() + return Target, ctx - def keyfunc(row): - return (Target, row.get(datetime_field)) - for (Target, datetime_), rows in group_by( - result, keyfunc).items(): - ctx = {} - if datetime_: - ctx['_datetime'] = datetime_ + for (Target, ctx), rows in groupby( + sorted(result, key=keyfunc), key=keyfunc): + rows = list(rows) with Transaction().set_context(ctx): if Target: targets = read_related( - field, Target, rows, fields_related[fname]) + field, Target, rows, list(fields_related[fname])) targets = {t['id']: t for t in targets} else: targets = {} diff --git a/trytond/tests/field_many2many.py b/trytond/tests/field_many2many.py index 90b4678fd7..d70c793839 100644 --- a/trytond/tests/field_many2many.py +++ b/trytond/tests/field_many2many.py @@ -3,6 +3,7 @@ from trytond.model import ModelSQL, fields from trytond.pool import Pool +from trytond.transaction import Transaction class Many2Many(ModelSQL): @@ -154,6 +155,31 @@ class Many2ManyTreeRelation(ModelSQL): child = fields.Many2One('test.many2many_tree', 'Child') +class Many2ManyContext(ModelSQL): + "Many2Many Context" + __name__ = 'test.many2many_context' + targets = fields.Many2Many( + 'test.many2many_context.relation', 'origin', 'target', "Targets", + context={'test': 'foo'}) + + +class Many2ManyContextRelation(ModelSQL): + "Many2Many Context Relation" + __name__ = 'test.many2many_context.relation' + origin = fields.Many2One('test.many2many_context', "Origin") + target = fields.Many2One('test.many2many_context.target', "Target") + + +class Many2ManyContextTarget(ModelSQL): + "Many2Many Context Target" + __name__ = 'test.many2many_context.target' + context = fields.Function(fields.Char("context"), 'get_context') + + def get_context(self, name): + context = Transaction().context + return context.get('test') + + def register(module): Pool.register( Many2Many, @@ -176,4 +202,7 @@ def register(module): Many2ManyFilterDomainRelation, Many2ManyTree, Many2ManyTreeRelation, + Many2ManyContext, + Many2ManyContextTarget, + Many2ManyContextRelation, module=module, type_='model') diff --git a/trytond/tests/field_many2one.py b/trytond/tests/field_many2one.py index 1fc56ac6fb..6c8cc07986 100644 --- a/trytond/tests/field_many2one.py +++ b/trytond/tests/field_many2one.py @@ -3,6 +3,7 @@ from trytond.model import ModelSQL, ModelStorage, DeactivableMixin, fields from trytond.pool import Pool +from trytond.transaction import Transaction class Many2OneTarget(DeactivableMixin, ModelSQL): @@ -64,6 +65,24 @@ class Many2OneMPTT(ModelSQL): return 0 +class Many2OneContext(ModelSQL): + "Many2One Context" + __name__ = 'test.many2one_context' + target = fields.Many2One( + 'test.many2one_context.target', "target", + context={'test': 'foo'}) + + +class Many2OneTargetContext(ModelSQL): + "Many2One Target Context" + __name__ = 'test.many2one_context.target' + context = fields.Function(fields.Char("context"), 'get_context') + + def get_context(self, name): + context = Transaction().context + return context.get('test') + + def register(module): Pool.register( Many2OneTarget, @@ -73,4 +92,6 @@ def register(module): Many2OneTargetStorage, Many2OneTree, Many2OneMPTT, + Many2OneContext, + Many2OneTargetContext, module=module, type_='model') diff --git a/trytond/tests/field_one2many.py b/trytond/tests/field_one2many.py index 3c7c97316b..d0830ef9dc 100644 --- a/trytond/tests/field_one2many.py +++ b/trytond/tests/field_one2many.py @@ -4,6 +4,7 @@ from trytond.model import ModelSQL, fields from trytond.pool import Pool from trytond.pyson import Eval +from trytond.transaction import Transaction class One2Many(ModelSQL): @@ -110,6 +111,25 @@ class One2ManyFilterDomainTarget(ModelSQL): value = fields.Integer('Value') +class One2ManyContext(ModelSQL): + "One2Many Context" + __name__ = 'test.one2many_context' + targets = fields.One2Many( + 'test.one2many_context.target', 'origin', "Targets", + context={'test': 'foo'}) + + +class One2ManyContextTarget(ModelSQL): + "One2Many Context Target" + __name__ = 'test.one2many_context.target' + origin = fields.Many2One('test.one2many_context', "Origin") + context = fields.Function(fields.Char("context"), 'get_context') + + def get_context(self, name): + context = Transaction().context + return context.get('test') + + def register(module): Pool.register( One2Many, @@ -126,4 +146,6 @@ def register(module): One2ManyFilterTarget, One2ManyFilterDomain, One2ManyFilterDomainTarget, + One2ManyContext, + One2ManyContextTarget, module=module, type_='model') diff --git a/trytond/tests/field_reference.py b/trytond/tests/field_reference.py index 89d4346a52..ca671d3b18 100644 --- a/trytond/tests/field_reference.py +++ b/trytond/tests/field_reference.py @@ -3,6 +3,7 @@ from trytond.model import ModelSQL, fields from trytond.pool import Pool +from trytond.transaction import Transaction class Reference(ModelSQL): @@ -29,9 +30,29 @@ class ReferenceRequired(ModelSQL): ], required=True) +class ReferenceContext(ModelSQL): + "Reference Context" + __name__ = 'test.reference_context' + target = fields.Reference("Reference", selection=[ + ('test.reference_context.target', "Target"), + ], context={'test': 'foo'}) + + +class ReferenceContextTarget(ModelSQL): + "Reference Context Target" + __name__ = 'test.reference_context.target' + context = fields.Function(fields.Char("context"), 'get_context') + + def get_context(self, name): + context = Transaction().context + return context.get('test') + + def register(module): Pool.register( Reference, ReferenceTarget, ReferenceRequired, + ReferenceContext, + ReferenceContextTarget, module=module, type_='model') diff --git a/trytond/tests/test_field_many2many.py b/trytond/tests/test_field_many2many.py index 1021c96d59..5008295b33 100644 --- a/trytond/tests/test_field_many2many.py +++ b/trytond/tests/test_field_many2many.py @@ -625,6 +625,43 @@ class FieldMany2ManyTestCase(unittest.TestCase, CommonTestCaseMixin): self.first1, self.first2, self.first3, self.first4, self.second1, self.second2, self.second3, self.second4])) + @with_transaction() + def test_context_attribute(self): + "Test context on many2many attribute" + pool = Pool() + Many2Many = pool.get('test.many2many_context') + + record, = Many2Many.create([{ + 'targets': [('create', [{}])], + }]) + + self.assertEqual(record.targets[0].context, 'foo') + + @with_transaction() + def test_context_read(self): + "Test context on many2many read" + pool = Pool() + Many2Many = pool.get('test.many2many_context') + + record, = Many2Many.create([{ + 'targets': [('create', [{}])], + }]) + data, = Many2Many.read([record.id], ['targets.context']) + + self.assertEqual(data['targets.'][0]['context'], 'foo') + + @with_transaction() + def test_context_set(self): + "Test context on many2many set" + pool = Pool() + Many2Many = pool.get('test.many2many_context') + Target = pool.get('test.many2many_context.target') + + target, = Target.create([{}]) + record = Many2Many(targets=[target.id]) + + self.assertEqual(record.targets[0].context, 'foo') + class FieldMany2ManyReferenceTestCase(unittest.TestCase, CommonTestCaseMixin): "Test Field Many2Many Reference" diff --git a/trytond/tests/test_field_many2one.py b/trytond/tests/test_field_many2one.py index e1a704d0d4..90fef4efc0 100644 --- a/trytond/tests/test_field_many2one.py +++ b/trytond/tests/test_field_many2one.py @@ -467,6 +467,47 @@ class FieldMany2OneTestCase(unittest.TestCase): "Test search many2one mptt not parent of empty" self._test_search_not_parent_of_empty('test.many2one_mptt') + @with_transaction() + def test_context_attribute(self): + "Test context on many2one attribute" + pool = Pool() + Many2One = pool.get('test.many2one_context') + Target = pool.get('test.many2one_context.target') + + target, = Target.create([{}]) + record, = Many2One.create([{ + 'target': target.id, + }]) + + self.assertEqual(record.target.context, 'foo') + + @with_transaction() + def test_context_read(self): + "Test context on many2one read" + pool = Pool() + Many2One = pool.get('test.many2one_context') + Target = pool.get('test.many2one_context.target') + + target, = Target.create([{}]) + record, = Many2One.create([{ + 'target': target.id, + }]) + data, = Many2One.read([record.id], ['target.context']) + + self.assertEqual(data['target.']['context'], 'foo') + + @with_transaction() + def test_context_set(self): + "Test context on many2one set" + pool = Pool() + Many2One = pool.get('test.many2one_context') + Target = pool.get('test.many2one_context.target') + + target, = Target.create([{}]) + record = Many2One(target=target.id) + + self.assertEqual(record.target.context, 'foo') + def suite(): return unittest.TestLoader().loadTestsFromTestCase(FieldMany2OneTestCase) diff --git a/trytond/tests/test_field_one2many.py b/trytond/tests/test_field_one2many.py index 4cb58bca41..6033c2555a 100644 --- a/trytond/tests/test_field_one2many.py +++ b/trytond/tests/test_field_one2many.py @@ -455,6 +455,43 @@ class FieldOne2ManyTestCase(unittest.TestCase, CommonTestCaseMixin): self.assertListEqual(one2manys, [one2many]) self.assertListEqual(one2manys_filtered, []) + @with_transaction() + def test_context_attribute(self): + "Test context on one2many attribute" + pool = Pool() + Many2One = pool.get('test.one2many_context') + + record, = Many2One.create([{ + 'targets': [('create', [{}])], + }]) + + self.assertEqual(record.targets[0].context, 'foo') + + @with_transaction() + def test_context_read(self): + "Test context on one2many read" + pool = Pool() + Many2One = pool.get('test.one2many_context') + + record, = Many2One.create([{ + 'targets': [('create', [{}])], + }]) + data, = Many2One.read([record.id], ['targets.context']) + + self.assertEqual(data['targets.'][0]['context'], 'foo') + + @with_transaction() + def test_context_set(self): + "Test context on one2many set" + pool = Pool() + Many2One = pool.get('test.one2many_context') + Target = pool.get('test.one2many_context.target') + + target, = Target.create([{}]) + record = Many2One(targets=[target.id]) + + self.assertEqual(record.targets[0].context, 'foo') + class FieldOne2ManyReferenceTestCase(unittest.TestCase, CommonTestCaseMixin): "Test Field One2Many Reference" diff --git a/trytond/tests/test_field_reference.py b/trytond/tests/test_field_reference.py index f75ec0f833..5d15d3b26c 100644 --- a/trytond/tests/test_field_reference.py +++ b/trytond/tests/test_field_reference.py @@ -398,6 +398,47 @@ class FieldReferenceTestCase(unittest.TestCase): self.assertEqual(reference.reference, None) + @with_transaction() + def test_context_attribute(self): + "Test context on reference attribute" + pool = Pool() + Reference = pool.get('test.reference_context') + Target = pool.get('test.reference_context.target') + + target, = Target.create([{}]) + record, = Reference.create([{ + 'target': str(target), + }]) + + self.assertEqual(record.target.context, 'foo') + + @with_transaction() + def test_context_read(self): + "Test context on reference read" + pool = Pool() + Reference = pool.get('test.reference_context') + Target = pool.get('test.reference_context.target') + + target, = Target.create([{}]) + record, = Reference.create([{ + 'target': str(target), + }]) + data, = Reference.read([record.id], ['target.context']) + + self.assertEqual(data['target.']['context'], 'foo') + + @with_transaction() + def test_context_set(self): + "Test context on reference set" + pool = Pool() + Reference = pool.get('test.reference_context') + Target = pool.get('test.reference_context.target') + + target, = Target.create([{}]) + record = Reference(target=str(target)) + + self.assertEqual(record.target.context, 'foo') + def suite(): return unittest.TestLoader().loadTestsFromTestCase(FieldReferenceTestCase)