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
This commit is contained in:
Cédric Krier
2019-04-04 19:06:09 +02:00
parent 34316ae615
commit 3e9784c8d7
15 changed files with 327 additions and 52 deletions
+2
View File
@@ -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
+16 -1
View File
@@ -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
+5 -2
View File
@@ -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()
+8 -5
View File
@@ -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)
+5 -2
View File
@@ -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
+7 -5
View File
@@ -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):
+35 -37
View File
@@ -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 = {}
+29
View File
@@ -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')
+21
View File
@@ -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')
+22
View File
@@ -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')
+21
View File
@@ -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')
+37
View File
@@ -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"
+41
View File
@@ -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)
+37
View File
@@ -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"
+41
View File
@@ -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)