Cause:
Due to the need to understand odoo's permission management, I went to see how odoo grants permissions to users. I found a lot that I couldn't understand. Therefore, we intend to start from the user's xml. See what it means inside
The first step is to check the user's xml. Find user source code
odoo / odoo / addons / base / views / res_users_views.xml
<record id="view_users_form" model="ir.ui.view">
<field name="name">res.users.form</field>
<field name="model">res.users</field>
<field name="arch" type="xml">
<form string="Users">
<header>
</header>
<sheet>
由于只看权限,这里全部省略....
<notebook colspan="4">
<page name="access_rights" string="Access Rights">
<group string="Multi Companies" attrs="{'invisible': [('companies_count', '<=', 1)]}">
<field string="Allowed Companies" name="company_ids" widget="many2many_tags" options="{'no_create': True}"/>
<field string="Current Company" name="company_id" context="{'user_preference': 0}"/>
<field string="Companies count" name="companies_count" invisible="1"/>
</group>
<field name="groups_id"/>
</page>
<page string="Preferences">
省略......
</page>
</notebook>
</sheet>
</form>
</field>
</record>
Find. There is no permission part of the code. . Then I looked for the inherited view_users_form, and found nothing. It's right to think about it. Since the permissions are dynamically generated, they must be done in the background, instead of being fixed in the foreground.
The interface needs to be updated due to additions, deletions and changes. So check the user's code behind. turn up
@api.model
def _update_user_groups_view(self):
""" Modify the view with xmlid ``base.user_groups_view``, which inherits
the user form view, and introduces the reified group fields.
"""
# remove the language to avoid translations, it will be handled at the view level
self = self.with_context(lang=None)
# We have to try-catch this, because at first init the view does not
# exist but we are already creating some basic groups.
# 获取 base.user_groups_view 组view 的xml
view = self.env.ref('base.user_groups_view', raise_if_not_found=False)
if view and view.exists() and view._name == 'ir.ui.view':
group_no_one = view.env.ref('base.group_no_one')
group_employee = view.env.ref('base.group_user')
xml1, xml2, xml3 = [], [], []
# 这两个固定
xml1.append(E.separator(string='User Type', colspan="2", groups='base.group_no_one'))
xml2.append(E.separator(string='Application Accesses', colspan="2"))
user_type_field_name = ''
for app, kind, gs in self.get_groups_by_application():
attrs = {}
# hide groups in categories 'Hidden' and 'Extra' (except for group_no_one)
if app.xml_id in ('base.module_category_hidden', 'base.module_category_extra', 'base.module_category_usability'):
attrs['groups'] = 'base.group_no_one'
# User type (employee, portal or public) is a separated group. This is the only 'selection'
# group of res.groups without implied groups (with each other).
if app.xml_id == 'base.module_category_user_type':
# application name with a selection field
field_name = name_selection_groups(gs.ids)
user_type_field_name = field_name
attrs['widget'] = 'radio'
attrs['groups'] = 'base.group_no_one'
xml1.append(E.field(name=field_name, **attrs))
xml1.append(E.newline())
elif kind == 'selection':
# 只要属于selection 的就放在Application Accesses 下
# application name with a selection field
field_name = name_selection_groups(gs.ids)
xml2.append(E.field(name=field_name, **attrs))
xml2.append(E.newline())
else:
# application separator with boolean fields
# 不属于那两个的则为 app_name
app_name = app.name or 'Other'
xml3.append(E.separator(string=app_name, colspan="4", **attrs))
for g in gs:
field_name = name_boolean_group(g.id)
if g == group_no_one:
# make the group_no_one invisible in the form view
xml3.append(E.field(name=field_name, invisible="1", **attrs))
else:
xml3.append(E.field(name=field_name, **attrs))
xml3.append({'class': "o_label_nowrap"})
if user_type_field_name:
user_type_attrs = {'invisible': [(user_type_field_name, '!=', group_employee.id)]}
else:
user_type_attrs = {}
xml = E.field(
E.group(*(xml1), col="2"),
E.group(*(xml2), col="2", attrs=str(user_type_attrs)),
E.group(*(xml3), col="4", attrs=str(user_type_attrs)), name="groups_id", position="replace")
xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
xml_content = etree.tostring(xml, pretty_print=True, encoding="unicode")
new_context = dict(view._context)
new_context.pop('install_mode_data', None) # don't set arch_fs for this computed view
new_context['lang'] = None
# 写入base.user_groups_view
view.with_context(new_context).write({'arch': xml_content})
can be seen. Generated completely in the background. . All have comments. Just talk about the principle
<record id="user_groups_view" model="ir.ui.view">
<field name="name">res.users.groups</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="view_users_form"/>
<field name="arch" type="xml">
<!-- dummy, will be modified by groups -->
<field name="groups_id" position="after"/>
</field>
</record>
1. Get the view of user_groups_view first. . And this view inherits users_form, and after
2. Generate content according to logic. Then write to the arch of the database and it is ok
There is still a question, how to determine the selection
def get_groups_by_application(self):
""" Return all groups classified by application (module category), as a list::
[(app, kind, groups), ...],
where ``app`` and ``groups`` are recordsets, and ``kind`` is either
``'boolean'`` or ``'selection'``. Applications are given in sequence
order. If ``kind`` is ``'selection'``, ``groups`` are given in
reverse implication order.
"""
# app 分类 gs 组
def linearize(app, gs):
# 'User Type' is an exception 特例 app.xml_id 是计算字段
if app.xml_id == 'base.module_category_user_type':
return (app, 'selection', gs.sorted('id'))
# determine sequence order: a group appears after its implied groups 自己继承的组
order = {g: len(g.trans_implied_ids & gs) for g in gs}
# check whether order is total, i.e., sequence orders are distinct
#判断当前group 是否有平级关系,没有平级关系的话,res_groups_implied_rel 肯定无数据,set{g:0,g:0}
# /set{g:1,g:2,g:1} 肯定不等于gs(组对象)
if len(set(order.values())) == len(gs):
return (app, 'selection', gs.sorted(key=order.get))
else:
return (app, 'boolean', gs)
# classify all groups by application
by_app, others = defaultdict(self.browse), self.browse()
# 遍历所有group
for g in self.get_application_groups([]):
if g.category_id:
#如果有 category
by_app[g.category_id] += g
else:
others += g
# build the result
res = []
for app, gs in sorted(by_app.items(), key=lambda it: it[0].sequence or 0):
#app 分类 gs 组
res.append(linearize(app, gs))
if others:
res.append((self.env['ir.module.category'], 'boolean', others))
return res
The key point is order = {g: len(g.trans_implied_ids & gs) for g in gs}
trans_implied_ids = fields.Many2many('res.groups', string='Transitively inherits',
compute='_compute_trans_implied')
@api.depends('implied_ids.trans_implied_ids')
def _compute_trans_implied(self):
# Compute the transitive closure recursively. Note that the performance
# is good, because the record cache behaves as a memo (the field is
# never computed twice on a given group.)
for g in self:
g.trans_implied_ids = g.implied_ids | g.mapped('implied_ids.trans_implied_ids')
As can be seen. trans_implied_ids is actually res_groups_implied_rel that only thinks about itself.
Only when the group of the current category and the id of trans_implied_ids are equal to each other, the selection will be displayed
a -----b
---------c
Not equal