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-2011 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
28 from lxml import etree
29 from lxml.builder import E
32 import openerp.exceptions
33 from osv import fields,osv
34 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)
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 # 'context_company_id': fields.related('context_company_id', rel='res.company',
156 # string='Company', required=True, context={'user_preference': True},
157 # help='The company this user is currently working for.',
158 # deprecated='Use the context_company_id field instead. This field will be removed as of OpenERP 7.1.'),
159 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
160 # backward compatibility fields
161 'user_email': fields.related('email', type='char',
162 deprecated='Use the email field instead of user_email. This field will be removed as of OpenERP 7.1.'),
163 'date': fields.related('date', type='date', store=True,
164 deprecated='use the login_date field instead of date. This field will be removed as of OpenERP 7.1.'),
167 def on_change_company_id(self, cr, uid, ids, company_id):
168 return {'warning' : {
169 'title': _("Company Switch Warning"),
170 '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)"),
174 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
175 def override_password(o):
176 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
177 o['password'] = '********'
179 result = super(users, self).read(cr, uid, ids, fields, context, load)
180 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
182 if isinstance(ids, (int, float)):
183 result = override_password(result)
185 result = map(override_password, result)
189 def _check_company(self, cr, uid, ids, context=None):
190 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
193 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
197 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
200 def _get_company(self,cr, uid, context=None, uid2=False):
203 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
204 company_id = user.get('company_id', False)
205 return company_id and company_id[0] or False
207 def _get_companies(self, cr, uid, context=None):
208 c = self._get_company(cr, uid, context)
213 def _get_menu(self,cr, uid, context=None):
214 dataobj = self.pool.get('ir.model.data')
216 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
217 if model != 'ir.actions.act_window':
223 def _get_group(self,cr, uid, context=None):
224 dataobj = self.pool.get('ir.model.data')
227 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
228 result.append(group_id)
229 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
230 result.append(group_id)
232 # If these groups does not exists anymore
240 'menu_id': _get_menu,
241 'company_id': _get_company,
242 'company_ids': _get_companies,
243 'groups_id': _get_group,
246 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
247 """ Override of res.users fields_view_get.
248 - if the view is specified: resume with normal behavior
249 - else: the default view is overrided and redirected to the partner
254 if not view_id and view_type == 'form':
256 return self.pool.get('res.partner').fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
257 return super(users, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
259 # User can write to a few of her own fields (but not her groups for example)
260 SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small']
262 def write(self, cr, uid, ids, values, context=None):
263 if not hasattr(ids, '__iter__'):
266 for key in values.keys():
267 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
270 if 'company_id' in values:
271 if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
272 del values['company_id']
273 uid = 1 # safe fields only, so we write as super-user to bypass access rights
275 res = super(users, self).write(cr, uid, ids, values, context=context)
277 # clear caches linked to the users
278 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
279 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
282 if db in self._uid_cache:
284 if id in self._uid_cache[db]:
285 del self._uid_cache[db][id]
289 def unlink(self, cr, uid, ids, context=None):
291 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, ...)'))
293 if db in self._uid_cache:
295 if id in self._uid_cache[db]:
296 del self._uid_cache[db][id]
297 return super(users, self).unlink(cr, uid, ids, context=context)
299 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
306 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
308 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
309 return self.name_get(cr, user, ids)
311 def copy(self, cr, uid, id, default=None, context=None):
312 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
315 copy_pattern = _("%s (copy)")
316 copydef = dict(login=(copy_pattern % user2copy['login']),
317 name=(copy_pattern % user2copy['name']),
319 copydef.update(default)
320 return super(users, self).copy(cr, uid, id, copydef, context)
322 def context_get(self, cr, uid, context=None):
323 user = self.browse(cr, uid, uid, context)
325 for k in self._all_columns.keys():
326 if k.startswith('context_'):
328 elif k in ['lang', 'tz']:
333 res = getattr(user,k) or False
334 if isinstance(res, browse_record):
336 result[context_key] = res or False
339 def action_get(self, cr, uid, context=None):
340 dataobj = self.pool.get('ir.model.data')
341 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
342 return dataobj.browse(cr, uid, data_id, context=context).res_id
344 def authenticate(self, db, login, password, user_agent_env):
345 """Verifies and returns the user ID corresponding to the given
346 ``login`` and ``password`` combination, or False if there was
349 :param str db: the database on which user is trying to authenticate
350 :param str login: username
351 :param str password: user password
352 :param dict user_agent_env: environment dictionary describing any
353 relevant environment attributes
355 uid = self.login(db, login, password)
356 if uid == openerp.SUPERUSER_ID:
357 # Successfully logged in as admin!
358 # Attempt to guess the web base url...
359 if user_agent_env and user_agent_env.get('base_location'):
360 cr = pooler.get_db(db).cursor()
362 self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url',
363 user_agent_env['base_location'])
366 _logger.exception("Failed to update web.base.url configuration parameter")
371 def login(self, db, login, password):
374 cr = pooler.get_db(db).cursor()
376 # autocommit: our single request will be performed atomically.
377 # (In this way, there is no opportunity to have two transactions
378 # interleaving their cr.execute()..cr.commit() calls and have one
379 # of them rolled back due to a concurrent access.)
380 # 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 cr.execute("""SELECT id from res_users
387 WHERE login=%s AND password=%s
388 AND active FOR UPDATE NOWAIT""",
389 (tools.ustr(login), tools.ustr(password)))
390 cr.execute("""UPDATE res_users
391 SET login_date = now() AT TIME ZONE 'UTC'
392 WHERE login=%s AND password=%s AND active
394 (tools.ustr(login), tools.ustr(password)))
396 # Failing to acquire the lock on the res_users row probably means
397 # another request is holding it. No big deal, we don't want to
398 # prevent/delay login in that case. It will also have been logged
399 # as a SQL error, if anyone cares.
400 cr.execute("""SELECT id from res_users
401 WHERE login=%s AND password=%s
403 (tools.ustr(login), tools.ustr(password)))
411 def check_super(self, passwd):
412 if passwd == tools.config['admin_passwd']:
415 raise openerp.exceptions.AccessDenied()
417 def check(self, db, uid, passwd):
418 """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
419 raise an exception if it is not."""
421 # empty passwords disallowed for obvious security reasons
422 raise openerp.exceptions.AccessDenied()
423 if self._uid_cache.get(db, {}).get(uid) == passwd:
425 cr = pooler.get_db(db).cursor()
427 cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
428 (int(uid), passwd, True))
429 res = cr.fetchone()[0]
431 raise openerp.exceptions.AccessDenied()
432 if self._uid_cache.has_key(db):
433 ulist = self._uid_cache[db]
436 self._uid_cache[db] = {uid:passwd}
440 def access(self, db, uid, passwd, sec_level, ids):
443 cr = pooler.get_db(db).cursor()
445 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
448 raise openerp.exceptions.AccessDenied()
453 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
454 """Change current user password. Old password must be provided explicitly
455 to prevent hijacking an existing user session, or for cases where the cleartext
456 password is not used to authenticate requests.
459 :raise: openerp.exceptions.AccessDenied when old password is wrong
460 :raise: except_osv when new password is not set or empty
462 self.check(cr.dbname, uid, old_passwd)
464 return self.write(cr, uid, uid, {'password': new_passwd})
465 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
467 def preference_save(self, cr, uid, ids, context=None):
469 'type': 'ir.actions.client',
473 def preference_change_password(self, cr, uid, ids, context=None):
475 'type': 'ir.actions.client',
476 'tag': 'change_password',
480 def has_group(self, cr, uid, group_ext_id):
481 """Checks whether user belongs to given group.
483 :param str group_ext_id: external ID (XML ID) of the group.
484 Must be provided in fully-qualified form (``module.ext_id``), as there
485 is no implicit module to use..
486 :return: True if the current user is a member of the group with the
487 given external ID (XML ID), else False.
489 assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
490 module, ext_id = group_ext_id.split('.')
491 cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
492 (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
493 (uid, module, ext_id))
494 return bool(cr.fetchone())
498 # Extension of res.groups and res.users with a relation for "implied" or
499 # "inherited" groups. Once a user belongs to a group, it automatically belongs
500 # to the implied groups (transitively).
504 """ A cset (constrained set) is a set of elements that may be constrained to
505 be a subset of other csets. Elements added to a cset are automatically
506 added to its supersets. Cycles in the subset constraints are supported.
508 def __init__(self, xs):
509 self.supersets = set()
510 self.elements = set(xs)
511 def subsetof(self, other):
512 if other is not self:
513 self.supersets.add(other)
514 other.update(self.elements)
515 def update(self, xs):
516 xs = set(xs) - self.elements
517 if xs: # xs will eventually be empty in case of a cycle
518 self.elements.update(xs)
519 for s in self.supersets:
522 return iter(self.elements)
525 """ return the concatenation of a list of iterables """
527 for l in ls: res.extend(l)
532 class groups_implied(osv.osv):
533 _inherit = 'res.groups'
535 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
536 "computes the transitive closure of relation implied_ids"
537 memo = {} # use a memo for performance and cycle avoidance
540 memo[g] = cset(g.implied_ids)
541 for h in g.implied_ids:
542 computed_set(h).subsetof(memo[g])
546 for g in self.browse(cr, 1, ids, context):
547 res[g.id] = map(int, computed_set(g))
551 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
552 string='Inherits', help='Users of this group automatically inherit those groups'),
553 'trans_implied_ids': fields.function(_get_trans_implied,
554 type='many2many', relation='res.groups', string='Transitively inherits'),
557 def create(self, cr, uid, values, context=None):
558 users = values.pop('users', None)
559 gid = super(groups_implied, self).create(cr, uid, values, context)
561 # delegate addition of users to add implied groups
562 self.write(cr, uid, [gid], {'users': users}, context)
565 def write(self, cr, uid, ids, values, context=None):
566 res = super(groups_implied, self).write(cr, uid, ids, values, context)
567 if values.get('users') or values.get('implied_ids'):
568 # add all implied groups (to all users of each group)
569 for g in self.browse(cr, uid, ids):
570 gids = map(int, g.trans_implied_ids)
571 vals = {'users': [(4, u.id) for u in g.users]}
572 super(groups_implied, self).write(cr, uid, gids, vals, context)
577 class users_implied(osv.osv):
578 _inherit = 'res.users'
580 def create(self, cr, uid, values, context=None):
581 groups = values.pop('groups_id', None)
582 user_id = super(users_implied, self).create(cr, uid, values, context)
584 # delegate addition of groups to add implied groups
585 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
588 def write(self, cr, uid, ids, values, context=None):
589 if not isinstance(ids,list):
591 res = super(users_implied, self).write(cr, uid, ids, values, context)
592 if values.get('groups_id'):
593 # add implied groups for all users
594 for user in self.browse(cr, uid, ids):
595 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
596 vals = {'groups_id': [(4, g.id) for g in gs]}
597 super(users_implied, self).write(cr, uid, [user.id], vals, context)
605 # Extension of res.groups and res.users for the special groups view in the users
606 # form. This extension presents groups with selection and boolean widgets:
607 # - Groups are shown by application, with boolean and/or selection fields.
608 # Selection fields typically defines a role "Name" for the given application.
609 # - Uncategorized groups are presented as boolean fields and grouped in a
612 # The user form view is modified by an inherited view (base.user_groups_view);
613 # the inherited view replaces the field 'groups_id' by a set of reified group
614 # fields (boolean or selection fields). The arch of that view is regenerated
615 # each time groups are changed.
617 # Naming conventions for reified groups fields:
618 # - boolean field 'in_group_ID' is True iff
619 # ID is in 'groups_id'
620 # - boolean field 'in_groups_ID1_..._IDk' is True iff
621 # any of ID1, ..., IDk is in 'groups_id'
622 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
623 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
625 def name_boolean_group(id): return 'in_group_' + str(id)
626 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
627 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
629 def is_boolean_group(name): return name.startswith('in_group_')
630 def is_boolean_groups(name): return name.startswith('in_groups_')
631 def is_selection_groups(name): return name.startswith('sel_groups_')
632 def is_reified_group(name):
633 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
635 def get_boolean_group(name): return int(name[9:])
636 def get_boolean_groups(name): return map(int, name[10:].split('_'))
637 def get_selection_groups(name): return map(int, name[11:].split('_'))
639 def partition(f, xs):
640 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
643 (yes if f(x) else nos).append(x)
648 class groups_view(osv.osv):
649 _inherit = 'res.groups'
651 def create(self, cr, uid, values, context=None):
652 res = super(groups_view, self).create(cr, uid, values, context)
653 self.update_user_groups_view(cr, uid, context)
656 def write(self, cr, uid, ids, values, context=None):
657 res = super(groups_view, self).write(cr, uid, ids, values, context)
658 self.update_user_groups_view(cr, uid, context)
661 def unlink(self, cr, uid, ids, context=None):
662 res = super(groups_view, self).unlink(cr, uid, ids, context)
663 self.update_user_groups_view(cr, uid, context)
666 def update_user_groups_view(self, cr, uid, context=None):
667 # the view with id 'base.user_groups_view' inherits the user form view,
668 # and introduces the reified group fields
669 view = self.get_user_groups_view(cr, uid, context)
672 xml1.append(E.separator(string=_('Application'), colspan="4"))
673 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
674 # hide groups in category 'Hidden' (except to group_no_one)
675 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
676 if kind == 'selection':
677 # application name with a selection field
678 field_name = name_selection_groups(map(int, gs))
679 xml1.append(E.field(name=field_name, **attrs))
680 xml1.append(E.newline())
682 # application separator with boolean fields
683 app_name = app and app.name or _('Other')
684 xml2.append(E.separator(string=app_name, colspan="4", **attrs))
686 field_name = name_boolean_group(g.id)
687 xml2.append(E.field(name=field_name, **attrs))
689 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
690 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
691 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
692 view.write({'arch': xml_content})
695 def get_user_groups_view(self, cr, uid, context=None):
697 view = self.pool.get('ir.model.data').get_object(cr, 1, 'base', 'user_groups_view', context)
698 assert view and view._table_name == 'ir.ui.view'
703 def get_application_groups(self, cr, uid, domain=None, context=None):
704 return self.search(cr, uid, domain or [])
706 def get_groups_by_application(self, cr, uid, context=None):
707 """ return all groups classified by application (module category), as a list of pairs:
708 [(app, kind, [group, ...]), ...],
709 where app and group are browse records, and kind is either 'boolean' or 'selection'.
710 Applications are given in sequence order. If kind is 'selection', the groups are
711 given in reverse implication order.
715 # determine sequence order: a group should appear after its implied groups
716 order = dict.fromkeys(gs, 0)
718 for h in gs.intersection(g.trans_implied_ids):
720 # check whether order is total, i.e., sequence orders are distinct
721 if len(set(order.itervalues())) == len(gs):
722 return sorted(gs, key=lambda g: order[g])
725 # classify all groups by application
726 gids = self.get_application_groups(cr, uid, context=context)
727 by_app, others = {}, []
728 for g in self.browse(cr, uid, gids, context):
730 by_app.setdefault(g.category_id, []).append(g)
735 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
737 gs = linearized(by_app[app])
739 res.append((app, 'selection', gs))
741 res.append((app, 'boolean', by_app[app]))
743 res.append((False, 'boolean', others))
748 class users_view(osv.osv):
749 _inherit = 'res.users'
751 def create(self, cr, uid, values, context=None):
752 self._set_reified_groups(values)
753 return super(users_view, self).create(cr, uid, values, context)
755 def write(self, cr, uid, ids, values, context=None):
756 self._set_reified_groups(values)
757 return super(users_view, self).write(cr, uid, ids, values, context)
759 def _set_reified_groups(self, values):
760 """ reflect reified group fields in values['groups_id'] """
761 if 'groups_id' in values:
762 # groups are already given, ignore group fields
763 for f in filter(is_reified_group, values.iterkeys()):
768 for f in values.keys():
769 if is_boolean_group(f):
770 target = add if values.pop(f) else remove
771 target.append(get_boolean_group(f))
772 elif is_boolean_groups(f):
773 if not values.pop(f):
774 remove.extend(get_boolean_groups(f))
775 elif is_selection_groups(f):
776 remove.extend(get_selection_groups(f))
777 selected = values.pop(f)
780 # update values *only* if groups are being modified, otherwise
781 # we introduce spurious changes that might break the super.write() call.
783 # remove groups in 'remove' and add groups in 'add'
784 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
786 def default_get(self, cr, uid, fields, context=None):
787 group_fields, fields = partition(is_reified_group, fields)
788 fields1 = (fields + ['groups_id']) if group_fields else fields
789 values = super(users_view, self).default_get(cr, uid, fields1, context)
790 self._get_reified_groups(group_fields, values)
793 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
795 fields = self.fields_get(cr, uid, context=context).keys()
796 group_fields, fields = partition(is_reified_group, fields)
797 if not 'groups_id' in fields:
798 fields.append('groups_id')
799 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
800 for values in (res if isinstance(res, list) else [res]):
801 self._get_reified_groups(group_fields, values)
804 def _get_reified_groups(self, fields, values):
805 """ compute the given reified group fields from values['groups_id'] """
806 gids = set(values.get('groups_id') or [])
808 if is_boolean_group(f):
809 values[f] = get_boolean_group(f) in gids
810 elif is_boolean_groups(f):
811 values[f] = not gids.isdisjoint(get_boolean_groups(f))
812 elif is_selection_groups(f):
813 selected = [gid for gid in get_selection_groups(f) if gid in gids]
814 values[f] = selected and selected[-1] or False
816 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
817 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
818 # add reified groups fields
819 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
820 if kind == 'selection':
821 # selection group field
822 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
823 res[name_selection_groups(map(int, gs))] = {
825 'string': app and app.name or _('Other'),
826 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
827 'help': '\n'.join(tips),
830 # boolean group fields
832 res[name_boolean_group(g.id)] = {
841 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: