From e0f401bae4f5b2dadfcc279575ca0f2f6d4a60e1 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 26 Jul 2011 12:31:00 +0200 Subject: [PATCH 01/27] [IMP] merged the python classes defining res.groups bzr revid: rco@openerp.com-20110726103100-6ibtqsfnim3unpg3 --- openerp/addons/base/res/res_user.py | 40 ++++++++++++----------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index f3c0f635d14..58793bc638d 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -36,6 +36,7 @@ class groups(osv.osv): _description = "Access Groups" _columns = { 'name': fields.char('Group Name', size=64, required=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 +77,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 +526,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' From 398d401724bde7cf0db0072b040c51897c916847 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 27 Jul 2011 15:11:47 +0200 Subject: [PATCH 02/27] [IMP] add an implication relation between groups bzr revid: rco@openerp.com-20110727131147-yiqgzy3sjv3ismvi --- openerp/addons/base/res/res_user.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 58793bc638d..0dd5d011be3 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -526,6 +526,30 @@ class users(osv.osv): users() + + +class groups2(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 implied_groups(self, cr, uid, ids, context=None): + "return all groups recursively implied by the given ids" + closure = set() + todo = self.browse(cr, uid, ids, context) + while todo: + g = todo.pop() + if g.id not in closure: + closure.add(g.id) + todo.extend(g.implied_ids) + return list(closure) + +groups2() + + + class res_config_view(osv.osv_memory): _name = 'res.config.view' _inherit = 'res.config' From 33eef3434934fe6d3384cbab2ff64b784e4de3ba Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 28 Jul 2011 09:25:44 +0200 Subject: [PATCH 03/27] first sketch of groups with boolean fields bzr revid: rco@openerp.com-20110728072544-tyseggysom5y8azw --- openerp/addons/base/res/res_user.py | 82 +++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 0dd5d011be3..540c62ac23d 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -29,6 +29,7 @@ import pooler from tools.translate import _ from service import security import netsvc +from lxml import etree class groups(osv.osv): _name = "res.groups" @@ -535,10 +536,10 @@ class groups2(osv.osv): string='Inherits', help='Users of this group automatically inherit those groups'), } - def implied_groups(self, cr, uid, ids, context=None): - "return all groups recursively implied by the given ids" + def get_closure(self, cr, ids): + "return the closure of ids, i.e., all groups recursively implied by ids" closure = set() - todo = self.browse(cr, uid, ids, context) + todo = self.browse(cr, 1, ids) while todo: g = todo.pop() if g.id not in closure: @@ -550,6 +551,81 @@ groups2() +class users2(osv.osv): + _inherit = 'res.users' + + def values_to_groups_id(self, cr, uid, values, context=None): + """ transform all fields like 'in_group_ID' into a 'groups_id', adding + also the implied groups """ + add, rem = [], [] + for k in values.keys(): + if k.startswith('in_group_'): + # remove k from values, and place it in add or rem + (add if values.pop(k) else rem).append(int(k[9:])) + if add or rem: + # remove groups in 'rem' and add all implied groups in 'add' + add = self.pool.get('res.groups').get_closure(cr, add) + values['groups_id'] = [(3, id) for id in rem] + [(4, id) for id in add] + return True + + def create(self, cr, uid, values, context=None): + # add processing for boolean fields 'in_group_ID' + self.values_to_groups_id(cr, uid, values, context) + return super(users2, self).create(cr, uid, values, context) + + def write(self, cr, uid, ids, values, context=None): + # add processing for boolean fields 'in_group_ID' + self.values_to_groups_id(cr, uid, values, context) + return super(users2, self).write(cr, uid, ids, values, context) + + def read(self, cr, uid, ids, fields, context=None, load='_classic_read'): + # add processing for boolean fields 'in_group_ID' + group_fields = [] + other_fields = [] + for f in fields: + if f.startswith('in_group_'): + group_fields.append(f) + else: + other_fields.append(f) + if group_fields: + fields = other_fields + ['groups_id'] + res = super(users2, self).read(cr, uid, ids, fields, context, load) + for record in res: + # remove the field 'groups_id' and insert the group_fields + groups = record.pop('groups_id') + for f in group_fields: + record[f] = int(f[9:]) in groups + return res + return super(users2, 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 boolean fields 'in_group_ID' + res = super(users2, self).fields_view_get(cr, uid, view_id, view_type, + context, toolbar, submenu) + if view_type == 'form': + root = etree.fromstring(res['arch']) + nodes = root.xpath("//field[@name='groups_id']") + if nodes: + # replace node by the boolean fields 'in_group_ID' + fields = res['fields'] + del fields['groups_id'] + groups = self.pool.get('res.groups') + elems = [] + for g in groups.browse(cr, 1, groups.search(cr, 1, [])): + name = 'in_group_%d' % g.id + fields[name] = {'type': 'boolean', 'string': g.name, 'views': {}} + elems.append('' % name) + new_node = etree.fromstring('' + ''.join(elems) + '') + for node in nodes: + node.getparent().replace(node, new_node) + res['arch'] = etree.tostring(root) + return res + +users2() + + + class res_config_view(osv.osv_memory): _name = 'res.config.view' _inherit = 'res.config' From 0c5ec449bffc59411a46f4c3c03d9eccd8082d77 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 28 Jul 2011 09:29:48 +0200 Subject: [PATCH 04/27] make group names translatable bzr revid: rco@openerp.com-20110728072948-5nkse5obxz739r4r --- openerp/addons/base/res/res_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 540c62ac23d..651a4915e88 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -36,7 +36,7 @@ class groups(osv.osv): _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', From 8a459051d5b3cf69242f11aaad04702328dfa231 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 28 Jul 2011 11:59:58 +0200 Subject: [PATCH 05/27] add method to get groups classified bzr revid: rco@openerp.com-20110728095958-bgplcmlrwbp017nr --- openerp/addons/base/res/res_user.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 651a4915e88..6d53be5955a 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -547,6 +547,27 @@ class groups2(osv.osv): todo.extend(g.implied_ids) return list(closure) + def get_classified(self, cr, uid, context=None): + "return a classification (by application, etc.) of all groups" + # determine the names of apps (that correspond to root menus) + 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 = set([m.name for m in menu_obj.browse(cr, uid, menu_ids, context)]) + + # classify groups depending on their names + groups, others = {}, {} + for g in self.browse(cr, uid, self.search(cr, uid, []), context): + names = [s.strip() for s in g.name.split('/', 1)] + if len(names) > 1: + if names[0] in apps: + groups.setdefault(names[0], []).append((g.id, names[1])) + else: + others.setdefault(names[0], []).append((g.id, names[1])) + else: + others.setdefault(None, []).append((g.id, names[0])) + + return groups, others + groups2() From 5444ec5a227747af016d3afae23eed08d5b51804 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 28 Jul 2011 14:06:54 +0200 Subject: [PATCH 06/27] add method to compute the closure of the relation implied_ids bzr revid: rco@openerp.com-20110728120654-q0tewgjk7qxf6t8h --- openerp/addons/base/res/res_user.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 6d53be5955a..c583848d549 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -547,6 +547,21 @@ class groups2(osv.osv): todo.extend(g.implied_ids) return list(closure) + def get_rec_implied(self, cr): + "return a dictionary giving the recursively implied groups of each group" + groups = self.browse(cr, 1, self.search(cr, 1, [])) + # compute the transitive closure of implied_ids + succs = dict([(g.id, set()) for g in groups]) + preds = dict([(g.id, set()) for g in groups]) + for g in groups: + for h in g.implied_ids: + # link g and its predecessors to h and its successors + ps = preds[g.id].union([g.id]) + ss = succs[h.id].union([h.id]) + for p in ps: succs[p] |= ss + for s in ss: preds[s] |= ps + return succs + def get_classified(self, cr, uid, context=None): "return a classification (by application, etc.) of all groups" # determine the names of apps (that correspond to root menus) From ab63014e156c0b9077b23fb4dd6fa895baf891e3 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 28 Jul 2011 15:10:22 +0200 Subject: [PATCH 07/27] improve group classification bzr revid: rco@openerp.com-20110728131022-1w6t86lnext9e2yg --- openerp/addons/base/res/res_user.py | 42 ++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index c583848d549..4ffe6b0bd51 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -563,26 +563,42 @@ class groups2(osv.osv): return succs def get_classified(self, cr, uid, context=None): - "return a classification (by application, etc.) of all groups" - # determine the names of apps (that correspond to root menus) - 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 = set([m.name for m in menu_obj.browse(cr, uid, menu_ids, context)]) + """ + classify all groups by prefix; return a pair (apps, groups) where + - apps is a list like [("App", [(id, "Name"), ...]), ...], + - groups is a dictionary like {'Class': [(id, "Name"), ...], ...} + - the key None is used in groups for groups not like App/Name + """ + # get the relation to order groups + order_relation = self.get_rec_implied(cr) + order = lambda x, y: (x[0] in order_relation[y[0]] and 1 or -1) # classify groups depending on their names - groups, others = {}, {} + classified = {} for g in self.browse(cr, uid, self.search(cr, uid, []), context): names = [s.strip() for s in g.name.split('/', 1)] if len(names) > 1: - if names[0] in apps: - groups.setdefault(names[0], []).append((g.id, names[1])) - else: - others.setdefault(names[0], []).append((g.id, names[1])) + classified.setdefault(names[0], []).append((g.id, names[1])) else: - others.setdefault(None, []).append((g.id, names[0])) + classified.setdefault(None, []).append((g.id, names[0])) + + # 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: + # sort application groups by implication + groups = classified.pop(m.name) + groups.sort(order) + apps.append((m.name, groups)) + + # sort remaining groups alphabetically in their class + for groups in classified.itervalues(): + groups.sort(key=lambda pair: pair[1]) + + return (apps, classified) - return groups, others - groups2() From 8004cea65f009c29d746e6480511b8e3305e50a9 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 28 Jul 2011 16:51:31 +0200 Subject: [PATCH 08/27] build the view based on classified groups bzr revid: rco@openerp.com-20110728145131-j8myem3vd25j1drf --- openerp/addons/base/res/res_user.py | 94 ++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 21 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 4ffe6b0bd51..fdd8fc7302e 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -603,17 +603,42 @@ groups2() +# 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 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('_')) + class users2(osv.osv): _inherit = 'res.users' def values_to_groups_id(self, cr, uid, values, context=None): - """ transform all fields like 'in_group_ID' into a 'groups_id', adding + """ transform all reified group fields into a 'groups_id', adding also the implied groups """ add, rem = [], [] for k in values.keys(): - if k.startswith('in_group_'): - # remove k from values, and place it in add or rem - (add if values.pop(k) else rem).append(int(k[9:])) + if is_boolean_group(k): + (add if values.pop(k) else rem).append(get_boolean_group(k)) + elif is_boolean_groups(k): + if not values.pop(k): + rem.extend(get_selection_groups(k)) + elif is_selection_groups(k): + rem.extend(get_selection_groups(k)) + add.append(int(values.pop(k))) if add or rem: # remove groups in 'rem' and add all implied groups in 'add' add = self.pool.get('res.groups').get_closure(cr, add) @@ -621,21 +646,20 @@ class users2(osv.osv): return True def create(self, cr, uid, values, context=None): - # add processing for boolean fields 'in_group_ID' + # add processing for reified group fields self.values_to_groups_id(cr, uid, values, context) return super(users2, self).create(cr, uid, values, context) def write(self, cr, uid, ids, values, context=None): - # add processing for boolean fields 'in_group_ID' + # add processing for reified group fields self.values_to_groups_id(cr, uid, values, context) return super(users2, self).write(cr, uid, ids, values, context) def read(self, cr, uid, ids, fields, context=None, load='_classic_read'): - # add processing for boolean fields 'in_group_ID' - group_fields = [] - other_fields = [] + # add processing for reified group fields + group_fields, other_fields = [], [] for f in fields: - if f.startswith('in_group_'): + if is_boolean_group(f) or is_boolean_groups(f) or is_selection_groups(f): group_fields.append(f) else: other_fields.append(f) @@ -644,31 +668,59 @@ class users2(osv.osv): res = super(users2, self).read(cr, uid, ids, fields, context, load) for record in res: # remove the field 'groups_id' and insert the group_fields - groups = record.pop('groups_id') + groups = set(record.pop('groups_id')) for f in group_fields: - record[f] = int(f[9:]) in groups + 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): + record[f] = False + for gid in get_selection_groups(f): + if gid in groups: + record[f] = gid + break return res return super(users2, 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 boolean fields 'in_group_ID' + # in form views, transform 'groups_id' into reified group fields res = super(users2, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu) if view_type == 'form': root = etree.fromstring(res['arch']) nodes = root.xpath("//field[@name='groups_id']") if nodes: - # replace node by the boolean fields 'in_group_ID' + # replace node by the reified group fields fields = res['fields'] - del fields['groups_id'] - groups = self.pool.get('res.groups') elems = [] - for g in groups.browse(cr, 1, groups.search(cr, 1, [])): - name = 'in_group_%d' % g.id - fields[name] = {'type': 'boolean', 'string': g.name, 'views': {}} - elems.append('' % name) - new_node = etree.fromstring('' + ''.join(elems) + '') + apps, others = self.pool.get('res.groups').get_classified(cr, uid, context) + # create section Applications + elems.append('' % _('Applications')) + for app, selection in apps: + ids = [id for id, name in selection] + app_name = name_boolean_groups(ids) + sel_name = name_selection_groups(ids) + fields[app_name] = {'type': 'boolean', 'string': app} + fields[sel_name] = {'type': 'selection', 'string': '', 'selection': selection} + elems.append('' % app_name) + elems.append('' % sel_name) + elems.append('') + # create other sections + sections = sorted(others.items(), key=lambda pair: pair[0]) + if sections and sections[0][0] is None: + sec, selection = sections.pop(0) + sections.append((_('Others'), selection)) + for sec, selection in sections: + elems.append('' % sec) + for id, gname in selection: + name = name_boolean_group(id) + fields[name] = {'type': 'boolean', 'string': gname} + elems.append('' % name) + elems.append('') + # replace xml node by new arch + new_node = etree.fromstring('' + ''.join(elems) + '') for node in nodes: node.getparent().replace(node, new_node) res['arch'] = etree.tostring(root) From 82ba03cf9d33ecfce21afb06be2f7081d51840c1 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 09:40:25 +0200 Subject: [PATCH 09/27] fix fields_view_get, and hide selection field when app not selected bzr revid: rco@openerp.com-20110729074025-ugr2rkm1mxnwle1s --- openerp/addons/base/res/res_user.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index fdd8fc7302e..f8c5c034f61 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -623,6 +623,8 @@ 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 + class users2(osv.osv): _inherit = 'res.users' @@ -635,7 +637,7 @@ class users2(osv.osv): (add if values.pop(k) else rem).append(get_boolean_group(k)) elif is_boolean_groups(k): if not values.pop(k): - rem.extend(get_selection_groups(k)) + rem.extend(get_boolean_groups(k)) elif is_selection_groups(k): rem.extend(get_selection_groups(k)) add.append(int(values.pop(k))) @@ -689,7 +691,7 @@ class users2(osv.osv): res = super(users2, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu) if view_type == 'form': - root = etree.fromstring(res['arch']) + root = etree.fromstring(encode(res['arch'])) nodes = root.xpath("//field[@name='groups_id']") if nodes: # replace node by the reified group fields @@ -698,6 +700,7 @@ class users2(osv.osv): apps, others = self.pool.get('res.groups').get_classified(cr, uid, context) # create section Applications elems.append('' % _('Applications')) + elems.append('') for app, selection in apps: ids = [id for id, name in selection] app_name = name_boolean_groups(ids) @@ -705,8 +708,9 @@ class users2(osv.osv): fields[app_name] = {'type': 'boolean', 'string': app} fields[sel_name] = {'type': 'selection', 'string': '', 'selection': selection} elems.append('' % app_name) - elems.append('' % sel_name) + elems.append("""""" % (sel_name, app_name)) elems.append('') + elems.append('') # create other sections sections = sorted(others.items(), key=lambda pair: pair[0]) if sections and sections[0][0] is None: From 75a63f5eb43213b356e6a2608c2988dfda953a7a Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 10:41:37 +0200 Subject: [PATCH 10/27] improve group selection fields bzr revid: rco@openerp.com-20110729084137-da89gorw9udmv8i0 --- openerp/addons/base/res/res_user.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index f8c5c034f61..0777070c99c 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -562,6 +562,15 @@ class groups2(osv.osv): for s in ss: preds[s] |= ps return succs + def get_maximal(self, cr, ids): + "return a maximal element among the group ids" + trans_implied = self.get_rec_implied(cr) + res = None + for gid in ids: + if (not res) or (res in trans_implied[gid]): + res = gid + return res + def get_classified(self, cr, uid, context=None): """ classify all groups by prefix; return a pair (apps, groups) where @@ -571,7 +580,7 @@ class groups2(osv.osv): """ # get the relation to order groups order_relation = self.get_rec_implied(cr) - order = lambda x, y: (x[0] in order_relation[y[0]] and 1 or -1) + order = lambda x, y: (x[0] in order_relation[y[0]] and -1 or 1) # classify groups depending on their names classified = {} @@ -666,6 +675,7 @@ class users2(osv.osv): else: other_fields.append(f) if group_fields: + group_obj = self.pool.get('res.groups') fields = other_fields + ['groups_id'] res = super(users2, self).read(cr, uid, ids, fields, context, load) for record in res: @@ -677,11 +687,8 @@ class users2(osv.osv): elif is_boolean_groups(f): record[f] = not groups.isdisjoint(get_boolean_groups(f)) elif is_selection_groups(f): - record[f] = False - for gid in get_selection_groups(f): - if gid in groups: - record[f] = gid - break + selected = groups.intersection(get_selection_groups(f)) + record[f] = group_obj.get_maximal(cr, selected) return res return super(users2, self).read(cr, uid, ids, fields, context, load) @@ -708,7 +715,7 @@ class users2(osv.osv): fields[app_name] = {'type': 'boolean', 'string': app} fields[sel_name] = {'type': 'selection', 'string': '', 'selection': selection} elems.append('' % app_name) - elems.append("""""" % (sel_name, app_name)) + elems.append("""""" % (sel_name, app_name)) elems.append('') elems.append('') # create other sections From 4fec9595c8f919459e51af9733d95366a77f87e8 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 11:46:43 +0200 Subject: [PATCH 11/27] add function partition bzr revid: rco@openerp.com-20110729094643-j0ky9qu3o3w2n1wr --- openerp/addons/base/res/res_user.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 0777070c99c..c696b6ec41a 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -530,6 +530,10 @@ users() class groups2(osv.osv): + """ Extension of res.groups with a relation for implied groups. + The data defines a view that inherits from the user form; that view is + updated when groups are modified, to reflect their structure. + """ _inherit = 'res.groups' _columns = { 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid', @@ -572,8 +576,7 @@ class groups2(osv.osv): return res def get_classified(self, cr, uid, context=None): - """ - classify all groups by prefix; return a pair (apps, groups) where + """ classify all groups by prefix; return a pair (apps, groups) where - apps is a list like [("App", [(id, "Name"), ...]), ...], - groups is a dictionary like {'Class': [(id, "Name"), ...], ...} - the key None is used in groups for groups not like App/Name @@ -627,6 +630,8 @@ 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('_')) @@ -668,12 +673,7 @@ class users2(osv.osv): def read(self, cr, uid, ids, fields, context=None, load='_classic_read'): # add processing for reified group fields - group_fields, other_fields = [], [] - for f in fields: - if is_boolean_group(f) or is_boolean_groups(f) or is_selection_groups(f): - group_fields.append(f) - else: - other_fields.append(f) + group_fields, other_fields = partition(is_field_group, fields) if group_fields: group_obj = self.pool.get('res.groups') fields = other_fields + ['groups_id'] @@ -761,4 +761,16 @@ class res_config_view(osv.osv_memory): res_config_view() + + +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 + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From a7c5e26646344e73e658ba62a67e15593061cd2b Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 12:07:14 +0200 Subject: [PATCH 12/27] small code improvements bzr revid: rco@openerp.com-20110729100714-apwfhm3ee72fkmg9 --- openerp/addons/base/res/res_user.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index c696b6ec41a..0d0023c9694 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -642,7 +642,7 @@ def encode(s): return s.encode('utf8') if isinstance(s, unicode) else s class users2(osv.osv): _inherit = 'res.users' - def values_to_groups_id(self, cr, uid, values, context=None): + def _values_to_groups_id(self, cr, uid, values, context=None): """ transform all reified group fields into a 'groups_id', adding also the implied groups """ add, rem = [], [] @@ -663,20 +663,20 @@ class users2(osv.osv): def create(self, cr, uid, values, context=None): # add processing for reified group fields - self.values_to_groups_id(cr, uid, values, context) + self._values_to_groups_id(cr, uid, values, context) return super(users2, self).create(cr, uid, values, context) def write(self, cr, uid, ids, values, context=None): # add processing for reified group fields - self.values_to_groups_id(cr, uid, values, context) + self._values_to_groups_id(cr, uid, values, context) return super(users2, self).write(cr, uid, ids, values, context) def read(self, cr, uid, ids, fields, context=None, load='_classic_read'): # add processing for reified group fields - group_fields, other_fields = partition(is_field_group, fields) + group_fields, fields = partition(is_field_group, fields) if group_fields: group_obj = self.pool.get('res.groups') - fields = other_fields + ['groups_id'] + fields.append('groups_id') res = super(users2, self).read(cr, uid, ids, fields, context, load) for record in res: # remove the field 'groups_id' and insert the group_fields From 3edcb0e8f3f8d5e71169104add4d76adaaa08fd4 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 12:12:38 +0200 Subject: [PATCH 13/27] small code improvements bzr revid: rco@openerp.com-20110729101238-udkcecldiy9ybfwq --- openerp/addons/base/res/res_user.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 0d0023c9694..9f8cb7d889c 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -714,9 +714,13 @@ class users2(osv.osv): sel_name = name_selection_groups(ids) fields[app_name] = {'type': 'boolean', 'string': app} fields[sel_name] = {'type': 'selection', 'string': '', 'selection': selection} - elems.append('' % app_name) - elems.append("""""" % (sel_name, app_name)) - elems.append('') + elems.append(""" + + + + """ % {'app': app_name, 'sel': sel_name}) elems.append('') # create other sections sections = sorted(others.items(), key=lambda pair: pair[0]) From 7fb84245e972d98540044e88f9a77482f7645f64 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 12:16:20 +0200 Subject: [PATCH 14/27] add comments and small code improvements bzr revid: rco@openerp.com-20110729101620-iknnq7vcchy9mlrr --- openerp/addons/base/res/res_user.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 9f8cb7d889c..8eb94ccfc0e 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -638,8 +638,21 @@ 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 users2(osv.osv): + """ Extension of res.users for: + - adding implied groups when added to a group + - defining special widgets for field groups_id + """ _inherit = 'res.users' def _values_to_groups_id(self, cr, uid, values, context=None): @@ -713,7 +726,7 @@ class users2(osv.osv): app_name = name_boolean_groups(ids) sel_name = name_selection_groups(ids) fields[app_name] = {'type': 'boolean', 'string': app} - fields[sel_name] = {'type': 'selection', 'string': '', 'selection': selection} + fields[sel_name] = {'type': 'selection', 'string': 'Group', 'selection': selection} elems.append(""" Date: Fri, 29 Jul 2011 12:27:46 +0200 Subject: [PATCH 15/27] update group and user views bzr revid: rco@openerp.com-20110729102746-nqah2jqynqiz2lo5 --- openerp/addons/base/base_update.xml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/base_update.xml b/openerp/addons/base/base_update.xml index 1820c8df9f3..86b67b56060 100644 --- a/openerp/addons/base/base_update.xml +++ b/openerp/addons/base/base_update.xml @@ -24,6 +24,10 @@ + + @@ -141,10 +145,9 @@ - - - - + + + From 8084d00780e67b7327121f50da6e74c29266cb39 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 13:03:32 +0200 Subject: [PATCH 16/27] improve API of some methods bzr revid: rco@openerp.com-20110729110332-zv5agzip0sq2rwkr --- openerp/addons/base/res/res_user.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 8eb94ccfc0e..17c511c6594 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -540,7 +540,7 @@ class groups2(osv.osv): string='Inherits', help='Users of this group automatically inherit those groups'), } - def get_closure(self, cr, ids): + 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) @@ -551,7 +551,7 @@ class groups2(osv.osv): todo.extend(g.implied_ids) return list(closure) - def get_rec_implied(self, cr): + def get_rec_implied(self, cr, uid, context=None): "return a dictionary giving the recursively implied groups of each group" groups = self.browse(cr, 1, self.search(cr, 1, [])) # compute the transitive closure of implied_ids @@ -566,9 +566,9 @@ class groups2(osv.osv): for s in ss: preds[s] |= ps return succs - def get_maximal(self, cr, ids): + def get_maximal(self, cr, uid, ids, context=None): "return a maximal element among the group ids" - trans_implied = self.get_rec_implied(cr) + trans_implied = self.get_rec_implied(cr, uid, context) res = None for gid in ids: if (not res) or (res in trans_implied[gid]): @@ -582,7 +582,7 @@ class groups2(osv.osv): - the key None is used in groups for groups not like App/Name """ # get the relation to order groups - order_relation = self.get_rec_implied(cr) + order_relation = self.get_rec_implied(cr, uid, context) order = lambda x, y: (x[0] in order_relation[y[0]] and -1 or 1) # classify groups depending on their names @@ -655,7 +655,7 @@ class users2(osv.osv): """ _inherit = 'res.users' - def _values_to_groups_id(self, cr, uid, values, context=None): + 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 = [], [] @@ -670,18 +670,18 @@ class users2(osv.osv): add.append(int(values.pop(k))) if add or rem: # remove groups in 'rem' and add all implied groups in 'add' - add = self.pool.get('res.groups').get_closure(cr, add) + add = self.pool.get('res.groups').get_closure(cr, uid, add, context) values['groups_id'] = [(3, id) for id in rem] + [(4, id) for id in add] return True def create(self, cr, uid, values, context=None): # add processing for reified group fields - self._values_to_groups_id(cr, uid, values, context) + self._process_values_groups(cr, uid, values, context) return super(users2, self).create(cr, uid, values, context) def write(self, cr, uid, ids, values, context=None): # add processing for reified group fields - self._values_to_groups_id(cr, uid, values, context) + self._process_values_groups(cr, uid, values, context) return super(users2, self).write(cr, uid, ids, values, context) def read(self, cr, uid, ids, fields, context=None, load='_classic_read'): @@ -701,7 +701,7 @@ class users2(osv.osv): 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, selected) + record[f] = group_obj.get_maximal(cr, uid, selected, context) return res return super(users2, self).read(cr, uid, ids, fields, context, load) From 2633848b3189f6b1d5fb2a9ae5da336755707b49 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 14:42:35 +0200 Subject: [PATCH 17/27] add implied groups in all cases bzr revid: rco@openerp.com-20110729124235-95a3igb3cmunwiz1 --- openerp/addons/base/res/res_user.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 17c511c6594..48b06b172c7 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -551,6 +551,26 @@ class groups2(osv.osv): todo.extend(g.implied_ids) return list(closure) + def create(self, cr, uid, values, context=None): + group_id = super(groups2, self).create(cr, uid, values, context) + if values.get('users') or values.get('implied_ids'): + # add implied groups to all users of the group + group = self.browse(cr, uid, group_id) + self.pool.get('res.users').write(cr, uid, map(int, group.users), + {'groups_id': [(4, group_id)]}, context) + return group_id + + def write(self, cr, uid, ids, values, context=None): + res = super(groups2, 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) + users_obj = self.pool.get('res.users') + groups = self.browse(cr, uid, ids) + for g in groups: + users_obj.write(cr, uid, map(int, g.users), + {'groups_id': [(4, g.id)]}, context) + return res + def get_rec_implied(self, cr, uid, context=None): "return a dictionary giving the recursively implied groups of each group" groups = self.browse(cr, 1, self.search(cr, 1, [])) @@ -672,6 +692,18 @@ class users2(osv.osv): # remove groups in 'rem' and add all implied groups in 'add' add = self.pool.get('res.groups').get_closure(cr, uid, add, context) values['groups_id'] = [(3, id) for id in rem] + [(4, id) for id in add] + elif 'groups_id' in values: + # add implied groups (only handles (4, ID) and (6, 0, IDs) cases) + closure = lambda ids: self.pool.get('res.groups').get_closure(cr, uid, ids, context) + groups = [] + for elem in values['groups_id']: + if elem[0] == 4: + groups.extend([(4, id) for id in closure([elem[1]])]) + elif elem[0] == 6: + groups.append((6, 0, closure(elem[2]))) + else: + groups.append(elem) + values['groups_id'] = groups return True def create(self, cr, uid, values, context=None): From 97dd3f3f414267d779a5b770bcad92987307f35f Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 14:44:51 +0200 Subject: [PATCH 18/27] move code around bzr revid: rco@openerp.com-20110729124451-8c4txfklkoflc2c1 --- openerp/addons/base/res/res_user.py | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 48b06b172c7..cc7b3e47ce6 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -527,6 +527,26 @@ class users(osv.osv): users() +class res_config_view(osv.osv_memory): + _name = 'res.config.view' + _inherit = 'res.config' + _columns = { + 'name':fields.char('Name', size=64), + 'view': fields.selection([('simple','Simplified'), + ('extended','Extended')], + 'Interface', required=True ), + } + _defaults={ + 'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple', + } + + def execute(self, cr, uid, ids, context=None): + res = self.read(cr, uid, ids)[0] + self.pool.get('res.users').write(cr, uid, [uid], + {'view':res['view']}, context=context) + +res_config_view() + class groups2(osv.osv): @@ -790,24 +810,4 @@ users2() -class res_config_view(osv.osv_memory): - _name = 'res.config.view' - _inherit = 'res.config' - _columns = { - 'name':fields.char('Name', size=64), - 'view': fields.selection([('simple','Simplified'), - ('extended','Extended')], - 'Interface', required=True ), - } - _defaults={ - 'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple', - } - - def execute(self, cr, uid, ids, context=None): - res = self.read(cr, uid, ids)[0] - self.pool.get('res.users').write(cr, uid, [uid], - {'view':res['view']}, context=context) - -res_config_view() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From beaf6ec43cef0745dc7218d237f639f1d34bbb89 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 16:04:00 +0200 Subject: [PATCH 19/27] split code in two parts: the implied groups, and enhanced view bzr revid: rco@openerp.com-20110729140400-agr3x0o7xpqxyt0p --- openerp/addons/base/res/res_user.py | 142 ++++++++++++++++------------ 1 file changed, 81 insertions(+), 61 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index cc7b3e47ce6..f903f7bfc4a 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -549,11 +549,13 @@ res_config_view() -class groups2(osv.osv): - """ Extension of res.groups with a relation for implied groups. - The data defines a view that inherits from the user form; that view is - updated when groups are modified, to reflect their structure. - """ +# +# 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', @@ -572,27 +574,25 @@ class groups2(osv.osv): return list(closure) def create(self, cr, uid, values, context=None): - group_id = super(groups2, self).create(cr, uid, values, context) - if values.get('users') or values.get('implied_ids'): - # add implied groups to all users of the group - group = self.browse(cr, uid, group_id) - self.pool.get('res.users').write(cr, uid, map(int, group.users), - {'groups_id': [(4, group_id)]}, context) - return group_id + users = values.pop('users') + 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(groups2, self).write(cr, uid, ids, values, context) + 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) - users_obj = self.pool.get('res.users') - groups = self.browse(cr, uid, ids) - for g in groups: - users_obj.write(cr, uid, map(int, g.users), - {'groups_id': [(4, g.id)]}, context) + 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_rec_implied(self, cr, uid, context=None): - "return a dictionary giving the recursively implied groups of each group" + def get_trans_implied(self, cr, uid, context=None): + "return a dictionary giving the transitively implied groups of each group" groups = self.browse(cr, 1, self.search(cr, 1, [])) # compute the transitive closure of implied_ids succs = dict([(g.id, set()) for g in groups]) @@ -608,21 +608,63 @@ class groups2(osv.osv): def get_maximal(self, cr, uid, ids, context=None): "return a maximal element among the group ids" - trans_implied = self.get_rec_implied(cr, uid, context) res = None for gid in ids: - if (not res) or (res in trans_implied[gid]): + if (not res) or (res in self.get_closure(cr, uid, [gid], context)): res = gid return res +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): + gids = groups_obj.get_closure(cr, uid, map(int, u.groups_id), context) + groups = [(6, 0, gids)] + super(users_implied, self).write(cr, uid, [u.id], {'groups_id': groups}, 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, groups) where + """ classify all groups by prefix; return a pair (apps, others) where - apps is a list like [("App", [(id, "Name"), ...]), ...], - - groups is a dictionary like {'Class': [(id, "Name"), ...], ...} + - others is a dictionary like {'Class': [(id, "Name"), ...], ...} - the key None is used in groups for groups not like App/Name """ # get the relation to order groups - order_relation = self.get_rec_implied(cr, uid, context) + order_relation = self.get_trans_implied(cr, uid, context) order = lambda x, y: (x[0] in order_relation[y[0]] and -1 or 1) # classify groups depending on their names @@ -650,10 +692,8 @@ class groups2(osv.osv): groups.sort(key=lambda pair: pair[1]) return (apps, classified) - -groups2() - +groups_view() # Naming conventions for reified groups fields: # - boolean field 'in_group_ID' is True iff @@ -688,11 +728,7 @@ def partition(f, xs): nos.append(x) return yes, nos -class users2(osv.osv): - """ Extension of res.users for: - - adding implied groups when added to a group - - defining special widgets for field groups_id - """ +class users_view(osv.osv): _inherit = 'res.users' def _process_values_groups(self, cr, uid, values, context=None): @@ -709,43 +745,29 @@ class users2(osv.osv): rem.extend(get_selection_groups(k)) add.append(int(values.pop(k))) if add or rem: - # remove groups in 'rem' and add all implied groups in 'add' - add = self.pool.get('res.groups').get_closure(cr, uid, add, context) - values['groups_id'] = [(3, id) for id in rem] + [(4, id) for id in add] - elif 'groups_id' in values: - # add implied groups (only handles (4, ID) and (6, 0, IDs) cases) - closure = lambda ids: self.pool.get('res.groups').get_closure(cr, uid, ids, context) - groups = [] - for elem in values['groups_id']: - if elem[0] == 4: - groups.extend([(4, id) for id in closure([elem[1]])]) - elif elem[0] == 6: - groups.append((6, 0, closure(elem[2]))) - else: - groups.append(elem) - values['groups_id'] = groups + # 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): - # add processing for reified group fields self._process_values_groups(cr, uid, values, context) - return super(users2, self).create(cr, uid, values, context) + return super(users_view, self).create(cr, uid, values, context) def write(self, cr, uid, ids, values, context=None): - # add processing for reified group fields self._process_values_groups(cr, uid, values, context) - return super(users2, self).write(cr, uid, ids, 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'): - # add processing for reified group fields group_fields, fields = partition(is_field_group, fields) if group_fields: group_obj = self.pool.get('res.groups') fields.append('groups_id') - res = super(users2, self).read(cr, uid, ids, fields, context, load) + # read the normal fields (and 'groups_id') + res = super(users_view, self).read(cr, uid, ids, fields, context, load) for record in res: - # remove the field 'groups_id' and insert the group_fields - groups = set(record.pop('groups_id')) + # 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 @@ -755,12 +777,12 @@ class users2(osv.osv): selected = groups.intersection(get_selection_groups(f)) record[f] = group_obj.get_maximal(cr, uid, selected, context) return res - return super(users2, self).read(cr, uid, ids, fields, context, load) + 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(users2, self).fields_view_get(cr, uid, view_id, view_type, + 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'])) @@ -806,8 +828,6 @@ class users2(osv.osv): res['arch'] = etree.tostring(root) return res -users2() - - +users_view() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From dca1812f7f3022b31bdaaa62e33c7cef3a469240 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 17:12:16 +0200 Subject: [PATCH 20/27] simple optimization when modifying groups of users bzr revid: rco@openerp.com-20110729151216-7spi02jeibzl7jhz --- openerp/addons/base/res/res_user.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index f903f7bfc4a..0247525616e 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -633,9 +633,11 @@ class users_implied(osv.osv): # add implied groups for all users groups_obj = self.pool.get('res.groups') for u in self.browse(cr, uid, ids): - gids = groups_obj.get_closure(cr, uid, map(int, u.groups_id), context) - groups = [(6, 0, gids)] - super(users_implied, self).write(cr, uid, [u.id], {'groups_id': groups}, context) + 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() From 90d202a4c9d6fa4ea9acde9c4587dccd901b6b88 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 29 Jul 2011 17:15:24 +0200 Subject: [PATCH 21/27] fix methods read and write in users_view bzr revid: rco@openerp.com-20110729151524-3szt56d6icc7j8pn --- openerp/addons/base/res/res_user.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 0247525616e..a18dddbbcb4 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -739,13 +739,18 @@ class users_view(osv.osv): add, rem = [], [] for k in values.keys(): if is_boolean_group(k): - (add if values.pop(k) else rem).append(get_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): - rem.extend(get_selection_groups(k)) - add.append(int(values.pop(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] @@ -767,7 +772,8 @@ class users_view(osv.osv): fields.append('groups_id') # read the normal fields (and 'groups_id') res = super(users_view, self).read(cr, uid, ids, fields, context, load) - for record in res: + 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: From 31131ed5cbecf749cee16d9e9e04a34e22636f5d Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Mon, 1 Aug 2011 16:07:03 +0200 Subject: [PATCH 22/27] [IMP] classification of groups bzr revid: rco@openerp.com-20110801140703-5odcxym9xnudffym --- openerp/addons/base/res/res_user.py | 56 ++++++++++++++--------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index a18dddbbcb4..1f1ef8f8698 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -661,22 +661,22 @@ class groups_view(osv.osv): def get_classified(self, cr, uid, context=None): """ classify all groups by prefix; return a pair (apps, others) where - - apps is a list like [("App", [(id, "Name"), ...]), ...], - - others is a dictionary like {'Class': [(id, "Name"), ...], ...} - - the key None is used in groups for groups not like App/Name + - 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') """ - # get the relation to order groups - order_relation = self.get_trans_implied(cr, uid, context) - order = lambda x, y: (x[0] in order_relation[y[0]] and -1 or 1) + # sort groups by implication, with implied groups first + trans_implied = self.get_trans_implied(cr, uid, context) + groups = self.browse(cr, uid, self.search(cr, uid, []), context) + groups.sort(key=lambda g: set([g.id]) | trans_implied[g.id]) # classify groups depending on their names classified = {} - for g in self.browse(cr, uid, self.search(cr, uid, []), context): - names = [s.strip() for s in g.name.split('/', 1)] - if len(names) > 1: - classified.setdefault(names[0], []).append((g.id, names[1])) - else: - classified.setdefault(None, []).append((g.id, names[0])) + 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') @@ -684,16 +684,17 @@ class groups_view(osv.osv): apps = [] for m in menu_obj.browse(cr, uid, menu_ids, context): if m.name in classified: - # sort application groups by implication - groups = classified.pop(m.name) - groups.sort(order) - apps.append((m.name, groups)) + # application groups are already sorted by implication + apps.append((m.name, classified.pop(m.name))) - # sort remaining groups alphabetically in their class - for groups in classified.itervalues(): - groups.sort(key=lambda pair: pair[1]) + # 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, classified) + return (apps, others) groups_view() @@ -803,10 +804,11 @@ class users_view(osv.osv): # create section Applications elems.append('' % _('Applications')) elems.append('') - for app, selection in apps: - ids = [id for id, name in selection] + 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} fields[sel_name] = {'type': 'selection', 'string': 'Group', 'selection': selection} elems.append(""" @@ -818,14 +820,10 @@ class users_view(osv.osv): """ % {'app': app_name, 'sel': sel_name}) elems.append('') # create other sections - sections = sorted(others.items(), key=lambda pair: pair[0]) - if sections and sections[0][0] is None: - sec, selection = sections.pop(0) - sections.append((_('Others'), selection)) - for sec, selection in sections: + for sec, groups in others: elems.append('' % sec) - for id, gname in selection: - name = name_boolean_group(id) + for gname, g in groups: + name = name_boolean_group(g.id) fields[name] = {'type': 'boolean', 'string': gname} elems.append('' % name) elems.append('') From 50a4801ff09366607149d7930d0e8b815433d788 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Mon, 1 Aug 2011 16:17:22 +0200 Subject: [PATCH 23/27] [IMP] add tooltip for groups bzr revid: rco@openerp.com-20110801141722-50lbhy56zt6ppsng --- openerp/addons/base/res/res_user.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 1f1ef8f8698..0e776c0a392 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -810,6 +810,9 @@ class users_view(osv.osv): 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(""" @@ -825,6 +828,8 @@ class users_view(osv.osv): 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('' % name) elems.append('') # replace xml node by new arch From 8faed1a08a05a6f84b376c57331d645e823c8e1e Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Mon, 1 Aug 2011 16:26:07 +0200 Subject: [PATCH 24/27] slight improvement of layout bzr revid: rco@openerp.com-20110801142607-ie3lqx5j4u77wbbn --- openerp/addons/base/res/res_user.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 0e776c0a392..07d8783a138 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -803,7 +803,6 @@ class users_view(osv.osv): apps, others = self.pool.get('res.groups').get_classified(cr, uid, context) # create section Applications elems.append('' % _('Applications')) - elems.append('') for app, groups in apps: ids = [g.id for name, g in groups] app_name = name_boolean_groups(ids) @@ -816,12 +815,11 @@ class users_view(osv.osv): fields[sel_name] = {'type': 'selection', 'string': 'Group', 'selection': selection} elems.append(""" - """ % {'app': app_name, 'sel': sel_name}) - elems.append('') # create other sections for sec, groups in others: elems.append('' % sec) From a3a96878b91c68e5d7c2ba3ebf154571c1db8a46 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Mon, 1 Aug 2011 16:51:43 +0200 Subject: [PATCH 25/27] [FIX] some incomplete behaviors bzr revid: rco@openerp.com-20110801145143-sbxda31bazobej6k --- openerp/addons/base/res/res_user.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index 07d8783a138..e3a996412be 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -574,7 +574,7 @@ class groups_implied(osv.osv): return list(closure) def create(self, cr, uid, values, context=None): - users = values.pop('users') + 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 @@ -767,7 +767,10 @@ class users_view(osv.osv): return super(users_view, self).write(cr, uid, ids, values, context) def read(self, cr, uid, ids, fields, context=None, load='_classic_read'): - group_fields, fields = partition(is_field_group, fields) + 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') From 4d79d7efc32269b42cc9ffc4eaa532d00d762c7f Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 2 Aug 2011 14:12:53 +0200 Subject: [PATCH 26/27] [IMP] get maximal group in set bzr revid: rco@openerp.com-20110802121253-hpb7s3pi8eiofz2u --- openerp/addons/base/res/res_user.py | 35 +++++++++++------------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/openerp/addons/base/res/res_user.py b/openerp/addons/base/res/res_user.py index e3a996412be..6e6b8f03e3f 100644 --- a/openerp/addons/base/res/res_user.py +++ b/openerp/addons/base/res/res_user.py @@ -29,6 +29,7 @@ import pooler from tools.translate import _ from service import security import netsvc +import logging from lxml import etree class groups(osv.osv): @@ -591,28 +592,19 @@ class groups_implied(osv.osv): super(groups_implied, self).write(cr, uid, gids, {'users': users}, context) return res - def get_trans_implied(self, cr, uid, context=None): - "return a dictionary giving the transitively implied groups of each group" - groups = self.browse(cr, 1, self.search(cr, 1, [])) - # compute the transitive closure of implied_ids - succs = dict([(g.id, set()) for g in groups]) - preds = dict([(g.id, set()) for g in groups]) - for g in groups: - for h in g.implied_ids: - # link g and its predecessors to h and its successors - ps = preds[g.id].union([g.id]) - ss = succs[h.id].union([h.id]) - for p in ps: succs[p] |= ss - for s in ss: preds[s] |= ps - return succs - def get_maximal(self, cr, uid, ids, context=None): - "return a maximal element among the group ids" - res = None + "return the maximal element among the group ids" + max_set, max_closure = set(), set() for gid in ids: - if (not res) or (res in self.get_closure(cr, uid, [gid], context)): - res = gid - return res + 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() @@ -667,9 +659,8 @@ class groups_view(osv.osv): - groups not like App/Name are at the end of others, under _('Others') """ # sort groups by implication, with implied groups first - trans_implied = self.get_trans_implied(cr, uid, context) groups = self.browse(cr, uid, self.search(cr, uid, []), context) - groups.sort(key=lambda g: set([g.id]) | trans_implied[g.id]) + groups.sort(key=lambda g: set(self.get_closure(cr, uid, [g.id], context))) # classify groups depending on their names classified = {} From a2be1bdbd7541a24cc1f241223fe78b168bd55be Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 2 Aug 2011 15:08:34 +0200 Subject: [PATCH 27/27] add group implications bzr revid: rco@openerp.com-20110802130834-0m7d7tt1ubls7xhn --- openerp/addons/base/security/base_security.xml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/security/base_security.xml b/openerp/addons/base/security/base_security.xml index 114c5d2bd8b..7389b45c814 100644 --- a/openerp/addons/base/security/base_security.xml +++ b/openerp/addons/base/security/base_security.xml @@ -5,13 +5,13 @@ - - Administration / Configuration - - Administration / Access Rights + + Administration / Configuration + + Employee @@ -29,12 +29,13 @@ Useability / No One - - Sales / Manager - Sales / User + + Sales / Manager + +