Raphael Collet (OpenERP) has proposed merging 
lp:~openerp-dev/openobject-server/trunk-usability-groups-rco into 
lp:~openerp-dev/openobject-server/trunk-usability-users.

Requested reviews:
  OpenERP R&D Team (openerp-dev)

For more details, see:
https://code.launchpad.net/~openerp-dev/openobject-server/trunk-usability-groups-rco/+merge/70024

New appearance of group access on the users form.

-- 
https://code.launchpad.net/~openerp-dev/openobject-server/trunk-usability-groups-rco/+merge/70024
Your team OpenERP R&D Team is requested to review the proposed merge of 
lp:~openerp-dev/openobject-server/trunk-usability-groups-rco into 
lp:~openerp-dev/openobject-server/trunk-usability-users.
=== modified file 'openerp/addons/base/base_update.xml'
--- openerp/addons/base/base_update.xml	2011-07-06 12:55:41 +0000
+++ openerp/addons/base/base_update.xml	2011-08-01 15:02:21 +0000
@@ -24,6 +24,10 @@
                         <page string="Users">
                             <field colspan="4" name="users" nolabel="1"/>
                         </page>
+                        <page string="Inherited">
+                            <label colspan="4" string="Users belonging to this group will be automatically included in the following groups."/>
+                            <field colspan="4" name="implied_ids" nolabel="1"/>
+                        </page>
                         <page string="Menus">
                             <field colspan="4" name="menu_access" nolabel="1"/>
                         </page>
@@ -141,10 +145,9 @@
                                 <separator string="Signature" colspan="2"/>
                                 <field colspan="2" name="signature" nolabel="1"/>
                             </group>
-                            <group colspan="2" col="2" expand="1">
-                                <separator string="Groups" colspan="2"/>
-                                <field colspan="2" nolabel="1" name="groups_id"/>
-                            </group>
+                        </page>
+                        <page string="Access Rights">
+                            <field nolabel="1" name="groups_id" select="1"/>
                         </page>
                         <page string="Companies" groups="base.group_multi_company">
                             <field colspan="4" nolabel="1" name="company_ids" select="1"/>

=== modified file 'openerp/addons/base/res/res_user.py'
--- openerp/addons/base/res/res_user.py	2011-07-06 15:40:01 +0000
+++ openerp/addons/base/res/res_user.py	2011-08-01 15:02:21 +0000
@@ -29,13 +29,15 @@
 from tools.translate import _
 from service import security
 import netsvc
+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 +78,21 @@
                 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 +527,6 @@
 
 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 +547,299 @@
 
 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_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
+        for gid in ids:
+            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):
+                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
+        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 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:

_______________________________________________
Mailing list: https://launchpad.net/~openerp-dev-gtk
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~openerp-dev-gtk
More help   : https://help.launchpad.net/ListHelp

Reply via email to