[FIX] core: unexpected MissingError when prefetching record
This is a rare case where method _read() raises a MissingError instead of just ignoring it. The issue is triggered by several conditions on a model M: - at least one ir.rule on M with a domain using a column field on M; - one deleted record Y which is in the prefetch set of a record X; - one reads a non-column field on record X. Fix method _read() to manage that case. It adds an extra call to exists() in that case, but adds no overhead in the general case. closes odoo/odoo#108050 X-original-commit: 1876dc87e5c88c711c9b3882c39be704ffc46532 Signed-off-by: Raphael Collet <rco@odoo.com> Signed-off-by: Rémy Voet <ryv@odoo.com>
This commit is contained in:
committed by
Raphael Collet
parent
58b7d309ce
commit
c6c79473dd
@@ -13,7 +13,7 @@ import psycopg2
|
||||
|
||||
from odoo import models, fields, Command
|
||||
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.exceptions import AccessError, MissingError, UserError, ValidationError
|
||||
from odoo.tests import common
|
||||
from odoo.tools import mute_logger, float_repr
|
||||
from odoo.tools.date_utils import add, subtract, start_of, end_of
|
||||
@@ -1624,6 +1624,54 @@ class TestFields(TransactionCaseWithUserDemo):
|
||||
with self.assertRaises(AccessError):
|
||||
cat1.name
|
||||
|
||||
def test_32_prefetch_missing_error(self):
|
||||
""" Test that prefetching non-column fields works in the presence of deleted records. """
|
||||
Discussion = self.env['test_new_api.discussion']
|
||||
|
||||
# add an ir.rule that forces reading field 'name'
|
||||
self.env['ir.rule'].create({
|
||||
'model_id': self.env['ir.model']._get(Discussion._name).id,
|
||||
'groups': [self.env.ref('base.group_user').id],
|
||||
'domain_force': "[('name', '!=', 'Super Secret discution')]",
|
||||
})
|
||||
|
||||
records = Discussion.with_user(self.user_demo).create([
|
||||
{'name': 'EXISTING'},
|
||||
{'name': 'MISSING'},
|
||||
])
|
||||
|
||||
# unpack to keep the prefetch on each recordset
|
||||
existing, deleted = records
|
||||
self.assertEqual(existing._prefetch_ids, records._ids)
|
||||
|
||||
# this invalidates the caches but the prefetching remains the same
|
||||
deleted.unlink()
|
||||
|
||||
# this should not trigger a MissingError
|
||||
existing.categories
|
||||
|
||||
# invalidate 'categories' for the assertQueryCount
|
||||
records.invalidate_model(['categories'])
|
||||
with self.assertQueryCount(4):
|
||||
# <categories>.__get__(existing)
|
||||
# -> records._fetch_field(['categories'])
|
||||
# -> records._read(['categories'])
|
||||
# -> records.check_access_rule('read')
|
||||
# -> records._filter_access_rules_python('read')
|
||||
# -> records.filtered_domain(...)
|
||||
# -> <name>.__get__(existing)
|
||||
# -> records._fetch_field(['name'])
|
||||
# -> records._read(['name', ...])
|
||||
# -> ONE QUERY to read ['name', ...] of records
|
||||
# -> ONE QUERY for deleted.exists() / code: forbidden = missing.exists()
|
||||
# -> ONE QUERY for records.exists() / code: self = self.exists()
|
||||
# -> ONE QUERY to read the many2many of existing
|
||||
existing.categories
|
||||
|
||||
# this one must trigger a MissingError
|
||||
with self.assertRaises(MissingError):
|
||||
deleted.categories
|
||||
|
||||
def test_40_real_vs_new(self):
|
||||
""" test field access on new records vs real records. """
|
||||
Model = self.env['test_new_api.category']
|
||||
|
||||
+1
-1
@@ -1167,7 +1167,7 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
recs._fetch_field(self)
|
||||
except AccessError:
|
||||
record._fetch_field(self)
|
||||
if not env.cache.contains(record, self) and not record.exists():
|
||||
if not env.cache.contains(record, self):
|
||||
raise MissingError("\n".join([
|
||||
_("Record does not exist or has been deleted."),
|
||||
_("(Record: %s, User: %s)") % (record, env.uid),
|
||||
|
||||
+11
-2
@@ -3221,8 +3221,17 @@ class BaseModel(metaclass=MetaModel):
|
||||
cr.execute(query_str, params + [sub_ids])
|
||||
result += cr.fetchall()
|
||||
else:
|
||||
self.check_access_rule('read')
|
||||
result = [(id_,) for id_ in self.ids]
|
||||
try:
|
||||
self.check_access_rule('read')
|
||||
except MissingError:
|
||||
# Method _read() should never raise a MissingError, but method
|
||||
# check_access_rule() can, because it must read fields on self.
|
||||
# So we restrict 'self' to existing records (to avoid an extra
|
||||
# exists() at the end of the method).
|
||||
self = self.exists()
|
||||
self.check_access_rule('read')
|
||||
|
||||
result = [(id_,) for id_ in self._ids]
|
||||
|
||||
fetched = self.browse()
|
||||
if result:
|
||||
|
||||
Reference in New Issue
Block a user