1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 # Copyright (C) 2010-2012 OpenERP s.a. (<http://openerp.com>).
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
24 from functools import partial
26 from lxml import etree
27 from lxml.builder import E
29 from openerp import SUPERUSER_ID
31 import openerp.exceptions
32 from osv import fields,osv
33 from osv.orm import browse_record
36 from service import security
38 from tools.translate import _
40 _logger = logging.getLogger(__name__)
42 class groups(osv.osv):
44 _description = "Access Groups"
45 _rec_name = 'full_name'
47 def _get_full_name(self, cr, uid, ids, field, arg, context=None):
49 for g in self.browse(cr, uid, ids, context):
51 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
57 'name': fields.char('Name', size=64, required=True, translate=True),
58 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
59 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
60 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
61 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
62 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
63 'comment' : fields.text('Comment', size=250, translate=True),
64 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
65 'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
69 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
72 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
73 # add explicit ordering if search is sorted on full_name
74 if order and order.startswith('full_name'):
75 ids = super(groups, self).search(cr, uid, args, context=context)
76 gs = self.browse(cr, uid, ids, context)
77 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
78 gs = gs[offset:offset+limit] if limit else gs[offset:]
80 return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
82 def copy(self, cr, uid, id, default=None, context=None):
83 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
84 default.update({'name': _('%s (copy)')%group_name})
85 return super(groups, self).copy(cr, uid, id, default, context)
87 def write(self, cr, uid, ids, vals, context=None):
89 if vals['name'].startswith('-'):
90 raise osv.except_osv(_('Error'),
91 _('The name of the group can not start with "-"'))
92 res = super(groups, self).write(cr, uid, ids, vals, context=context)
93 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
98 class res_users(osv.osv):
99 """ User class. A res.users record models an OpenERP user and is different
102 res.users class now inherits from res.partner. The partner model is
103 used to store the data related to the partner: lang, name, address,
104 avatar, ... The user model is now dedicated to technical data.
109 'res.partner': 'partner_id',
112 _description = 'Users'
115 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
117 # Do not update the password if no value is provided, ignore silently.
118 # For example web client submits False values for all empty fields.
121 # To change their own password users must use the client-specific change password wizard,
122 # so that the new password is immediately used for further RPC requests, otherwise the user
123 # will face unexpected 'Access Denied' exceptions.
124 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
125 self.write(cr, uid, id, {'password': value})
127 def _get_password(self, cr, uid, ids, arg, karg, context=None):
128 return dict.fromkeys(ids, '')
131 'id': fields.integer('ID'),
132 'login_date': fields.date('Latest connection', select=1),
133 'partner_id': fields.many2one('res.partner', required=True,
134 string='Related Partner', ondelete='cascade',
135 help='Partner-related data of the user'),
136 'login': fields.char('Login', size=64, required=True,
137 help="Used to log into the system"),
138 'password': fields.char('Password', size=64, invisible=True,
139 help="Keep empty if you don't want the user to be able to connect on the system."),
140 'new_password': fields.function(_get_password, type='char', size=64,
141 fnct_inv=_set_new_password, string='Set Password',
142 help="Specify a value only when creating a user or if you're "\
143 "changing the user's password, otherwise leave empty. After "\
144 "a change of password, the user has to login again."),
145 'signature': fields.text('Signature', size=64),
146 'active': fields.boolean('Active'),
147 'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at logon for this user, in addition to the standard menu."),
148 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
149 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
150 # Special behavior for this field: res.company.search() will only return the companies
151 # available to the current user (should be the user's companies?), when the user_preference
153 'company_id': fields.many2one('res.company', 'Company', required=True,
154 help='The company this user is currently working for.', context={'user_preference': True}),
155 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
156 # backward compatibility fields
157 'user_email': fields.related('email', type='char',
158 deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'),
161 def on_change_company_id(self, cr, uid, ids, company_id):
162 return {'warning' : {
163 'title': _("Company Switch Warning"),
164 'message': _("Please keep in mind that documents currently displayed may not be relevant after switching to another company. If you have unsaved changes, please make sure to save and close all forms before switching to a different company. (You can click on Cancel in the User Preferences now)"),
168 def onchange_type(self, cr, uid, ids, is_company, context=None):
169 """ Wrapper on the user.partner onchange_type, because some calls to the
170 partner form view applied to the user may trigger the
171 partner.onchange_type method, but applied to the user object.
173 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
174 return self.pool.get('res.partner').onchange_type(cr, uid, partner_ids, is_company, context=context)
176 def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
177 """ Wrapper on the user.partner onchange_address, because some calls to the
178 partner form view applied to the user may trigger the
179 partner.onchange_type method, but applied to the user object.
181 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
182 return self.pool.get('res.partner').onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
184 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
185 def override_password(o):
186 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
187 o['password'] = '********'
189 result = super(res_users, self).read(cr, uid, ids, fields, context, load)
190 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
192 if isinstance(ids, (int, long)):
193 result = override_password(result)
195 result = map(override_password, result)
199 def _check_company(self, cr, uid, ids, context=None):
200 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
203 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
207 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
210 def _get_company(self,cr, uid, context=None, uid2=False):
213 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
214 company_id = user.get('company_id', False)
215 return company_id and company_id[0] or False
217 def _get_companies(self, cr, uid, context=None):
218 c = self._get_company(cr, uid, context)
223 def _get_menu(self,cr, uid, context=None):
224 dataobj = self.pool.get('ir.model.data')
226 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
227 if model != 'ir.actions.act_window':
233 def _get_group(self,cr, uid, context=None):
234 dataobj = self.pool.get('ir.model.data')
237 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_user')
238 result.append(group_id)
239 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_partner_manager')
240 result.append(group_id)
242 # If these groups does not exists anymore
250 'menu_id': _get_menu,
251 'company_id': _get_company,
252 'company_ids': _get_companies,
253 'groups_id': _get_group,
254 'image': lambda self, cr, uid, context: self.pool.get('res.partner')._get_default_image(cr, uid, False, context, colorize=True),
257 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
258 """ Override of res.users fields_view_get.
259 - if the view is specified: resume with normal behavior
260 - else: the default view is overrided and redirected to the partner
263 if not view_id and view_type == 'form':
264 return self.pool.get('res.partner').fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
265 return super(res_users, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
267 # User can write to a few of her own fields (but not her groups for example)
268 SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small']
270 def write(self, cr, uid, ids, values, context=None):
271 if not hasattr(ids, '__iter__'):
274 for key in values.keys():
275 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
278 if 'company_id' in values:
279 if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):
280 del values['company_id']
281 uid = 1 # safe fields only, so we write as super-user to bypass access rights
283 res = super(res_users, self).write(cr, uid, ids, values, context=context)
285 # clear caches linked to the users
286 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
287 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
290 if db in self._uid_cache:
292 if id in self._uid_cache[db]:
293 del self._uid_cache[db][id]
297 def unlink(self, cr, uid, ids, context=None):
299 raise osv.except_osv(_('Can not remove root user!'), _('You can not remove the admin user as it is used internally for resources created by OpenERP (updates, module installation, ...)'))
301 if db in self._uid_cache:
303 if id in self._uid_cache[db]:
304 del self._uid_cache[db][id]
305 return super(res_users, self).unlink(cr, uid, ids, context=context)
307 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
314 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
316 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
317 return self.name_get(cr, user, ids)
319 def copy(self, cr, uid, id, default=None, context=None):
320 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
323 copy_pattern = _("%s (copy)")
324 copydef = dict(login=(copy_pattern % user2copy['login']),
325 name=(copy_pattern % user2copy['name']),
327 copydef.update(default)
328 return super(res_users, self).copy(cr, uid, id, copydef, context)
330 def context_get(self, cr, uid, context=None):
331 user = self.browse(cr, SUPERUSER_ID, uid, context)
333 for k in self._all_columns.keys():
334 if k.startswith('context_'):
336 elif k in ['lang', 'tz']:
341 res = getattr(user,k) or False
342 if isinstance(res, browse_record):
344 result[context_key] = res or False
347 def action_get(self, cr, uid, context=None):
348 dataobj = self.pool.get('ir.model.data')
349 data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
350 return dataobj.browse(cr, uid, data_id, context=context).res_id
352 def check_super(self, passwd):
353 if passwd == tools.config['admin_passwd']:
356 raise openerp.exceptions.AccessDenied()
358 def check_credentials(self, cr, uid, password):
359 """ Override this method to plug additional authentication methods"""
360 res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
362 raise openerp.exceptions.AccessDenied()
364 def login(self, db, login, password):
368 cr = pooler.get_db(db).cursor()
370 # autocommit: our single update request will be performed atomically.
371 # (In this way, there is no opportunity to have two transactions
372 # interleaving their cr.execute()..cr.commit() calls and have one
373 # of them rolled back due to a concurrent access.)
375 # check if user exists
376 res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
380 self.check_credentials(cr, user_id, password)
381 # We effectively unconditionally write the res_users line.
382 # Even w/ autocommit there's a chance the user row will be locked,
383 # in which case we can't delay the login just for the purpose of
384 # update the last login date - hence we use FOR UPDATE NOWAIT to
385 # try to get the lock - fail-fast
386 # Failing to acquire the lock on the res_users row probably means
387 # another request is holding it. No big deal, we don't want to
388 # prevent/delay login in that case. It will also have been logged
389 # as a SQL error, if anyone cares.
391 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", str(user_id))
392 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", str(user_id))
394 _logger.exception("Failed to update last_login for db:%s login:%s", db, login)
395 except openerp.exceptions.AccessDenied:
396 _logger.info("Login failed for db:%s login:%s", db, login)
403 def authenticate(self, db, login, password, user_agent_env):
404 """Verifies and returns the user ID corresponding to the given
405 ``login`` and ``password`` combination, or False if there was
408 :param str db: the database on which user is trying to authenticate
409 :param str login: username
410 :param str password: user password
411 :param dict user_agent_env: environment dictionary describing any
412 relevant environment attributes
414 uid = self.login(db, login, password)
415 if uid == openerp.SUPERUSER_ID:
416 # Successfully logged in as admin!
417 # Attempt to guess the web base url...
418 if user_agent_env and user_agent_env.get('base_location'):
419 cr = pooler.get_db(db).cursor()
421 base = user_agent_env['base_location']
422 self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url', base)
425 _logger.exception("Failed to update web.base.url configuration parameter")
430 def check(self, db, uid, passwd):
431 """Verifies that the given (uid, password) is authorized for the database ``db`` and
432 raise an exception if it is not."""
434 # empty passwords disallowed for obvious security reasons
435 raise openerp.exceptions.AccessDenied()
436 if self._uid_cache.get(db, {}).get(uid) == passwd:
438 cr = pooler.get_db(db).cursor()
440 self.check_credentials(cr, uid, passwd)
441 if self._uid_cache.has_key(db):
442 self._uid_cache[db][uid] = passwd
444 self._uid_cache[db] = {uid:passwd}
448 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
449 """Change current user password. Old password must be provided explicitly
450 to prevent hijacking an existing user session, or for cases where the cleartext
451 password is not used to authenticate requests.
454 :raise: openerp.exceptions.AccessDenied when old password is wrong
455 :raise: except_osv when new password is not set or empty
457 self.check(cr.dbname, uid, old_passwd)
459 return self.write(cr, uid, uid, {'password': new_passwd})
460 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
462 def preference_save(self, cr, uid, ids, context=None):
464 'type': 'ir.actions.client',
468 def preference_change_password(self, cr, uid, ids, context=None):
470 'type': 'ir.actions.client',
471 'tag': 'change_password',
475 def has_group(self, cr, uid, group_ext_id):
476 """Checks whether user belongs to given group.
478 :param str group_ext_id: external ID (XML ID) of the group.
479 Must be provided in fully-qualified form (``module.ext_id``), as there
480 is no implicit module to use..
481 :return: True if the current user is a member of the group with the
482 given external ID (XML ID), else False.
484 assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
485 module, ext_id = group_ext_id.split('.')
486 cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
487 (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
488 (uid, module, ext_id))
489 return bool(cr.fetchone())
493 # Extension of res.groups and res.users with a relation for "implied" or
494 # "inherited" groups. Once a user belongs to a group, it automatically belongs
495 # to the implied groups (transitively).
499 """ A cset (constrained set) is a set of elements that may be constrained to
500 be a subset of other csets. Elements added to a cset are automatically
501 added to its supersets. Cycles in the subset constraints are supported.
503 def __init__(self, xs):
504 self.supersets = set()
505 self.elements = set(xs)
506 def subsetof(self, other):
507 if other is not self:
508 self.supersets.add(other)
509 other.update(self.elements)
510 def update(self, xs):
511 xs = set(xs) - self.elements
512 if xs: # xs will eventually be empty in case of a cycle
513 self.elements.update(xs)
514 for s in self.supersets:
517 return iter(self.elements)
520 """ return the concatenation of a list of iterables """
522 for l in ls: res.extend(l)
527 class groups_implied(osv.osv):
528 _inherit = 'res.groups'
530 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
531 "computes the transitive closure of relation implied_ids"
532 memo = {} # use a memo for performance and cycle avoidance
535 memo[g] = cset(g.implied_ids)
536 for h in g.implied_ids:
537 computed_set(h).subsetof(memo[g])
541 for g in self.browse(cr, SUPERUSER_ID, ids, context):
542 res[g.id] = map(int, computed_set(g))
546 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
547 string='Inherits', help='Users of this group automatically inherit those groups'),
548 'trans_implied_ids': fields.function(_get_trans_implied,
549 type='many2many', relation='res.groups', string='Transitively inherits'),
552 def create(self, cr, uid, values, context=None):
553 users = values.pop('users', None)
554 gid = super(groups_implied, self).create(cr, uid, values, context)
556 # delegate addition of users to add implied groups
557 self.write(cr, uid, [gid], {'users': users}, context)
560 def write(self, cr, uid, ids, values, context=None):
561 res = super(groups_implied, self).write(cr, uid, ids, values, context)
562 if values.get('users') or values.get('implied_ids'):
563 # add all implied groups (to all users of each group)
564 for g in self.browse(cr, uid, ids):
565 gids = map(int, g.trans_implied_ids)
566 vals = {'users': [(4, u.id) for u in g.users]}
567 super(groups_implied, self).write(cr, uid, gids, vals, context)
570 class users_implied(osv.osv):
571 _inherit = 'res.users'
573 def create(self, cr, uid, values, context=None):
574 groups = values.pop('groups_id', None)
575 user_id = super(users_implied, self).create(cr, uid, values, context)
577 # delegate addition of groups to add implied groups
578 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
581 def write(self, cr, uid, ids, values, context=None):
582 if not isinstance(ids,list):
584 res = super(users_implied, self).write(cr, uid, ids, values, context)
585 if values.get('groups_id'):
586 # add implied groups for all users
587 for user in self.browse(cr, uid, ids):
588 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
589 vals = {'groups_id': [(4, g.id) for g in gs]}
590 super(users_implied, self).write(cr, uid, [user.id], vals, context)
594 # Extension of res.groups and res.users for the special groups view in the users
595 # form. This extension presents groups with selection and boolean widgets:
596 # - Groups are shown by application, with boolean and/or selection fields.
597 # Selection fields typically defines a role "Name" for the given application.
598 # - Uncategorized groups are presented as boolean fields and grouped in a
601 # The user form view is modified by an inherited view (base.user_groups_view);
602 # the inherited view replaces the field 'groups_id' by a set of reified group
603 # fields (boolean or selection fields). The arch of that view is regenerated
604 # each time groups are changed.
606 # Naming conventions for reified groups fields:
607 # - boolean field 'in_group_ID' is True iff
608 # ID is in 'groups_id'
609 # - boolean field 'in_groups_ID1_..._IDk' is True iff
610 # any of ID1, ..., IDk is in 'groups_id'
611 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
612 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
614 def name_boolean_group(id): return 'in_group_' + str(id)
615 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
616 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
618 def is_boolean_group(name): return name.startswith('in_group_')
619 def is_boolean_groups(name): return name.startswith('in_groups_')
620 def is_selection_groups(name): return name.startswith('sel_groups_')
621 def is_reified_group(name):
622 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
624 def get_boolean_group(name): return int(name[9:])
625 def get_boolean_groups(name): return map(int, name[10:].split('_'))
626 def get_selection_groups(name): return map(int, name[11:].split('_'))
628 def partition(f, xs):
629 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
632 (yes if f(x) else nos).append(x)
637 class groups_view(osv.osv):
638 _inherit = 'res.groups'
640 def create(self, cr, uid, values, context=None):
641 res = super(groups_view, self).create(cr, uid, values, context)
642 self.update_user_groups_view(cr, uid, context)
645 def write(self, cr, uid, ids, values, context=None):
646 res = super(groups_view, self).write(cr, uid, ids, values, context)
647 self.update_user_groups_view(cr, uid, context)
650 def unlink(self, cr, uid, ids, context=None):
651 res = super(groups_view, self).unlink(cr, uid, ids, context)
652 self.update_user_groups_view(cr, uid, context)
655 def update_user_groups_view(self, cr, uid, context=None):
656 # the view with id 'base.user_groups_view' inherits the user form view,
657 # and introduces the reified group fields
658 view = self.get_user_groups_view(cr, uid, context)
661 xml1.append(E.separator(string=_('Application'), colspan="4"))
662 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
663 # hide groups in category 'Hidden' (except to group_no_one)
664 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
665 if kind == 'selection':
666 # application name with a selection field
667 field_name = name_selection_groups(map(int, gs))
668 xml1.append(E.field(name=field_name, **attrs))
669 xml1.append(E.newline())
671 # application separator with boolean fields
672 app_name = app and app.name or _('Other')
673 xml2.append(E.separator(string=app_name, colspan="4", **attrs))
675 field_name = name_boolean_group(g.id)
676 xml2.append(E.field(name=field_name, **attrs))
678 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
679 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
680 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
681 view.write({'arch': xml_content})
684 def get_user_groups_view(self, cr, uid, context=None):
686 view = self.pool.get('ir.model.data').get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
687 assert view and view._table_name == 'ir.ui.view'
692 def get_application_groups(self, cr, uid, domain=None, context=None):
693 return self.search(cr, uid, domain or [])
695 def get_groups_by_application(self, cr, uid, context=None):
696 """ return all groups classified by application (module category), as a list of pairs:
697 [(app, kind, [group, ...]), ...],
698 where app and group are browse records, and kind is either 'boolean' or 'selection'.
699 Applications are given in sequence order. If kind is 'selection', the groups are
700 given in reverse implication order.
704 # determine sequence order: a group should appear after its implied groups
705 order = dict.fromkeys(gs, 0)
707 for h in gs.intersection(g.trans_implied_ids):
709 # check whether order is total, i.e., sequence orders are distinct
710 if len(set(order.itervalues())) == len(gs):
711 return sorted(gs, key=lambda g: order[g])
714 # classify all groups by application
715 gids = self.get_application_groups(cr, uid, context=context)
716 by_app, others = {}, []
717 for g in self.browse(cr, uid, gids, context):
719 by_app.setdefault(g.category_id, []).append(g)
724 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
726 gs = linearized(by_app[app])
728 res.append((app, 'selection', gs))
730 res.append((app, 'boolean', by_app[app]))
732 res.append((False, 'boolean', others))
735 class users_view(osv.osv):
736 _inherit = 'res.users'
738 def create(self, cr, uid, values, context=None):
739 self._set_reified_groups(values)
740 return super(users_view, self).create(cr, uid, values, context)
742 def write(self, cr, uid, ids, values, context=None):
743 self._set_reified_groups(values)
744 return super(users_view, self).write(cr, uid, ids, values, context)
746 def _set_reified_groups(self, values):
747 """ reflect reified group fields in values['groups_id'] """
748 if 'groups_id' in values:
749 # groups are already given, ignore group fields
750 for f in filter(is_reified_group, values.iterkeys()):
755 for f in values.keys():
756 if is_boolean_group(f):
757 target = add if values.pop(f) else remove
758 target.append(get_boolean_group(f))
759 elif is_boolean_groups(f):
760 if not values.pop(f):
761 remove.extend(get_boolean_groups(f))
762 elif is_selection_groups(f):
763 remove.extend(get_selection_groups(f))
764 selected = values.pop(f)
767 # update values *only* if groups are being modified, otherwise
768 # we introduce spurious changes that might break the super.write() call.
770 # remove groups in 'remove' and add groups in 'add'
771 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
773 def default_get(self, cr, uid, fields, context=None):
774 group_fields, fields = partition(is_reified_group, fields)
775 fields1 = (fields + ['groups_id']) if group_fields else fields
776 values = super(users_view, self).default_get(cr, uid, fields1, context)
777 self._get_reified_groups(group_fields, values)
780 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
782 fields = self.fields_get(cr, uid, context=context).keys()
783 group_fields, fields = partition(is_reified_group, fields)
784 if not 'groups_id' in fields:
785 fields.append('groups_id')
786 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
787 for values in (res if isinstance(res, list) else [res]):
788 self._get_reified_groups(group_fields, values)
791 def _get_reified_groups(self, fields, values):
792 """ compute the given reified group fields from values['groups_id'] """
793 gids = set(values.get('groups_id') or [])
795 if is_boolean_group(f):
796 values[f] = get_boolean_group(f) in gids
797 elif is_boolean_groups(f):
798 values[f] = not gids.isdisjoint(get_boolean_groups(f))
799 elif is_selection_groups(f):
800 selected = [gid for gid in get_selection_groups(f) if gid in gids]
801 values[f] = selected and selected[-1] or False
803 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
804 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
805 # add reified groups fields
806 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
807 if kind == 'selection':
808 # selection group field
809 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
810 res[name_selection_groups(map(int, gs))] = {
812 'string': app and app.name or _('Other'),
813 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
814 'help': '\n'.join(tips),
817 # boolean group fields
819 res[name_boolean_group(g.id)] = {
826 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: