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)
56 def _search_group(self, cr, uid, obj, name, args, context=None):
59 values = operand.split('/')
60 group_name = values[0]
61 where = [('name', operator, group_name)]
63 application_name = values[0]
64 group_name = values[1]
65 where = ['|',('category_id.name', operator, application_name)] + where
69 'name': fields.char('Name', size=64, required=True, translate=True),
70 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
71 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
72 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
73 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
74 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
75 'comment' : fields.text('Comment', size=250, translate=True),
76 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
77 'full_name': fields.function(_get_full_name, type='char', string='Group Name', fnct_search=_search_group),
81 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
84 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
85 # add explicit ordering if search is sorted on full_name
86 if order and order.startswith('full_name'):
87 ids = super(groups, self).search(cr, uid, args, context=context)
88 gs = self.browse(cr, uid, ids, context)
89 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
90 gs = gs[offset:offset+limit] if limit else gs[offset:]
92 return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
94 def copy(self, cr, uid, id, default=None, context=None):
95 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
96 default.update({'name': _('%s (copy)')%group_name})
97 return super(groups, self).copy(cr, uid, id, default, context)
99 def write(self, cr, uid, ids, vals, context=None):
101 if vals['name'].startswith('-'):
102 raise osv.except_osv(_('Error'),
103 _('The name of the group can not start with "-"'))
104 res = super(groups, self).write(cr, uid, ids, vals, context=context)
105 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
110 class res_users(osv.osv):
111 """ User class. A res.users record models an OpenERP user and is different
114 res.users class now inherits from res.partner. The partner model is
115 used to store the data related to the partner: lang, name, address,
116 avatar, ... The user model is now dedicated to technical data.
121 'res.partner': 'partner_id',
124 _description = 'Users'
126 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
128 # Do not update the password if no value is provided, ignore silently.
129 # For example web client submits False values for all empty fields.
132 # To change their own password users must use the client-specific change password wizard,
133 # so that the new password is immediately used for further RPC requests, otherwise the user
134 # will face unexpected 'Access Denied' exceptions.
135 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
136 self.write(cr, uid, id, {'password': value})
138 def _get_password(self, cr, uid, ids, arg, karg, context=None):
139 return dict.fromkeys(ids, '')
142 'id': fields.integer('ID'),
143 'login_date': fields.date('Latest connection', select=1),
144 'partner_id': fields.many2one('res.partner', required=True,
145 string='Related Partner', ondelete='cascade',
146 help='Partner-related data of the user'),
147 'login': fields.char('Login', size=64, required=True,
148 help="Used to log into the system"),
149 'password': fields.char('Password', size=64, invisible=True,
150 help="Keep empty if you don't want the user to be able to connect on the system."),
151 'new_password': fields.function(_get_password, type='char', size=64,
152 fnct_inv=_set_new_password, string='Set Password',
153 help="Specify a value only when creating a user or if you're "\
154 "changing the user's password, otherwise leave empty. After "\
155 "a change of password, the user has to login again."),
156 'signature': fields.text('Signature', size=64),
157 'active': fields.boolean('Active'),
158 '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."),
159 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
160 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
161 # Special behavior for this field: res.company.search() will only return the companies
162 # available to the current user (should be the user's companies?), when the user_preference
164 'company_id': fields.many2one('res.company', 'Company', required=True,
165 help='The company this user is currently working for.', context={'user_preference': True}),
166 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
167 # backward compatibility fields
168 'user_email': fields.related('email', type='char',
169 deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'),
172 def on_change_company_id(self, cr, uid, ids, company_id):
173 return {'warning' : {
174 'title': _("Company Switch Warning"),
175 '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)"),
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', '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 write(self, cr, uid, ids, values, context=None):
283 if not hasattr(ids, '__iter__'):
286 for key in values.keys():
287 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
290 if 'company_id' in values:
291 if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):
292 del values['company_id']
293 uid = 1 # safe fields only, so we write as super-user to bypass access rights
295 res = super(res_users, self).write(cr, uid, ids, values, context=context)
297 # clear caches linked to the users
298 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
299 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
302 if db in self._uid_cache:
304 if id in self._uid_cache[db]:
305 del self._uid_cache[db][id]
309 def unlink(self, cr, uid, ids, context=None):
311 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, ...)'))
313 if db in self._uid_cache:
315 if id in self._uid_cache[db]:
316 del self._uid_cache[db][id]
317 return super(res_users, self).unlink(cr, uid, ids, context=context)
319 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
326 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit, context=context)
328 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
329 return self.name_get(cr, user, ids, context=context)
331 def copy(self, cr, uid, id, default=None, context=None):
332 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
333 default = dict(default or {})
334 if ('name' not in default) and ('partner_id' not in default):
335 default['name'] = _("%s (copy)") % user2copy['name']
336 if 'login' not in default:
337 default['login'] = _("%s (copy)") % user2copy['login']
338 return super(res_users, self).copy(cr, uid, id, default, context)
340 def context_get(self, cr, uid, context=None):
341 user = self.browse(cr, SUPERUSER_ID, uid, context)
343 for k in self._all_columns.keys():
344 if k.startswith('context_'):
346 elif k in ['lang', 'tz', 'tz_offset']:
351 res = getattr(user,k) or False
352 if isinstance(res, browse_record):
354 result[context_key] = res or False
357 def action_get(self, cr, uid, context=None):
358 dataobj = self.pool.get('ir.model.data')
359 data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
360 return dataobj.browse(cr, uid, data_id, context=context).res_id
362 def check_super(self, passwd):
363 if passwd == tools.config['admin_passwd']:
366 raise openerp.exceptions.AccessDenied()
368 def check_credentials(self, cr, uid, password):
369 """ Override this method to plug additional authentication methods"""
370 res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
372 raise openerp.exceptions.AccessDenied()
374 def login(self, db, login, password):
378 cr = pooler.get_db(db).cursor()
380 # autocommit: our single update request will be performed atomically.
381 # (In this way, there is no opportunity to have two transactions
382 # interleaving their cr.execute()..cr.commit() calls and have one
383 # of them rolled back due to a concurrent access.)
385 # check if user exists
386 res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
390 self.check_credentials(cr, user_id, password)
391 # We effectively unconditionally write the res_users line.
392 # Even w/ autocommit there's a chance the user row will be locked,
393 # in which case we can't delay the login just for the purpose of
394 # update the last login date - hence we use FOR UPDATE NOWAIT to
395 # try to get the lock - fail-fast
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.
401 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
402 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
404 _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
405 except openerp.exceptions.AccessDenied:
406 _logger.info("Login failed for db:%s login:%s", db, login)
413 def authenticate(self, db, login, password, user_agent_env):
414 """Verifies and returns the user ID corresponding to the given
415 ``login`` and ``password`` combination, or False if there was
418 :param str db: the database on which user is trying to authenticate
419 :param str login: username
420 :param str password: user password
421 :param dict user_agent_env: environment dictionary describing any
422 relevant environment attributes
424 uid = self.login(db, login, password)
425 if uid == openerp.SUPERUSER_ID:
426 # Successfully logged in as admin!
427 # Attempt to guess the web base url...
428 if user_agent_env and user_agent_env.get('base_location'):
429 cr = pooler.get_db(db).cursor()
431 base = user_agent_env['base_location']
432 self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url', base)
435 _logger.exception("Failed to update web.base.url configuration parameter")
440 def check(self, db, uid, passwd):
441 """Verifies that the given (uid, password) is authorized for the database ``db`` and
442 raise an exception if it is not."""
444 # empty passwords disallowed for obvious security reasons
445 raise openerp.exceptions.AccessDenied()
446 if self._uid_cache.get(db, {}).get(uid) == passwd:
448 cr = pooler.get_db(db).cursor()
450 self.check_credentials(cr, uid, passwd)
451 if self._uid_cache.has_key(db):
452 self._uid_cache[db][uid] = passwd
454 self._uid_cache[db] = {uid:passwd}
458 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
459 """Change current user password. Old password must be provided explicitly
460 to prevent hijacking an existing user session, or for cases where the cleartext
461 password is not used to authenticate requests.
464 :raise: openerp.exceptions.AccessDenied when old password is wrong
465 :raise: except_osv when new password is not set or empty
467 self.check(cr.dbname, uid, old_passwd)
469 return self.write(cr, uid, uid, {'password': new_passwd})
470 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
472 def preference_save(self, cr, uid, ids, context=None):
474 'type': 'ir.actions.client',
478 def preference_change_password(self, cr, uid, ids, context=None):
480 'type': 'ir.actions.client',
481 'tag': 'change_password',
485 def has_group(self, cr, uid, group_ext_id):
486 """Checks whether user belongs to given group.
488 :param str group_ext_id: external ID (XML ID) of the group.
489 Must be provided in fully-qualified form (``module.ext_id``), as there
490 is no implicit module to use..
491 :return: True if the current user is a member of the group with the
492 given external ID (XML ID), else False.
494 assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
495 module, ext_id = group_ext_id.split('.')
496 cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
497 (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
498 (uid, module, ext_id))
499 return bool(cr.fetchone())
503 # Extension of res.groups and res.users with a relation for "implied" or
504 # "inherited" groups. Once a user belongs to a group, it automatically belongs
505 # to the implied groups (transitively).
509 """ A cset (constrained set) is a set of elements that may be constrained to
510 be a subset of other csets. Elements added to a cset are automatically
511 added to its supersets. Cycles in the subset constraints are supported.
513 def __init__(self, xs):
514 self.supersets = set()
515 self.elements = set(xs)
516 def subsetof(self, other):
517 if other is not self:
518 self.supersets.add(other)
519 other.update(self.elements)
520 def update(self, xs):
521 xs = set(xs) - self.elements
522 if xs: # xs will eventually be empty in case of a cycle
523 self.elements.update(xs)
524 for s in self.supersets:
527 return iter(self.elements)
530 """ return the concatenation of a list of iterables """
532 for l in ls: res.extend(l)
537 class groups_implied(osv.osv):
538 _inherit = 'res.groups'
540 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
541 "computes the transitive closure of relation implied_ids"
542 memo = {} # use a memo for performance and cycle avoidance
545 memo[g] = cset(g.implied_ids)
546 for h in g.implied_ids:
547 computed_set(h).subsetof(memo[g])
551 for g in self.browse(cr, SUPERUSER_ID, ids, context):
552 res[g.id] = map(int, computed_set(g))
556 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
557 string='Inherits', help='Users of this group automatically inherit those groups'),
558 'trans_implied_ids': fields.function(_get_trans_implied,
559 type='many2many', relation='res.groups', string='Transitively inherits'),
562 def create(self, cr, uid, values, context=None):
563 users = values.pop('users', None)
564 gid = super(groups_implied, self).create(cr, uid, values, context)
566 # delegate addition of users to add implied groups
567 self.write(cr, uid, [gid], {'users': users}, context)
570 def write(self, cr, uid, ids, values, context=None):
571 res = super(groups_implied, self).write(cr, uid, ids, values, context)
572 if values.get('users') or values.get('implied_ids'):
573 # add all implied groups (to all users of each group)
574 for g in self.browse(cr, uid, ids):
575 gids = map(int, g.trans_implied_ids)
576 vals = {'users': [(4, u.id) for u in g.users]}
577 super(groups_implied, self).write(cr, uid, gids, vals, context)
580 class users_implied(osv.osv):
581 _inherit = 'res.users'
583 def create(self, cr, uid, values, context=None):
584 groups = values.pop('groups_id', None)
585 user_id = super(users_implied, self).create(cr, uid, values, context)
587 # delegate addition of groups to add implied groups
588 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
591 def write(self, cr, uid, ids, values, context=None):
592 if not isinstance(ids,list):
594 res = super(users_implied, self).write(cr, uid, ids, values, context)
595 if values.get('groups_id'):
596 # add implied groups for all users
597 for user in self.browse(cr, uid, ids):
598 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
599 vals = {'groups_id': [(4, g.id) for g in gs]}
600 super(users_implied, self).write(cr, uid, [user.id], vals, context)
604 # Extension of res.groups and res.users for the special groups view in the users
605 # form. This extension presents groups with selection and boolean widgets:
606 # - Groups are shown by application, with boolean and/or selection fields.
607 # Selection fields typically defines a role "Name" for the given application.
608 # - Uncategorized groups are presented as boolean fields and grouped in a
611 # The user form view is modified by an inherited view (base.user_groups_view);
612 # the inherited view replaces the field 'groups_id' by a set of reified group
613 # fields (boolean or selection fields). The arch of that view is regenerated
614 # each time groups are changed.
616 # Naming conventions for reified groups fields:
617 # - boolean field 'in_group_ID' is True iff
618 # ID is in 'groups_id'
619 # - boolean field 'in_groups_ID1_..._IDk' is True iff
620 # any of ID1, ..., IDk is in 'groups_id'
621 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
622 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
624 def name_boolean_group(id): return 'in_group_' + str(id)
625 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
626 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
628 def is_boolean_group(name): return name.startswith('in_group_')
629 def is_boolean_groups(name): return name.startswith('in_groups_')
630 def is_selection_groups(name): return name.startswith('sel_groups_')
631 def is_reified_group(name):
632 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
634 def get_boolean_group(name): return int(name[9:])
635 def get_boolean_groups(name): return map(int, name[10:].split('_'))
636 def get_selection_groups(name): return map(int, name[11:].split('_'))
638 def partition(f, xs):
639 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
642 (yes if f(x) else nos).append(x)
647 class groups_view(osv.osv):
648 _inherit = 'res.groups'
650 def create(self, cr, uid, values, context=None):
651 res = super(groups_view, self).create(cr, uid, values, context)
652 self.update_user_groups_view(cr, uid, context)
655 def write(self, cr, uid, ids, values, context=None):
656 res = super(groups_view, self).write(cr, uid, ids, values, context)
657 self.update_user_groups_view(cr, uid, context)
660 def unlink(self, cr, uid, ids, context=None):
661 res = super(groups_view, self).unlink(cr, uid, ids, context)
662 self.update_user_groups_view(cr, uid, context)
665 def update_user_groups_view(self, cr, uid, context=None):
666 # the view with id 'base.user_groups_view' inherits the user form view,
667 # and introduces the reified group fields
668 view = self.get_user_groups_view(cr, uid, context)
671 xml1.append(E.separator(string=_('Application'), colspan="4"))
672 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
673 # hide groups in category 'Hidden' (except to group_no_one)
674 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
675 if kind == 'selection':
676 # application name with a selection field
677 field_name = name_selection_groups(map(int, gs))
678 xml1.append(E.field(name=field_name, **attrs))
679 xml1.append(E.newline())
681 # application separator with boolean fields
682 app_name = app and app.name or _('Other')
683 xml2.append(E.separator(string=app_name, colspan="4", **attrs))
685 field_name = name_boolean_group(g.id)
686 xml2.append(E.field(name=field_name, **attrs))
688 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
689 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
690 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
691 view.write({'arch': xml_content})
694 def get_user_groups_view(self, cr, uid, context=None):
696 view = self.pool.get('ir.model.data').get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
697 assert view and view._table_name == 'ir.ui.view'
702 def get_application_groups(self, cr, uid, domain=None, context=None):
703 return self.search(cr, uid, domain or [])
705 def get_groups_by_application(self, cr, uid, context=None):
706 """ return all groups classified by application (module category), as a list of pairs:
707 [(app, kind, [group, ...]), ...],
708 where app and group are browse records, and kind is either 'boolean' or 'selection'.
709 Applications are given in sequence order. If kind is 'selection', the groups are
710 given in reverse implication order.
714 # determine sequence order: a group should appear after its implied groups
715 order = dict.fromkeys(gs, 0)
717 for h in gs.intersection(g.trans_implied_ids):
719 # check whether order is total, i.e., sequence orders are distinct
720 if len(set(order.itervalues())) == len(gs):
721 return sorted(gs, key=lambda g: order[g])
724 # classify all groups by application
725 gids = self.get_application_groups(cr, uid, context=context)
726 by_app, others = {}, []
727 for g in self.browse(cr, uid, gids, context):
729 by_app.setdefault(g.category_id, []).append(g)
734 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
736 gs = linearized(by_app[app])
738 res.append((app, 'selection', gs))
740 res.append((app, 'boolean', by_app[app]))
742 res.append((False, 'boolean', others))
745 class users_view(osv.osv):
746 _inherit = 'res.users'
748 def create(self, cr, uid, values, context=None):
749 self._set_reified_groups(values)
750 return super(users_view, self).create(cr, uid, values, context)
752 def write(self, cr, uid, ids, values, context=None):
753 self._set_reified_groups(values)
754 return super(users_view, self).write(cr, uid, ids, values, context)
756 def _set_reified_groups(self, values):
757 """ reflect reified group fields in values['groups_id'] """
758 if 'groups_id' in values:
759 # groups are already given, ignore group fields
760 for f in filter(is_reified_group, values.iterkeys()):
765 for f in values.keys():
766 if is_boolean_group(f):
767 target = add if values.pop(f) else remove
768 target.append(get_boolean_group(f))
769 elif is_boolean_groups(f):
770 if not values.pop(f):
771 remove.extend(get_boolean_groups(f))
772 elif is_selection_groups(f):
773 remove.extend(get_selection_groups(f))
774 selected = values.pop(f)
777 # update values *only* if groups are being modified, otherwise
778 # we introduce spurious changes that might break the super.write() call.
780 # remove groups in 'remove' and add groups in 'add'
781 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
783 def default_get(self, cr, uid, fields, context=None):
784 group_fields, fields = partition(is_reified_group, fields)
785 fields1 = (fields + ['groups_id']) if group_fields else fields
786 values = super(users_view, self).default_get(cr, uid, fields1, context)
787 self._get_reified_groups(group_fields, values)
790 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
792 fields = self.fields_get(cr, uid, context=context).keys()
793 group_fields, fields = partition(is_reified_group, fields)
794 if not 'groups_id' in fields:
795 fields.append('groups_id')
796 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
797 for values in (res if isinstance(res, list) else [res]):
798 self._get_reified_groups(group_fields, values)
801 def _get_reified_groups(self, fields, values):
802 """ compute the given reified group fields from values['groups_id'] """
803 gids = set(values.get('groups_id') or [])
805 if is_boolean_group(f):
806 values[f] = get_boolean_group(f) in gids
807 elif is_boolean_groups(f):
808 values[f] = not gids.isdisjoint(get_boolean_groups(f))
809 elif is_selection_groups(f):
810 selected = [gid for gid in get_selection_groups(f) if gid in gids]
811 values[f] = selected and selected[-1] or False
813 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
814 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
815 # add reified groups fields
816 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
817 if kind == 'selection':
818 # selection group field
819 tips = ['%s: %s' % (g.name, g.comment) for g in gs if g.comment]
820 res[name_selection_groups(map(int, gs))] = {
822 'string': app and app.name or _('Other'),
823 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
824 'help': '\n'.join(tips),
827 # boolean group fields
829 res[name_boolean_group(g.id)] = {
836 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: