One of the main issue with ormcache is that the invalidation clears everything, meaning that some value, slow to compute but with a long lifetime, can be removed from the cache because an easy to invalidate value is cleared, like after writting or creating a product has an example. Most example in the code will try to invalidate the cache of the models doing something like `env['ir.qweb'].clear_caches()` but it is finally equivalent to `env.registry.clear_cache()`, and cross worker. The idea is to have multiple cache, maybe with specific sizes for a specific purpose. Having one per model is maybe a bad idea because it will be difficult to size the LRU correcly, and it is too dynamic. Checking invalidation may be expensive. The proposed solution is closed allow a limited number of named caches, using onse sequence per cache. This is actually close to the cache_longterm. We want to discourage using a specific cache for one use case in the buisness code. Adding a cache shouldn't be something easy, doable in stable. Note that we could also change the invalisation mecanism using an insert only table. We an check the sequence of this table, but also fetch all invalidation messages. Another possible improvement, especially if we have more than x cache is to have a global sequence, checking signaling would mean to check the main sequence, and only the other ones if the main one changed. Note that this poc is inspired from the long term cache but not all use case where applie yet. Part-of: odoo/odoo#119813
278 lines
13 KiB
Python
278 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo.tests.common import TransactionCase
|
|
from odoo.exceptions import ValidationError
|
|
from odoo import Command
|
|
|
|
|
|
class TestHasGroup(TransactionCase):
|
|
def setUp(self):
|
|
super(TestHasGroup, self).setUp()
|
|
|
|
self.group0 = 'test_user_has_group.group0'
|
|
self.group1 = 'test_user_has_group.group1'
|
|
group0, group1 = self.env['res.groups']._load_records([
|
|
dict(xml_id=self.group0, values={'name': 'group0'}),
|
|
dict(xml_id=self.group1, values={'name': 'group1'}),
|
|
])
|
|
|
|
self.test_user = self.env['res.users'].create({
|
|
'login': 'testuser',
|
|
'partner_id': self.env['res.partner'].create({
|
|
'name': "Strawman Test User"
|
|
}).id,
|
|
'groups_id': [Command.set([group0.id])]
|
|
})
|
|
|
|
self.grp_internal_xml_id = 'base.group_user'
|
|
self.grp_internal = self.env.ref(self.grp_internal_xml_id)
|
|
self.grp_portal_xml_id = 'base.group_portal'
|
|
self.grp_portal = self.env.ref(self.grp_portal_xml_id)
|
|
self.grp_public_xml_id = 'base.group_public'
|
|
self.grp_public = self.env.ref(self.grp_public_xml_id)
|
|
|
|
def test_env_uid(self):
|
|
Users = self.env['res.users'].with_user(self.test_user)
|
|
self.assertTrue(
|
|
Users.has_group(self.group0),
|
|
"the test user should belong to group0"
|
|
)
|
|
self.assertFalse(
|
|
Users.has_group(self.group1),
|
|
"the test user should *not* belong to group1"
|
|
)
|
|
|
|
def test_record(self):
|
|
self.assertTrue(
|
|
self.test_user.has_group(self.group0),
|
|
"the test user should belong to group0",
|
|
)
|
|
self.assertFalse(
|
|
self.test_user.has_group(self.group1),
|
|
"the test user shoudl not belong to group1"
|
|
)
|
|
|
|
def test_portal_creation(self):
|
|
"""Here we check that portal user creation fails if it tries to create a user
|
|
who would also have group_user by implied_group.
|
|
Otherwise, it succeeds with the groups we asked for.
|
|
"""
|
|
grp_public = self.env.ref('base.group_public')
|
|
grp_test_portal_xml_id = 'test_user_has_group.portal_implied_group'
|
|
grp_test_portal = self.env['res.groups']._load_records([
|
|
dict(xml_id=grp_test_portal_xml_id, values={'name': 'Test Group Portal'})
|
|
])
|
|
grp_test_internal1 = self.env['res.groups']._load_records([
|
|
dict(xml_id='test_user_has_group.internal_implied_group1', values={'name': 'Test Group Itnernal 1'})
|
|
])
|
|
grp_test_internal2_xml_id = 'test_user_has_group.internal_implied_group2'
|
|
grp_test_internal2 = self.env['res.groups']._load_records([
|
|
dict(xml_id=grp_test_internal2_xml_id, values={'name': 'Test Group Internal 2'})
|
|
])
|
|
self.grp_portal.implied_ids = grp_test_portal
|
|
|
|
grp_test_internal1.implied_ids = False
|
|
grp_test_internal2.implied_ids = False
|
|
|
|
portal_user = self.env['res.users'].create({
|
|
'login': 'portalTest',
|
|
'name': 'Portal test',
|
|
'sel_groups_%s_%s_%s' % (self.grp_internal.id, self.grp_portal.id, grp_public.id): self.grp_portal.id,
|
|
'sel_groups_%s_%s' % (grp_test_internal1.id, grp_test_internal2.id): grp_test_internal2.id,
|
|
})
|
|
|
|
self.assertTrue(
|
|
portal_user.has_group(self.grp_portal_xml_id),
|
|
"The portal user should belong to '%s'" % self.grp_portal_xml_id,
|
|
)
|
|
self.assertTrue(
|
|
portal_user.has_group(grp_test_portal_xml_id),
|
|
"The portal user should belong to '%s'" % grp_test_portal_xml_id,
|
|
)
|
|
self.assertTrue(
|
|
portal_user.has_group(grp_test_internal2_xml_id),
|
|
"The portal user should belong to '%s'" % grp_test_internal2_xml_id
|
|
)
|
|
self.assertFalse(
|
|
portal_user.has_group(self.grp_internal_xml_id),
|
|
"The portal user should not belong to '%s'" % self.grp_internal_xml_id
|
|
)
|
|
|
|
portal_user.unlink() # otherwise, badly modifying the implication would raise
|
|
|
|
grp_test_internal1.implied_ids = self.grp_internal
|
|
grp_test_internal2.implied_ids = self.grp_internal
|
|
|
|
with self.assertRaises(ValidationError): # current group implications forbid to create a portal user
|
|
portal_user = self.env['res.users'].create({
|
|
'login': 'portalFail',
|
|
'name': 'Portal fail',
|
|
'sel_groups_%s_%s_%s' % (self.grp_internal.id, self.grp_portal.id, grp_public.id): self.grp_portal.id,
|
|
'sel_groups_%s_%s' % (grp_test_internal1.id, grp_test_internal2.id): grp_test_internal2.id,
|
|
})
|
|
|
|
def test_portal_write(self):
|
|
"""Check that adding a new group to a portal user works as expected,
|
|
except if it implies group_user/public, in chich case it should raise.
|
|
"""
|
|
grp_test_portal = self.env["res.groups"].create({"name": "implied by portal"})
|
|
self.grp_portal.implied_ids = grp_test_portal
|
|
|
|
portal_user = self.env['res.users'].create({
|
|
'login': 'portalTest2',
|
|
'name': 'Portal test 2',
|
|
'groups_id': [Command.set([self.grp_portal.id])],
|
|
})
|
|
|
|
self.assertEqual(
|
|
portal_user.groups_id, (self.grp_portal + grp_test_portal),
|
|
"The portal user should have the implied group.",
|
|
)
|
|
|
|
grp_fail = self.env["res.groups"].create(
|
|
{"name": "fail", "implied_ids": [Command.set([self.grp_internal.id])]})
|
|
|
|
with self.assertRaises(ValidationError):
|
|
portal_user.write({'groups_id': [Command.link(grp_fail.id)]})
|
|
|
|
def test_two_user_types(self):
|
|
#Create a user with two groups of user types kind (Internal and Portal)
|
|
grp_test = self.env['res.groups']._load_records([
|
|
dict(xml_id='test_two_user_types.implied_groups', values={'name': 'Test Group'})
|
|
])
|
|
grp_test.implied_ids += self.grp_internal
|
|
grp_test.implied_ids += self.grp_portal
|
|
|
|
with self.assertRaises(ValidationError):
|
|
self.env['res.users'].create({
|
|
'login': 'test_two_user_types',
|
|
'name': "Test User with two user types",
|
|
'groups_id': [Command.set([grp_test.id])]
|
|
})
|
|
|
|
#Add a user with portal to the group Internal
|
|
test_user = self.env['res.users'].create({
|
|
'login': 'test_user_portal',
|
|
'name': "Test User with two user types",
|
|
'groups_id': [Command.set([self.grp_portal.id])]
|
|
})
|
|
with self.assertRaises(ValidationError):
|
|
self.grp_internal.users = [Command.link(test_user.id)]
|
|
|
|
def test_two_user_types_implied_groups(self):
|
|
"""Contrarily to test_two_user_types, we simply add an implied_id to a group.
|
|
This will trigger the addition of the relevant users to the relevant groups;
|
|
if, say, this was done in SQL and thus bypassing the ORM, it would bypass the constraints
|
|
and thus give us a case uncovered by the aforementioned test.
|
|
"""
|
|
grp_test = self.env["res.groups"].create(
|
|
{"name": "test", "implied_ids": [Command.set([self.grp_internal.id])]})
|
|
|
|
test_user = self.env['res.users'].create({
|
|
'login': 'test_user_portal',
|
|
'name': "Test User with one user types",
|
|
'groups_id': [Command.set([grp_test.id])]
|
|
})
|
|
|
|
with self.assertRaises(ValidationError):
|
|
grp_test.write({'implied_ids': [Command.link(self.grp_portal.id)]})
|
|
|
|
def test_demote_user(self):
|
|
"""When a user is demoted to the status of portal/public,
|
|
we should strip him of all his (previous) rights
|
|
"""
|
|
group_0 = self.env.ref(self.group0) # the group to which test_user already belongs
|
|
group_U = self.env["res.groups"].create({"name": "U", "implied_ids": [Command.set([self.grp_internal.id])]})
|
|
self.grp_internal.implied_ids = False # only there to simplify the test by not having to care about its trans_implied_ids
|
|
|
|
self.test_user.write({'groups_id': [Command.link(group_U.id)]})
|
|
self.assertEqual(
|
|
self.test_user.groups_id, (group_0 + group_U + self.grp_internal),
|
|
"We should have our 2 groups and the implied user group",
|
|
)
|
|
|
|
# Now we demote him. The JS framework sends 3 and 4 commands,
|
|
# which is what we write here, but it should work even with a 5 command or whatever.
|
|
self.test_user.write({'groups_id': [
|
|
Command.unlink(self.grp_internal.id),
|
|
Command.unlink(self.grp_public.id),
|
|
Command.link(self.grp_portal.id),
|
|
]})
|
|
|
|
# if we screw up the removing groups/adding the implied ids, we could end up in two situations:
|
|
# 1. we have a portal user with way too much rights (e.g. 'Contact Creation', which does not imply any other group)
|
|
# 2. because a group may be (transitively) implying group_user, then it would raise an exception
|
|
# so as a compromise we remove all groups when demoting a user
|
|
# (even technical display groups, e.g. TaxB2B, which could be re-added later)
|
|
self.assertEqual(
|
|
self.test_user.groups_id, (self.grp_portal),
|
|
"Here the portal group does not imply any other group, so we should only have this group.",
|
|
)
|
|
|
|
def test_implied_groups(self):
|
|
""" We check that the adding of implied ids works correctly for normal users and portal users.
|
|
In the second case, working normally means raising if a group implies to give 'group_user'
|
|
rights to a portal user.
|
|
"""
|
|
U = self.env["res.users"]
|
|
G = self.env["res.groups"]
|
|
group_user = self.env.ref('base.group_user')
|
|
group_portal = self.env.ref('base.group_portal')
|
|
group_no_one = self.env.ref('base.group_no_one')
|
|
|
|
group_A = G.create({"name": "A"})
|
|
group_AA = G.create({"name": "AA", "implied_ids": [Command.set([group_A.id])]})
|
|
group_B = G.create({"name": "B"})
|
|
group_BB = G.create({"name": "BB", "implied_ids": [Command.set([group_B.id])]})
|
|
|
|
# user_a is a normal user, so we expect groups to be added when we add them,
|
|
# as well as 'implied_groups'; otherwise nothing else should happen.
|
|
# By contrast, for a portal user we want implied groups not to be added
|
|
# if and only if it would not give group_user (or group_public) privileges
|
|
user_a = U.create({"name": "a", "login": "a", "groups_id": [Command.set([group_AA.id, group_user.id])]})
|
|
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_user + group_no_one))
|
|
|
|
user_b = U.create({"name": "b", "login": "b", "groups_id": [Command.set([group_portal.id, group_AA.id])]})
|
|
self.assertEqual(user_b.groups_id, (group_AA + group_A + group_portal))
|
|
|
|
# user_b is not an internal user, but giving it a new group just added a new group
|
|
(user_a + user_b).write({"groups_id": [Command.link(group_BB.id)]})
|
|
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_BB + group_B + group_user + group_no_one))
|
|
self.assertEqual(user_b.groups_id, (group_AA + group_A + group_BB + group_B + group_portal))
|
|
|
|
# now we create a group that implies the group_user
|
|
# adding it to a user should work normally, whereas adding it to a portal user should raise
|
|
group_C = G.create({"name": "C", "implied_ids": [Command.set([group_user.id])]})
|
|
|
|
user_a.write({"groups_id": [Command.link(group_C.id)]})
|
|
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_BB + group_B + group_C + group_user + group_no_one))
|
|
|
|
with self.assertRaises(ValidationError):
|
|
user_b.write({"groups_id": [Command.link(group_C.id)]})
|
|
|
|
def test_has_group_cleared_cache_on_write(self):
|
|
self.env.registry.clear_cache()
|
|
self.assertFalse(self.registry._Registry__caches['default'], "Ensure ormcache is empty")
|
|
|
|
def populate_cache():
|
|
self.test_user.has_group('test_user_has_group.group0')
|
|
self.assertTrue(self.registry._Registry__caches['default'], "user.has_group cache must be populated")
|
|
|
|
populate_cache()
|
|
|
|
self.env.ref(self.group0).write({"share": True})
|
|
self.assertFalse(self.registry._Registry__caches['default'], "Writing on group must invalidate user.has_group cache")
|
|
|
|
populate_cache()
|
|
# call_cache_clearing_methods is called in res.groups.write to invalidate
|
|
# cache before calling its parent class method (`odoo.models.Model.write`)
|
|
# as explain in the `res.group.write` comment.
|
|
# This verifies that calling `call_cache_clearing_methods()` invalidates
|
|
# the ormcache of method `user.has_group()`
|
|
self.env['ir.model.access'].call_cache_clearing_methods()
|
|
self.assertFalse(
|
|
self.registry._Registry__caches['default'],
|
|
"call_cache_clearing_methods() must invalidate user.has_group cache"
|
|
)
|