[MERGE] add group implications, and create group fields on users form

bzr revid: rco@openerp.com-20110803085503-sgklwad22n1d6xz9
This commit is contained in:
Raphael Collet
2011-08-03 10:55:03 +02:00
2 changed files with 312 additions and 32 deletions
+304 -25
View File
@@ -29,13 +29,16 @@ import pooler
from tools.translate import _
from service import security
import netsvc
import logging
from lxml import etree
class groups(osv.osv):
_name = "res.groups"
_order = 'name'
_description = "Access Groups"
_columns = {
'name': fields.char('Group Name', size=64, required=True),
'name': fields.char('Group Name', size=64, required=True, translate=True),
'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
@@ -76,6 +79,21 @@ class groups(osv.osv):
aid.write({'groups_id': [(4, gid)]})
return gid
def unlink(self, cr, uid, ids, context=None):
group_users = []
for record in self.read(cr, uid, ids, ['users'], context=context):
if record['users']:
group_users.extend(record['users'])
if group_users:
user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
user_names = list(set(user_names))
if len(user_names) >= 5:
user_names = user_names[:5] + ['...']
raise osv.except_osv(_('Warning !'),
_('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
', '.join(user_names))
return super(groups, self).unlink(cr, uid, ids, context=context)
def get_extended_interface_group(self, cr, uid, context=None):
data_obj = self.pool.get('ir.model.data')
extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
@@ -510,30 +528,6 @@ class users(osv.osv):
users()
class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
_inherit = 'res.groups'
_columns = {
'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
}
def unlink(self, cr, uid, ids, context=None):
group_users = []
for record in self.read(cr, uid, ids, ['users'], context=context):
if record['users']:
group_users.extend(record['users'])
if group_users:
user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
user_names = list(set(user_names))
if len(user_names) >= 5:
user_names = user_names[:5] + ['...']
raise osv.except_osv(_('Warning !'),
_('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
', '.join(user_names))
return super(groups2, self).unlink(cr, uid, ids, context=context)
groups2()
class res_config_view(osv.osv_memory):
_name = 'res.config.view'
_inherit = 'res.config'
@@ -554,4 +548,289 @@ class res_config_view(osv.osv_memory):
res_config_view()
#
# Extension of res.groups and res.users with a relation for "implied" or
# "inherited" groups. Once a user belongs to a group, it automatically belongs
# to the implied groups (transitively).
#
class groups_implied(osv.osv):
_inherit = 'res.groups'
_columns = {
'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
string='Inherits', help='Users of this group automatically inherit those groups'),
}
def get_closure(self, cr, uid, ids, context=None):
"return the closure of ids, i.e., all groups recursively implied by ids"
closure = set()
todo = self.browse(cr, 1, ids)
while todo:
g = todo.pop()
if g.id not in closure:
closure.add(g.id)
todo.extend(g.implied_ids)
return list(closure)
def create(self, cr, uid, values, context=None):
users = values.pop('users', None)
gid = super(groups_implied, self).create(cr, uid, values, context)
if users:
# delegate addition of users to add implied groups
self.write(cr, uid, [gid], {'users': users}, context)
return gid
def write(self, cr, uid, ids, values, context=None):
res = super(groups_implied, self).write(cr, uid, ids, values, context)
if values.get('users') or values.get('implied_ids'):
# add implied groups (to all users of each group)
for g in self.browse(cr, uid, ids):
gids = self.get_closure(cr, uid, [g.id], context)
users = [(4, u.id) for u in g.users]
super(groups_implied, self).write(cr, uid, gids, {'users': users}, context)
return res
def get_maximal(self, cr, uid, ids, context=None):
"return the maximal element among the group ids"
max_set, max_closure = set(), set()
for gid in ids:
if gid not in max_closure:
closure = set(self.get_closure(cr, uid, [gid], context))
max_set -= closure # remove implied groups from max_set
max_set.add(gid) # gid is maximal
max_closure |= closure # update closure of max_set
if len(max_set) > 1:
log = logging.getLogger('res.groups')
log.warning('Groups %s are maximal among %s, only one expected.', max_set, ids)
return bool(max_set) and max_set.pop()
groups_implied()
class users_implied(osv.osv):
_inherit = 'res.users'
def create(self, cr, uid, values, context=None):
groups = values.pop('groups_id')
user_id = super(users_implied, self).create(cr, uid, values, context)
if groups:
# delegate addition of groups to add implied groups
self.write(cr, uid, [user_id], {'groups_id': groups}, context)
return user_id
def write(self, cr, uid, ids, values, context=None):
res = super(users_implied, self).write(cr, uid, ids, values, context)
if values.get('groups_id'):
# add implied groups for all users
groups_obj = self.pool.get('res.groups')
for u in self.browse(cr, uid, ids):
old_gids = map(int, u.groups_id)
new_gids = groups_obj.get_closure(cr, uid, old_gids, context)
if len(old_gids) != len(new_gids):
values = {'groups_id': [(6, 0, new_gids)]}
super(users_implied, self).write(cr, uid, [u.id], values, context)
return res
users_implied()
#
# Extension of res.groups and res.users for the special groups view in the users
# form. This extension presents groups with selection and boolean widgets:
# - Groups named as "App/Name" (corresponding to root menu "App") are presented
# per application, with one boolean and selection field each. The selection
# field defines a role "Name" for the given application.
# - Groups named as "Stuff/Name" are presented as boolean fields and grouped
# under sections "Stuff".
# - The remaining groups are presented as boolean fields and grouped in a
# section "Others".
#
class groups_view(osv.osv):
_inherit = 'res.groups'
def get_classified(self, cr, uid, context=None):
""" classify all groups by prefix; return a pair (apps, others) where
- both are lists like [("App", [("Name", browse_group), ...]), ...];
- apps is sorted in menu order;
- others are sorted in alphabetic order;
- groups not like App/Name are at the end of others, under _('Others')
"""
# sort groups by implication, with implied groups first
groups = self.browse(cr, uid, self.search(cr, uid, []), context)
groups.sort(key=lambda g: set(self.get_closure(cr, uid, [g.id], context)))
# classify groups depending on their names
classified = {}
for g in groups:
# split() returns 1 or 2 elements, so names[-2] is prefix or None
names = [None] + [s.strip() for s in g.name.split('/', 1)]
classified.setdefault(names[-2], []).append((names[-1], g))
# determine the apps (that correspond to root menus, in order)
menu_obj = self.pool.get('ir.ui.menu')
menu_ids = menu_obj.search(cr, uid, [('parent_id','=',False)], context={'ir.ui.menu.full_list': True})
apps = []
for m in menu_obj.browse(cr, uid, menu_ids, context):
if m.name in classified:
# application groups are already sorted by implication
apps.append((m.name, classified.pop(m.name)))
# other groups
others = sorted(classified.items(), key=lambda pair: pair[0])
if others and others[0][0] is None:
others.append((_('Others'), others.pop(0)[1]))
for sec, groups in others:
groups.sort(key=lambda pair: pair[0])
return (apps, others)
groups_view()
# Naming conventions for reified groups fields:
# - boolean field 'in_group_ID' is True iff
# ID is in 'groups_id'
# - boolean field 'in_groups_ID1_..._IDk' is True iff
# any of ID1, ..., IDk is in 'groups_id'
# - selection field 'sel_groups_ID1_..._IDk' is ID iff
# ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
def name_boolean_group(id): return 'in_group_' + str(id)
def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
def is_boolean_group(name): return name.startswith('in_group_')
def is_boolean_groups(name): return name.startswith('in_groups_')
def is_selection_groups(name): return name.startswith('sel_groups_')
def is_field_group(name):
return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
def get_boolean_group(name): return int(name[9:])
def get_boolean_groups(name): return map(int, name[10:].split('_'))
def get_selection_groups(name): return map(int, name[11:].split('_'))
def encode(s): return s.encode('utf8') if isinstance(s, unicode) else s
def partition(f, xs):
"return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
yes, nos = [], []
for x in xs:
if f(x):
yes.append(x)
else:
nos.append(x)
return yes, nos
class users_view(osv.osv):
_inherit = 'res.users'
def _process_values_groups(self, cr, uid, values, context=None):
""" transform all reified group fields into a 'groups_id', adding
also the implied groups """
add, rem = [], []
for k in values.keys():
if is_boolean_group(k):
if values.pop(k):
add.append(get_boolean_group(k))
else:
rem.append(get_boolean_group(k))
elif is_boolean_groups(k):
if not values.pop(k):
rem.extend(get_boolean_groups(k))
elif is_selection_groups(k):
gid = values.pop(k)
if gid:
rem.extend(get_selection_groups(k))
add.append(gid)
if add or rem:
# remove groups in 'rem' and add groups in 'add'
gdiff = [(3, id) for id in rem] + [(4, id) for id in add]
values.setdefault('groups_id', []).extend(gdiff)
return True
def create(self, cr, uid, values, context=None):
self._process_values_groups(cr, uid, values, context)
return super(users_view, self).create(cr, uid, values, context)
def write(self, cr, uid, ids, values, context=None):
self._process_values_groups(cr, uid, values, context)
return super(users_view, self).write(cr, uid, ids, values, context)
def read(self, cr, uid, ids, fields, context=None, load='_classic_read'):
if not fields:
group_fields, fields = [], self.fields_get(cr, uid, context).keys()
else:
group_fields, fields = partition(is_field_group, fields)
if group_fields:
group_obj = self.pool.get('res.groups')
fields.append('groups_id')
# read the normal fields (and 'groups_id')
res = super(users_view, self).read(cr, uid, ids, fields, context, load)
records = res if isinstance(res, list) else [res]
for record in records:
# get the field 'groups_id' and insert the group_fields
groups = set(record['groups_id'])
for f in group_fields:
if is_boolean_group(f):
record[f] = get_boolean_group(f) in groups
elif is_boolean_groups(f):
record[f] = not groups.isdisjoint(get_boolean_groups(f))
elif is_selection_groups(f):
selected = groups.intersection(get_selection_groups(f))
record[f] = group_obj.get_maximal(cr, uid, selected, context)
return res
return super(users_view, self).read(cr, uid, ids, fields, context, load)
def fields_view_get(self, cr, uid, view_id=None, view_type='form',
context=None, toolbar=False, submenu=False):
# in form views, transform 'groups_id' into reified group fields
res = super(users_view, self).fields_view_get(cr, uid, view_id, view_type,
context, toolbar, submenu)
if view_type == 'form':
root = etree.fromstring(encode(res['arch']))
nodes = root.xpath("//field[@name='groups_id']")
if nodes:
# replace node by the reified group fields
fields = res['fields']
elems = []
apps, others = self.pool.get('res.groups').get_classified(cr, uid, context)
# create section Applications
elems.append('<separator colspan="6" string="%s"/>' % _('Applications'))
for app, groups in apps:
ids = [g.id for name, g in groups]
app_name = name_boolean_groups(ids)
sel_name = name_selection_groups(ids)
selection = [(g.id, name) for name, g in groups]
fields[app_name] = {'type': 'boolean', 'string': app}
tips = [name + ': ' + (g.comment or '') for name, g in groups]
if tips:
fields[app_name].update(help='\n'.join(tips))
fields[sel_name] = {'type': 'selection', 'string': 'Group', 'selection': selection}
elems.append("""
<field name="%(app)s"/>
<field name="%(sel)s" nolabel="1" colspan="2"
attrs="{'invisible': [('%(app)s', '=', False)]}"
modifiers="{'invisible': [('%(app)s', '=', False)]}"/>
<newline/>
""" % {'app': app_name, 'sel': sel_name})
# create other sections
for sec, groups in others:
elems.append('<separator colspan="6" string="%s"/>' % sec)
for gname, g in groups:
name = name_boolean_group(g.id)
fields[name] = {'type': 'boolean', 'string': gname}
if g.comment:
fields[name].update(help=g.comment)
elems.append('<field name="%s"/>' % name)
elems.append('<newline/>')
# replace xml node by new arch
new_node = etree.fromstring('<group col="6">' + ''.join(elems) + '</group>')
for node in nodes:
node.getparent().replace(node, new_node)
res['arch'] = etree.tostring(root)
return res
users_view()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@ -5,13 +5,13 @@
<!--
Users Groups
-->
<record model="res.groups" id="group_system">
<field name="name">Administration / Configuration</field>
</record>
<record model="res.groups" id="group_erp_manager">
<field name="name">Administration / Access Rights</field>
</record>
<record model="res.groups" id="group_system">
<field name="name">Administration / Configuration</field>
<field name="implied_ids" eval="[(4, ref('group_erp_manager'))]"/>
</record>
<record model="res.groups" id="group_user">
<field name="name">Employee</field>
@@ -29,12 +29,13 @@
<field name="name">Useability / No One</field>
</record>
<record id="group_sale_manager" context="{'noadmin':True}" model="res.groups">
<field name="name">Sales / Manager</field>
</record>
<record id="group_sale_salesman" context="{'noadmin':True}" model="res.groups">
<field name="name">Sales / User</field>
</record>
<record id="group_sale_manager" context="{'noadmin':True}" model="res.groups">
<field name="name">Sales / Manager</field>
<field name="implied_ids" eval="[(4, ref('group_sale_salesman'))]"/>
</record>
<!-- Set accesses to menu -->
<record model="ir.ui.menu" id="base.menu_administration">