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-2013 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 ##############################################################################
22 from functools import partial
24 from lxml import etree
25 from lxml.builder import E
28 from openerp import SUPERUSER_ID
29 from openerp import pooler, tools
30 import openerp.exceptions
31 from openerp.osv import fields,osv
32 from openerp.osv.orm import browse_record
33 from openerp.tools.translate import _
35 _logger = logging.getLogger(__name__)
37 class groups(osv.osv):
39 _description = "Access Groups"
40 _rec_name = 'full_name'
42 def _get_full_name(self, cr, uid, ids, field, arg, context=None):
44 for g in self.browse(cr, uid, ids, context):
46 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
51 def _search_group(self, cr, uid, obj, name, args, context=None):
54 values = operand.split('/')
55 group_name = values[0]
56 where = [('name', operator, group_name)]
58 application_name = values[0]
59 group_name = values[1]
60 where = ['|',('category_id.name', operator, application_name)] + where
64 'name': fields.char('Name', size=64, required=True, translate=True),
65 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
66 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
67 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
68 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
69 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
70 'view_access': fields.many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', 'Views'),
71 'comment' : fields.text('Comment', size=250, translate=True),
72 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
73 'full_name': fields.function(_get_full_name, type='char', string='Group Name', fnct_search=_search_group),
77 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
80 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
81 # add explicit ordering if search is sorted on full_name
82 if order and order.startswith('full_name'):
83 ids = super(groups, self).search(cr, uid, args, context=context)
84 gs = self.browse(cr, uid, ids, context)
85 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
86 gs = gs[offset:offset+limit] if limit else gs[offset:]
88 return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
90 def copy(self, cr, uid, id, default=None, context=None):
91 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
92 default.update({'name': _('%s (copy)')%group_name})
93 return super(groups, self).copy(cr, uid, id, default, context)
95 def write(self, cr, uid, ids, vals, context=None):
97 if vals['name'].startswith('-'):
98 raise osv.except_osv(_('Error'),
99 _('The name of the group can not start with "-"'))
100 res = super(groups, self).write(cr, uid, ids, vals, context=context)
101 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
106 class res_users(osv.osv):
107 """ User class. A res.users record models an OpenERP user and is different
110 res.users class now inherits from res.partner. The partner model is
111 used to store the data related to the partner: lang, name, address,
112 avatar, ... The user model is now dedicated to technical data.
117 'res.partner': 'partner_id',
120 _description = 'Users'
122 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
124 # Do not update the password if no value is provided, ignore silently.
125 # For example web client submits False values for all empty fields.
128 # To change their own password users must use the client-specific change password wizard,
129 # so that the new password is immediately used for further RPC requests, otherwise the user
130 # will face unexpected 'Access Denied' exceptions.
131 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
132 self.write(cr, uid, id, {'password': value})
134 def _get_password(self, cr, uid, ids, arg, karg, context=None):
135 return dict.fromkeys(ids, '')
138 'id': fields.integer('ID'),
139 'login_date': fields.date('Latest connection', select=1),
140 'partner_id': fields.many2one('res.partner', required=True,
141 string='Related Partner', ondelete='restrict',
142 help='Partner-related data of the user'),
143 'login': fields.char('Login', size=64, required=True,
144 help="Used to log into the system"),
145 'password': fields.char('Password', size=64, invisible=True,
146 help="Keep empty if you don't want the user to be able to connect on the system."),
147 'new_password': fields.function(_get_password, type='char', size=64,
148 fnct_inv=_set_new_password, string='Set Password',
149 help="Specify a value only when creating a user or if you're "\
150 "changing the user's password, otherwise leave empty. After "\
151 "a change of password, the user has to login again."),
152 'signature': fields.text('Signature'),
153 'active': fields.boolean('Active'),
154 '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."),
155 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
156 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
157 # Special behavior for this field: res.company.search() will only return the companies
158 # available to the current user (should be the user's companies?), when the user_preference
160 'company_id': fields.many2one('res.company', 'Company', required=True,
161 help='The company this user is currently working for.', context={'user_preference': True}),
162 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
163 # backward compatibility fields
164 'user_email': fields.related('email', type='char',
165 deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'),
168 def on_change_company_id(self, cr, uid, ids, company_id):
169 return {'warning' : {
170 'title': _("Company Switch Warning"),
171 '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)"),
175 def onchange_state(self, cr, uid, ids, state_id, context=None):
176 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
177 return self.pool.get('res.partner').onchange_state(cr, uid, partner_ids, state_id, context=context)
179 def onchange_type(self, cr, uid, ids, is_company, context=None):
180 """ Wrapper on the user.partner onchange_type, because some calls to the
181 partner form view applied to the user may trigger the
182 partner.onchange_type method, but applied to the user object.
184 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
185 return self.pool.get('res.partner').onchange_type(cr, uid, partner_ids, is_company, context=context)
187 def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
188 """ Wrapper on the user.partner onchange_address, because some calls to the
189 partner form view applied to the user may trigger the
190 partner.onchange_type method, but applied to the user object.
192 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
193 return self.pool.get('res.partner').onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
195 def _check_company(self, cr, uid, ids, context=None):
196 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
199 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
203 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
206 def _get_company(self,cr, uid, context=None, uid2=False):
209 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
210 company_id = user.get('company_id', False)
211 return company_id and company_id[0] or False
213 def _get_companies(self, cr, uid, context=None):
214 c = self._get_company(cr, uid, context)
219 def _get_menu(self,cr, uid, context=None):
220 dataobj = self.pool.get('ir.model.data')
222 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
223 if model != 'ir.actions.act_window':
229 def _get_group(self,cr, uid, context=None):
230 dataobj = self.pool.get('ir.model.data')
233 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_user')
234 result.append(group_id)
235 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_partner_manager')
236 result.append(group_id)
238 # If these groups does not exists anymore
246 'menu_id': _get_menu,
247 'company_id': _get_company,
248 'company_ids': _get_companies,
249 'groups_id': _get_group,
250 'image': lambda self, cr, uid, ctx={}: self.pool.get('res.partner')._get_default_image(cr, uid, False, ctx, colorize=True),
253 # User can write on a few of his own fields (but not his groups for example)
254 SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz']
255 # User can read a few of his own fields
256 SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update']
258 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
259 def override_password(o):
260 if 'password' in o and ('id' not in o or o['id'] != uid):
261 o['password'] = '********'
264 if fields and (ids == [uid] or ids == uid):
266 if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')):
269 # safe fields only, so we read as super-user to bypass access rights
272 result = super(res_users, self).read(cr, uid, ids, fields=fields, context=context, load=load)
273 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
275 if isinstance(ids, (int, long)):
276 result = override_password(result)
278 result = map(override_password, result)
282 def create(self, cr, uid, vals, context=None):
283 user_id = super(res_users, self).create(cr, uid, vals, context=context)
284 user = self.browse(cr, uid, user_id, context=context)
285 if user.partner_id.company_id:
286 user.partner_id.write({'company_id': user.company_id.id})
289 def write(self, cr, uid, ids, values, context=None):
290 if not hasattr(ids, '__iter__'):
293 for key in values.keys():
294 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
297 if 'company_id' in values:
298 if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):
299 del values['company_id']
300 uid = 1 # safe fields only, so we write as super-user to bypass access rights
302 res = super(res_users, self).write(cr, uid, ids, values, context=context)
303 if 'company_id' in values:
304 for user in self.browse(cr, uid, ids, context=context):
305 # if partner is global we keep it that way
306 if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']:
307 user.partner_id.write({'company_id': user.company_id.id})
308 # clear caches linked to the users
309 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
310 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
313 if db in self._uid_cache:
315 if id in self._uid_cache[db]:
316 del self._uid_cache[db][id]
317 self.context_get.clear_cache(self)
320 def unlink(self, cr, uid, ids, context=None):
322 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, ...)'))
324 if db in self._uid_cache:
326 if id in self._uid_cache[db]:
327 del self._uid_cache[db][id]
328 return super(res_users, self).unlink(cr, uid, ids, context=context)
330 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
337 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit, context=context)
339 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
340 return self.name_get(cr, user, ids, context=context)
342 def copy(self, cr, uid, id, default=None, context=None):
343 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
344 default = dict(default or {})
345 if ('name' not in default) and ('partner_id' not in default):
346 default['name'] = _("%s (copy)") % user2copy['name']
347 if 'login' not in default:
348 default['login'] = _("%s (copy)") % user2copy['login']
349 return super(res_users, self).copy(cr, uid, id, default, context)
351 @tools.ormcache(skiparg=2)
352 def context_get(self, cr, uid, context=None):
353 user = self.browse(cr, SUPERUSER_ID, uid, context)
355 for k in self._all_columns.keys():
356 if k.startswith('context_'):
358 elif k in ['lang', 'tz']:
363 res = getattr(user,k) or False
364 if isinstance(res, browse_record):
366 result[context_key] = res or False
369 def action_get(self, cr, uid, context=None):
370 dataobj = self.pool.get('ir.model.data')
371 data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
372 return dataobj.browse(cr, uid, data_id, context=context).res_id
374 def check_super(self, passwd):
375 if passwd == tools.config['admin_passwd']:
378 raise openerp.exceptions.AccessDenied()
380 def check_credentials(self, cr, uid, password):
381 """ Override this method to plug additional authentication methods"""
382 res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
384 raise openerp.exceptions.AccessDenied()
386 def login(self, db, login, password):
390 cr = pooler.get_db(db).cursor()
392 # autocommit: our single update request will be performed atomically.
393 # (In this way, there is no opportunity to have two transactions
394 # interleaving their cr.execute()..cr.commit() calls and have one
395 # of them rolled back due to a concurrent access.)
397 # check if user exists
398 res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
402 self.check_credentials(cr, user_id, password)
403 # We effectively unconditionally write the res_users line.
404 # Even w/ autocommit there's a chance the user row will be locked,
405 # in which case we can't delay the login just for the purpose of
406 # update the last login date - hence we use FOR UPDATE NOWAIT to
407 # try to get the lock - fail-fast
408 # Failing to acquire the lock on the res_users row probably means
409 # another request is holding it. No big deal, we don't want to
410 # prevent/delay login in that case. It will also have been logged
411 # as a SQL error, if anyone cares.
413 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
414 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
416 _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
417 except openerp.exceptions.AccessDenied:
418 _logger.info("Login failed for db:%s login:%s", db, login)
425 def authenticate(self, db, login, password, user_agent_env):
426 """Verifies and returns the user ID corresponding to the given
427 ``login`` and ``password`` combination, or False if there was
430 :param str db: the database on which user is trying to authenticate
431 :param str login: username
432 :param str password: user password
433 :param dict user_agent_env: environment dictionary describing any
434 relevant environment attributes
436 uid = self.login(db, login, password)
437 if uid == openerp.SUPERUSER_ID:
438 # Successfully logged in as admin!
439 # Attempt to guess the web base url...
440 if user_agent_env and user_agent_env.get('base_location'):
441 cr = pooler.get_db(db).cursor()
443 base = user_agent_env['base_location']
444 ICP = self.pool.get('ir.config_parameter')
445 if not ICP.get_param(cr, uid, 'web.base.url.freeze'):
446 ICP.set_param(cr, uid, 'web.base.url', base)
449 _logger.exception("Failed to update web.base.url configuration parameter")
454 def check(self, db, uid, passwd):
455 """Verifies that the given (uid, password) is authorized for the database ``db`` and
456 raise an exception if it is not."""
458 # empty passwords disallowed for obvious security reasons
459 raise openerp.exceptions.AccessDenied()
460 if self._uid_cache.get(db, {}).get(uid) == passwd:
462 cr = pooler.get_db(db).cursor()
464 self.check_credentials(cr, uid, passwd)
465 if self._uid_cache.has_key(db):
466 self._uid_cache[db][uid] = passwd
468 self._uid_cache[db] = {uid:passwd}
472 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
473 """Change current user password. Old password must be provided explicitly
474 to prevent hijacking an existing user session, or for cases where the cleartext
475 password is not used to authenticate requests.
478 :raise: openerp.exceptions.AccessDenied when old password is wrong
479 :raise: except_osv when new password is not set or empty
481 self.check(cr.dbname, uid, old_passwd)
483 return self.write(cr, uid, uid, {'password': new_passwd})
484 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
486 def preference_save(self, cr, uid, ids, context=None):
488 'type': 'ir.actions.client',
492 def preference_change_password(self, cr, uid, ids, context=None):
494 'type': 'ir.actions.client',
495 'tag': 'change_password',
499 def has_group(self, cr, uid, group_ext_id):
500 """Checks whether user belongs to given group.
502 :param str group_ext_id: external ID (XML ID) of the group.
503 Must be provided in fully-qualified form (``module.ext_id``), as there
504 is no implicit module to use..
505 :return: True if the current user is a member of the group with the
506 given external ID (XML ID), else False.
508 assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
509 module, ext_id = group_ext_id.split('.')
510 cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
511 (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
512 (uid, module, ext_id))
513 return bool(cr.fetchone())
517 # Extension of res.groups and res.users with a relation for "implied" or
518 # "inherited" groups. Once a user belongs to a group, it automatically belongs
519 # to the implied groups (transitively).
523 """ A cset (constrained set) is a set of elements that may be constrained to
524 be a subset of other csets. Elements added to a cset are automatically
525 added to its supersets. Cycles in the subset constraints are supported.
527 def __init__(self, xs):
528 self.supersets = set()
529 self.elements = set(xs)
530 def subsetof(self, other):
531 if other is not self:
532 self.supersets.add(other)
533 other.update(self.elements)
534 def update(self, xs):
535 xs = set(xs) - self.elements
536 if xs: # xs will eventually be empty in case of a cycle
537 self.elements.update(xs)
538 for s in self.supersets:
541 return iter(self.elements)
544 """ return the concatenation of a list of iterables """
546 for l in ls: res.extend(l)
551 class groups_implied(osv.osv):
552 _inherit = 'res.groups'
554 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
555 "computes the transitive closure of relation implied_ids"
556 memo = {} # use a memo for performance and cycle avoidance
559 memo[g] = cset(g.implied_ids)
560 for h in g.implied_ids:
561 computed_set(h).subsetof(memo[g])
565 for g in self.browse(cr, SUPERUSER_ID, ids, context):
566 res[g.id] = map(int, computed_set(g))
570 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
571 string='Inherits', help='Users of this group automatically inherit those groups'),
572 'trans_implied_ids': fields.function(_get_trans_implied,
573 type='many2many', relation='res.groups', string='Transitively inherits'),
576 def create(self, cr, uid, values, context=None):
577 users = values.pop('users', None)
578 gid = super(groups_implied, self).create(cr, uid, values, context)
580 # delegate addition of users to add implied groups
581 self.write(cr, uid, [gid], {'users': users}, context)
584 def write(self, cr, uid, ids, values, context=None):
585 res = super(groups_implied, self).write(cr, uid, ids, values, context)
586 if values.get('users') or values.get('implied_ids'):
587 # add all implied groups (to all users of each group)
588 for g in self.browse(cr, uid, ids):
589 gids = map(int, g.trans_implied_ids)
590 vals = {'users': [(4, u.id) for u in g.users]}
591 super(groups_implied, self).write(cr, uid, gids, vals, context)
594 class users_implied(osv.osv):
595 _inherit = 'res.users'
597 def create(self, cr, uid, values, context=None):
598 groups = values.pop('groups_id', None)
599 user_id = super(users_implied, self).create(cr, uid, values, context)
601 # delegate addition of groups to add implied groups
602 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
605 def write(self, cr, uid, ids, values, context=None):
606 if not isinstance(ids,list):
608 res = super(users_implied, self).write(cr, uid, ids, values, context)
609 if values.get('groups_id'):
610 # add implied groups for all users
611 for user in self.browse(cr, uid, ids):
612 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
613 vals = {'groups_id': [(4, g.id) for g in gs]}
614 super(users_implied, self).write(cr, uid, [user.id], vals, context)
618 # Extension of res.groups and res.users for the special groups view in the users
619 # form. This extension presents groups with selection and boolean widgets:
620 # - Groups are shown by application, with boolean and/or selection fields.
621 # Selection fields typically defines a role "Name" for the given application.
622 # - Uncategorized groups are presented as boolean fields and grouped in a
625 # The user form view is modified by an inherited view (base.user_groups_view);
626 # the inherited view replaces the field 'groups_id' by a set of reified group
627 # fields (boolean or selection fields). The arch of that view is regenerated
628 # each time groups are changed.
630 # Naming conventions for reified groups fields:
631 # - boolean field 'in_group_ID' is True iff
632 # ID is in 'groups_id'
633 # - boolean field 'in_groups_ID1_..._IDk' is True iff
634 # any of ID1, ..., IDk is in 'groups_id'
635 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
636 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
638 def name_boolean_group(id): return 'in_group_' + str(id)
639 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
640 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
642 def is_boolean_group(name): return name.startswith('in_group_')
643 def is_boolean_groups(name): return name.startswith('in_groups_')
644 def is_selection_groups(name): return name.startswith('sel_groups_')
645 def is_reified_group(name):
646 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
648 def get_boolean_group(name): return int(name[9:])
649 def get_boolean_groups(name): return map(int, name[10:].split('_'))
650 def get_selection_groups(name): return map(int, name[11:].split('_'))
652 def partition(f, xs):
653 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
656 (yes if f(x) else nos).append(x)
661 class groups_view(osv.osv):
662 _inherit = 'res.groups'
664 def create(self, cr, uid, values, context=None):
665 res = super(groups_view, self).create(cr, uid, values, context)
666 self.update_user_groups_view(cr, uid, context)
669 def write(self, cr, uid, ids, values, context=None):
670 res = super(groups_view, self).write(cr, uid, ids, values, context)
671 self.update_user_groups_view(cr, uid, context)
674 def unlink(self, cr, uid, ids, context=None):
675 res = super(groups_view, self).unlink(cr, uid, ids, context)
676 self.update_user_groups_view(cr, uid, context)
679 def update_user_groups_view(self, cr, uid, context=None):
680 # the view with id 'base.user_groups_view' inherits the user form view,
681 # and introduces the reified group fields
682 view = self.get_user_groups_view(cr, uid, context)
685 xml1.append(E.separator(string=_('Application'), colspan="4"))
686 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
687 # hide groups in category 'Hidden' (except to group_no_one)
688 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
689 if kind == 'selection':
690 # application name with a selection field
691 field_name = name_selection_groups(map(int, gs))
692 xml1.append(E.field(name=field_name, **attrs))
693 xml1.append(E.newline())
695 # application separator with boolean fields
696 app_name = app and app.name or _('Other')
697 xml2.append(E.separator(string=app_name, colspan="4", **attrs))
699 field_name = name_boolean_group(g.id)
700 xml2.append(E.field(name=field_name, **attrs))
702 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
703 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
704 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
705 view.write({'arch': xml_content})
708 def get_user_groups_view(self, cr, uid, context=None):
710 view = self.pool.get('ir.model.data').get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
711 assert view and view._table_name == 'ir.ui.view'
716 def get_application_groups(self, cr, uid, domain=None, context=None):
717 return self.search(cr, uid, domain or [])
719 def get_groups_by_application(self, cr, uid, context=None):
720 """ return all groups classified by application (module category), as a list of pairs:
721 [(app, kind, [group, ...]), ...],
722 where app and group are browse records, and kind is either 'boolean' or 'selection'.
723 Applications are given in sequence order. If kind is 'selection', the groups are
724 given in reverse implication order.
728 # determine sequence order: a group should appear after its implied groups
729 order = dict.fromkeys(gs, 0)
731 for h in gs.intersection(g.trans_implied_ids):
733 # check whether order is total, i.e., sequence orders are distinct
734 if len(set(order.itervalues())) == len(gs):
735 return sorted(gs, key=lambda g: order[g])
738 # classify all groups by application
739 gids = self.get_application_groups(cr, uid, context=context)
740 by_app, others = {}, []
741 for g in self.browse(cr, uid, gids, context):
743 by_app.setdefault(g.category_id, []).append(g)
748 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
750 gs = linearized(by_app[app])
752 res.append((app, 'selection', gs))
754 res.append((app, 'boolean', by_app[app]))
756 res.append((False, 'boolean', others))
759 class users_view(osv.osv):
760 _inherit = 'res.users'
762 def create(self, cr, uid, values, context=None):
763 self._set_reified_groups(values)
764 return super(users_view, self).create(cr, uid, values, context)
766 def write(self, cr, uid, ids, values, context=None):
767 self._set_reified_groups(values)
768 return super(users_view, self).write(cr, uid, ids, values, context)
770 def _set_reified_groups(self, values):
771 """ reflect reified group fields in values['groups_id'] """
772 if 'groups_id' in values:
773 # groups are already given, ignore group fields
774 for f in filter(is_reified_group, values.iterkeys()):
779 for f in values.keys():
780 if is_boolean_group(f):
781 target = add if values.pop(f) else remove
782 target.append(get_boolean_group(f))
783 elif is_boolean_groups(f):
784 if not values.pop(f):
785 remove.extend(get_boolean_groups(f))
786 elif is_selection_groups(f):
787 remove.extend(get_selection_groups(f))
788 selected = values.pop(f)
791 # update values *only* if groups are being modified, otherwise
792 # we introduce spurious changes that might break the super.write() call.
794 # remove groups in 'remove' and add groups in 'add'
795 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
797 def default_get(self, cr, uid, fields, context=None):
798 group_fields, fields = partition(is_reified_group, fields)
799 fields1 = (fields + ['groups_id']) if group_fields else fields
800 values = super(users_view, self).default_get(cr, uid, fields1, context)
801 self._get_reified_groups(group_fields, values)
804 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
806 fields = self.fields_get(cr, uid, context=context).keys()
807 group_fields, fields = partition(is_reified_group, fields)
808 if not 'groups_id' in fields:
809 fields.append('groups_id')
810 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
811 for values in (res if isinstance(res, list) else [res]):
812 self._get_reified_groups(group_fields, values)
815 def _get_reified_groups(self, fields, values):
816 """ compute the given reified group fields from values['groups_id'] """
817 gids = set(values.get('groups_id') or [])
819 if is_boolean_group(f):
820 values[f] = get_boolean_group(f) in gids
821 elif is_boolean_groups(f):
822 values[f] = not gids.isdisjoint(get_boolean_groups(f))
823 elif is_selection_groups(f):
824 selected = [gid for gid in get_selection_groups(f) if gid in gids]
825 values[f] = selected and selected[-1] or False
827 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
828 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
829 # add reified groups fields
830 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
831 if kind == 'selection':
832 # selection group field
833 tips = ['%s: %s' % (g.name, g.comment) for g in gs if g.comment]
834 res[name_selection_groups(map(int, gs))] = {
836 'string': app and app.name or _('Other'),
837 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
838 'help': '\n'.join(tips),
842 # boolean group fields
844 res[name_boolean_group(g.id)] = {
852 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: