The cache key of _get_bindings was not super efficient.
The result of _get_bindings is cached,
but its performance was altered by the cache
key which requires to fetch the user groups for each call to
_get_bindings.
Besides, as there is a lot of possible group
combination, this resulted in a lot of possible cache keys,
and therefore a lot of cached values.
This revision aims to make _get_bindings more efficient
by:
- do not use the groups in the cache keys (less cached values)
- filter out actions not available to the user groups after
retrieving them from the cache
- use has_group to do the above, which is itself cached as well,
and therefore do not need to fetch the user groups
at each call to get_bindings.
In addition, move get_bindings from `get_view`
to `get_views`. If there was 3 views asked by `get_views`
(let's say kanban, list, form)
`get_bindings` was being called 3 times, through `get_view`
with each time the same arguments and therefore the same result :-).
Moving it to `get_views` allows to call it only once for all view types
requested, and for the web client it doesn't change much,
as it always request the toolbar/get_bindings through `get_views` only.
In addition, add the lang to the cache of _get_bindings.
it was actually a bug not to put it: if you had 2 users
with the same group set, using 2 different languages,
the user accessing first the get_bindings would cache
the action names within his language, and then the second
user would see the action name within the language of the first user
:-).
Before
```py
In [1]: %time for i in range(1000): self.env['ir.actions.actions'].get_bindings('res.partner'); self.env.invalidate_all();
CPU times: user 790 ms, sys: 104 ms, total: 893 ms
Wall time: 1.7 s
```
After
```py
In [1]: %time for i in range(1000): self.env['ir.actions.actions'].get_bindings('res.partner'); self.env.invalidate_all();
CPU times: user 23.5 ms, sys: 9.12 ms, total: 32.7 ms
Wall time: 36.9 ms
```
Part-of: odoo/odoo#99417
220 lines
10 KiB
Python
220 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import OrderedDict
|
|
from itertools import chain
|
|
from lxml import etree
|
|
|
|
from odoo.addons.hr.tests.common import TestHrCommon
|
|
from odoo.tests import new_test_user, tagged, Form
|
|
from odoo.exceptions import AccessError
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestSelfAccessProfile(TestHrCommon):
|
|
|
|
def test_access_my_profile(self):
|
|
""" A simple user should be able to read all fields in his profile """
|
|
james = new_test_user(self.env, login='hel', groups='base.group_user', name='Simple employee', email='ric@example.com')
|
|
james = james.with_user(james)
|
|
self.env['hr.employee'].create({
|
|
'name': 'James',
|
|
'user_id': james.id,
|
|
})
|
|
view = self.env.ref('hr.res_users_view_form_profile')
|
|
view_infos = james.get_view(view.id)
|
|
fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')]
|
|
james.read(fields)
|
|
|
|
def test_readonly_fields(self):
|
|
""" Employee related fields should be readonly if self editing is not allowed """
|
|
self.env['ir.config_parameter'].sudo().set_param('hr.hr_employee_self_edit', False)
|
|
james = new_test_user(self.env, login='hel', groups='base.group_user', name='Simple employee', email='ric@example.com')
|
|
james = james.with_user(james)
|
|
self.env['hr.employee'].create({
|
|
'name': 'James',
|
|
'user_id': james.id,
|
|
})
|
|
|
|
view = self.env.ref('hr.res_users_view_form_profile')
|
|
fields = james._fields
|
|
view_infos = james.get_view(view.id)
|
|
employee_related_fields = {
|
|
el.get('name')
|
|
for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')
|
|
if fields[el.get('name')].related and fields[el.get('name')].related.split('.')[0] == 'employee_id'
|
|
}
|
|
|
|
form = Form(james, view=view)
|
|
for field in employee_related_fields:
|
|
with self.assertRaises(AssertionError, msg="Field '%s' should be readonly in the employee profile when self editing is not allowed." % field):
|
|
form.__setattr__(field, 'some value')
|
|
|
|
|
|
def test_profile_view_fields(self):
|
|
""" A simple user should see all fields in profile view, even if they are protected by groups """
|
|
view = self.env.ref('hr.res_users_view_form_profile')
|
|
|
|
# For reference, check the view with user with every groups protecting user fields
|
|
all_groups_xml_ids = chain(*[
|
|
field.groups.split(',')
|
|
for field in self.env['res.users']._fields.values()
|
|
if field.groups
|
|
if field.groups != '.' # "no-access" group on purpose
|
|
])
|
|
all_groups = self.env['res.groups']
|
|
for xml_id in all_groups_xml_ids:
|
|
all_groups |= self.env.ref(xml_id.strip())
|
|
user_all_groups = new_test_user(self.env, groups='base.group_user', login='hel', name='God')
|
|
user_all_groups.write({'groups_id': [(4, group.id, False) for group in all_groups]})
|
|
view_infos = self.env['res.users'].with_user(user_all_groups).get_view(view.id)
|
|
full_fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')]
|
|
|
|
# Now check the view for a simple user
|
|
user = new_test_user(self.env, login='gro', name='Grouillot')
|
|
view_infos = self.env['res.users'].with_user(user).get_view(view.id)
|
|
fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')]
|
|
|
|
# Compare both
|
|
self.assertEqual(full_fields, fields, "View fields should not depend on user's groups")
|
|
|
|
def test_access_my_profile_toolbar(self):
|
|
""" A simple user shouldn't have the possibilities to see the 'Change Password' action"""
|
|
james = new_test_user(self.env, login='jam', groups='base.group_user', name='Simple employee', email='jam@example.com')
|
|
james = james.with_user(james)
|
|
self.env['hr.employee'].create({
|
|
'name': 'James',
|
|
'user_id': james.id,
|
|
})
|
|
view = self.env.ref('hr.res_users_view_form_profile')
|
|
available_actions = james.get_views([(view.id, 'form')], {'toolbar': True})['views']['form']['toolbar']['action']
|
|
change_password_action = self.env.ref("base.change_password_wizard_action")
|
|
|
|
self.assertFalse(any(x['id'] == change_password_action.id for x in available_actions))
|
|
|
|
# An ERP manager should have the possibilities to see the 'Change Password'
|
|
john = new_test_user(self.env, login='joh', groups='base.group_erp_manager', name='ERP Manager', email='joh@example.com')
|
|
john = john.with_user(john)
|
|
self.env['hr.employee'].create({
|
|
'name': 'John',
|
|
'user_id': john.id,
|
|
})
|
|
view = self.env.ref('hr.res_users_view_form_profile')
|
|
available_actions = john.get_views([(view.id, 'form')], {'toolbar': True})['views']['form']['toolbar']['action']
|
|
self.assertTrue(any(x['id'] == change_password_action.id for x in available_actions))
|
|
|
|
|
|
class TestSelfAccessRights(TestHrCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestSelfAccessRights, cls).setUpClass()
|
|
cls.richard = new_test_user(cls.env, login='ric', groups='base.group_user', name='Simple employee', email='ric@example.com')
|
|
cls.richard_emp = cls.env['hr.employee'].create({
|
|
'name': 'Richard',
|
|
'user_id': cls.richard.id,
|
|
'address_home_id': cls.env['res.partner'].create({'name': 'Richard', 'phone': '21454', 'type': 'private'}).id,
|
|
})
|
|
cls.hubert = new_test_user(cls.env, login='hub', groups='base.group_user', name='Simple employee', email='hub@example.com')
|
|
cls.hubert_emp = cls.env['hr.employee'].create({
|
|
'name': 'Hubert',
|
|
'user_id': cls.hubert.id,
|
|
'address_home_id': cls.env['res.partner'].create({'name': 'Hubert', 'type': 'private'}).id,
|
|
})
|
|
|
|
cls.protected_fields_emp = OrderedDict([(k, v) for k, v in cls.env['hr.employee']._fields.items() if v.groups == 'hr.group_hr_user'])
|
|
# Compute fields and id field are always readable by everyone
|
|
cls.read_protected_fields_emp = OrderedDict([(k, v) for k, v in cls.env['hr.employee']._fields.items() if not v.compute and k != 'id'])
|
|
cls.self_protected_fields_user = OrderedDict([
|
|
(k, v)
|
|
for k, v in cls.env['res.users']._fields.items()
|
|
if v.groups == 'hr.group_hr_user' and k in cls.env['res.users'].SELF_READABLE_FIELDS
|
|
])
|
|
|
|
# Read hr.employee #
|
|
def testReadSelfEmployee(self):
|
|
with self.assertRaises(AccessError):
|
|
self.hubert_emp.with_user(self.richard).read(self.protected_fields_emp.keys())
|
|
|
|
def testReadOtherEmployee(self):
|
|
with self.assertRaises(AccessError):
|
|
self.hubert_emp.with_user(self.richard).read(self.protected_fields_emp.keys())
|
|
|
|
# Write hr.employee #
|
|
def testWriteSelfEmployee(self):
|
|
for f in self.protected_fields_emp:
|
|
with self.assertRaises(AccessError):
|
|
self.richard_emp.with_user(self.richard).write({f: 'dummy'})
|
|
|
|
def testWriteOtherEmployee(self):
|
|
for f in self.protected_fields_emp:
|
|
with self.assertRaises(AccessError):
|
|
self.hubert_emp.with_user(self.richard).write({f: 'dummy'})
|
|
|
|
# Read res.users #
|
|
def testReadSelfUserEmployee(self):
|
|
for f in self.self_protected_fields_user:
|
|
self.richard.with_user(self.richard).read([f]) # should not raise
|
|
|
|
def testReadOtherUserEmployee(self):
|
|
with self.assertRaises(AccessError):
|
|
self.hubert.with_user(self.richard).read(self.self_protected_fields_user)
|
|
|
|
# Write res.users #
|
|
def testWriteSelfUserEmployeeSettingFalse(self):
|
|
for f, v in self.self_protected_fields_user.items():
|
|
with self.assertRaises(AccessError):
|
|
self.richard.with_user(self.richard).write({f: 'dummy'})
|
|
|
|
def testWriteSelfUserEmployee(self):
|
|
self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', True)
|
|
for f, v in self.self_protected_fields_user.items():
|
|
val = None
|
|
if v.type == 'char' or v.type == 'text':
|
|
val = '0000' if f == 'pin' else 'dummy'
|
|
if val is not None:
|
|
self.richard.with_user(self.richard).write({f: val})
|
|
|
|
def testWriteSelfUserPreferencesEmployee(self):
|
|
# self should always be able to update non hr.employee fields if
|
|
# they are in SELF_READABLE_FIELDS
|
|
self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False)
|
|
# should not raise
|
|
vals = [
|
|
{'tz': "Australia/ACT"},
|
|
{'email': "new@example.com"},
|
|
{'signature': "<p>I'm Richard!</p>"},
|
|
{'notification_type': "email"},
|
|
]
|
|
for v in vals:
|
|
# should not raise
|
|
self.richard.with_user(self.richard).write(v)
|
|
|
|
def testWriteOtherUserPreferencesEmployee(self):
|
|
# self should always be able to update non hr.employee fields if
|
|
# they are in SELF_READABLE_FIELDS
|
|
self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False)
|
|
vals = [
|
|
{'tz': "Australia/ACT"},
|
|
{'email': "new@example.com"},
|
|
{'signature': "<p>I'm Richard!</p>"},
|
|
{'notification_type': "email"},
|
|
]
|
|
for v in vals:
|
|
with self.assertRaises(AccessError):
|
|
self.hubert.with_user(self.richard).write(v)
|
|
|
|
def testWriteSelfPhoneEmployee(self):
|
|
# phone is a related from res.partner (from base) but added in SELF_READABLE_FIELDS
|
|
self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False)
|
|
with self.assertRaises(AccessError):
|
|
self.richard.with_user(self.richard).write({'phone': '2154545'})
|
|
|
|
def testWriteOtherUserEmployee(self):
|
|
for f in self.self_protected_fields_user:
|
|
with self.assertRaises(AccessError):
|
|
self.hubert.with_user(self.richard).write({f: 'dummy'})
|
|
|
|
def testSearchUserEMployee(self):
|
|
# Searching user based on employee_id field should not raise bad query error
|
|
self.env['res.users'].with_user(self.richard).search([('employee_id', 'ilike', 'Hubert')])
|