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-2014 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 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 #----------------------------------------------------------
38 # Basic res.groups and res.users
39 #----------------------------------------------------------
41 class res_groups(osv.osv):
43 _description = "Access Groups"
44 _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 'view_access': fields.many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', 'Views'),
76 'comment' : fields.text('Comment', size=250, translate=True),
77 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
78 'full_name': fields.function(_get_full_name, type='char', string='Group Name', fnct_search=_search_group),
82 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
85 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
86 # add explicit ordering if search is sorted on full_name
87 if order and order.startswith('full_name'):
88 ids = super(res_groups, self).search(cr, uid, args, context=context)
89 gs = self.browse(cr, uid, ids, context)
90 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
91 gs = gs[offset:offset+limit] if limit else gs[offset:]
93 return super(res_groups, self).search(cr, uid, args, offset, limit, order, context, count)
95 def copy(self, cr, uid, id, default=None, context=None):
96 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
97 default.update({'name': _('%s (copy)')%group_name})
98 return super(res_groups, self).copy(cr, uid, id, default, context)
100 def write(self, cr, uid, ids, vals, context=None):
102 if vals['name'].startswith('-'):
103 raise osv.except_osv(_('Error'),
104 _('The name of the group can not start with "-"'))
105 res = super(res_groups, self).write(cr, uid, ids, vals, context=context)
106 self.pool['ir.model.access'].call_cache_clearing_methods(cr)
109 class res_users(osv.osv):
110 """ User class. A res.users record models an OpenERP user and is different
113 res.users class now inherits from res.partner. The partner model is
114 used to store the data related to the partner: lang, name, address,
115 avatar, ... The user model is now dedicated to technical data.
120 'res.partner': 'partner_id',
123 _description = 'Users'
125 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
127 # Do not update the password if no value is provided, ignore silently.
128 # For example web client submits False values for all empty fields.
131 # To change their own password users must use the client-specific change password wizard,
132 # so that the new password is immediately used for further RPC requests, otherwise the user
133 # will face unexpected 'Access Denied' exceptions.
134 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
135 self.write(cr, uid, id, {'password': value})
137 def _get_password(self, cr, uid, ids, arg, karg, context=None):
138 return dict.fromkeys(ids, '')
141 'id': fields.integer('ID'),
142 'login_date': fields.date('Latest connection', select=1),
143 'partner_id': fields.many2one('res.partner', required=True,
144 string='Related Partner', ondelete='restrict',
145 help='Partner-related data of the user'),
146 'login': fields.char('Login', size=64, required=True,
147 help="Used to log into the system"),
148 'password': fields.char('Password', size=64, invisible=True,
149 help="Keep empty if you don't want the user to be able to connect on the system."),
150 'new_password': fields.function(_get_password, type='char', size=64,
151 fnct_inv=_set_new_password, string='Set Password',
152 help="Specify a value only when creating a user or if you're "\
153 "changing the user's password, otherwise leave empty. After "\
154 "a change of password, the user has to login again."),
155 'signature': fields.text('Signature'),
156 'active': fields.boolean('Active'),
157 '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."),
158 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
159 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
160 # Special behavior for this field: res.company.search() will only return the companies
161 # available to the current user (should be the user's companies?), when the user_preference
163 'company_id': fields.many2one('res.company', 'Company', required=True,
164 help='The company this user is currently working for.', context={'user_preference': True}),
165 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
166 # backward compatibility fields
167 'user_email': fields.related('email', type='char',
168 deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'),
171 def on_change_login(self, cr, uid, ids, login, context=None):
172 if login and tools.single_email_re.match(login):
173 return {'value': {'email': login}}
176 def onchange_state(self, cr, uid, ids, state_id, context=None):
177 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
178 return self.pool.get('res.partner').onchange_state(cr, uid, partner_ids, state_id, context=context)
180 def onchange_type(self, cr, uid, ids, is_company, context=None):
181 """ Wrapper on the user.partner onchange_type, because some calls to the
182 partner form view applied to the user may trigger the
183 partner.onchange_type method, but applied to the user object.
185 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
186 return self.pool['res.partner'].onchange_type(cr, uid, partner_ids, is_company, context=context)
188 def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
189 """ Wrapper on the user.partner onchange_address, because some calls to the
190 partner form view applied to the user may trigger the
191 partner.onchange_type method, but applied to the user object.
193 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
194 return self.pool['res.partner'].onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
196 def _check_company(self, cr, uid, ids, context=None):
197 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
200 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
204 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
207 def _get_company(self,cr, uid, context=None, uid2=False):
210 user = self.pool['res.users'].read(cr, uid, uid2, ['company_id'], context)
211 company_id = user.get('company_id', False)
212 return company_id and company_id[0] or False
214 def _get_companies(self, cr, uid, context=None):
215 c = self._get_company(cr, uid, context)
220 def _get_menu(self,cr, uid, context=None):
221 dataobj = self.pool.get('ir.model.data')
223 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
224 if model != 'ir.actions.act_window':
230 def _get_group(self,cr, uid, context=None):
231 dataobj = self.pool.get('ir.model.data')
234 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_user')
235 result.append(group_id)
236 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_partner_manager')
237 result.append(group_id)
239 # If these groups does not exists anymore
247 'menu_id': _get_menu,
248 'company_id': _get_company,
249 'company_ids': _get_companies,
250 'groups_id': _get_group,
251 'image': lambda self, cr, uid, ctx={}: self.pool['res.partner']._get_default_image(cr, uid, False, ctx, colorize=True),
254 # User can write on a few of his own fields (but not his groups for example)
255 SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz']
256 # User can read a few of his own fields
257 SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update']
259 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
260 def override_password(o):
261 if 'password' in o and ('id' not in o or o['id'] != uid):
262 o['password'] = '********'
265 if fields and (ids == [uid] or ids == uid):
267 if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')):
270 # safe fields only, so we read as super-user to bypass access rights
273 result = super(res_users, self).read(cr, uid, ids, fields=fields, context=context, load=load)
274 canwrite = self.pool['ir.model.access'].check(cr, uid, 'res.users', 'write', False)
276 if isinstance(ids, (int, long)):
277 result = override_password(result)
279 result = map(override_password, result)
283 def create(self, cr, uid, vals, context=None):
284 user_id = super(res_users, self).create(cr, uid, vals, context=context)
285 user = self.browse(cr, uid, user_id, context=context)
286 if user.partner_id.company_id:
287 user.partner_id.write({'company_id': user.company_id.id})
290 def write(self, cr, uid, ids, values, context=None):
291 if not hasattr(ids, '__iter__'):
294 for key in values.keys():
295 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
298 if 'company_id' in values:
299 if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):
300 del values['company_id']
301 uid = 1 # safe fields only, so we write as super-user to bypass access rights
303 res = super(res_users, self).write(cr, uid, ids, values, context=context)
304 if 'company_id' in values:
305 for user in self.browse(cr, uid, ids, context=context):
306 # if partner is global we keep it that way
307 if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']:
308 user.partner_id.write({'company_id': user.company_id.id})
309 # clear caches linked to the users
310 self.pool['ir.model.access'].call_cache_clearing_methods(cr)
311 clear = partial(self.pool['ir.rule'].clear_cache, cr)
314 if db in self._uid_cache:
316 if id in self._uid_cache[db]:
317 del self._uid_cache[db][id]
318 self.context_get.clear_cache(self)
321 def unlink(self, cr, uid, ids, context=None):
323 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, ...)'))
325 if db in self._uid_cache:
327 if id in self._uid_cache[db]:
328 del self._uid_cache[db][id]
329 return super(res_users, self).unlink(cr, uid, ids, context=context)
331 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
338 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit, context=context)
340 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
341 return self.name_get(cr, user, ids, context=context)
343 def copy(self, cr, uid, id, default=None, context=None):
344 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
345 default = dict(default or {})
346 if ('name' not in default) and ('partner_id' not in default):
347 default['name'] = _("%s (copy)") % user2copy['name']
348 if 'login' not in default:
349 default['login'] = _("%s (copy)") % user2copy['login']
350 return super(res_users, self).copy(cr, uid, id, default, context)
352 @tools.ormcache(skiparg=2)
353 def context_get(self, cr, uid, context=None):
354 user = self.browse(cr, SUPERUSER_ID, uid, context)
356 for k in self._all_columns.keys():
357 if k.startswith('context_'):
359 elif k in ['lang', 'tz']:
364 res = getattr(user,k) or False
365 if isinstance(res, browse_record):
367 result[context_key] = res or False
370 def action_get(self, cr, uid, context=None):
371 dataobj = self.pool['ir.model.data']
372 data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
373 return dataobj.browse(cr, uid, data_id, context=context).res_id
375 def check_super(self, passwd):
376 if passwd == tools.config['admin_passwd']:
379 raise openerp.exceptions.AccessDenied()
381 def check_credentials(self, cr, uid, password):
382 """ Override this method to plug additional authentication methods"""
383 res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
385 raise openerp.exceptions.AccessDenied()
387 def login(self, db, login, password):
391 cr = self.pool.db.cursor()
393 # autocommit: our single update request will be performed atomically.
394 # (In this way, there is no opportunity to have two transactions
395 # interleaving their cr.execute()..cr.commit() calls and have one
396 # of them rolled back due to a concurrent access.)
398 # check if user exists
399 res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
403 self.check_credentials(cr, user_id, password)
404 # We effectively unconditionally write the res_users line.
405 # Even w/ autocommit there's a chance the user row will be locked,
406 # in which case we can't delay the login just for the purpose of
407 # update the last login date - hence we use FOR UPDATE NOWAIT to
408 # try to get the lock - fail-fast
409 # Failing to acquire the lock on the res_users row probably means
410 # another request is holding it. No big deal, we don't want to
411 # prevent/delay login in that case. It will also have been logged
412 # as a SQL error, if anyone cares.
414 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
415 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
417 _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
418 except openerp.exceptions.AccessDenied:
419 _logger.info("Login failed for db:%s login:%s", db, login)
426 def authenticate(self, db, login, password, user_agent_env):
427 """Verifies and returns the user ID corresponding to the given
428 ``login`` and ``password`` combination, or False if there was
431 :param str db: the database on which user is trying to authenticate
432 :param str login: username
433 :param str password: user password
434 :param dict user_agent_env: environment dictionary describing any
435 relevant environment attributes
437 uid = self.login(db, login, password)
438 if uid == openerp.SUPERUSER_ID:
439 # Successfully logged in as admin!
440 # Attempt to guess the web base url...
441 if user_agent_env and user_agent_env.get('base_location'):
442 cr = self.pool.db.cursor()
444 base = user_agent_env['base_location']
445 ICP = self.pool['ir.config_parameter']
446 if not ICP.get_param(cr, uid, 'web.base.url.freeze'):
447 ICP.set_param(cr, uid, 'web.base.url', base)
450 _logger.exception("Failed to update web.base.url configuration parameter")
455 def check(self, db, uid, passwd):
456 """Verifies that the given (uid, password) is authorized for the database ``db`` and
457 raise an exception if it is not."""
459 # empty passwords disallowed for obvious security reasons
460 raise openerp.exceptions.AccessDenied()
461 if self._uid_cache.get(db, {}).get(uid) == passwd:
463 cr = self.pool.db.cursor()
465 self.check_credentials(cr, uid, passwd)
466 if self._uid_cache.has_key(db):
467 self._uid_cache[db][uid] = passwd
469 self._uid_cache[db] = {uid:passwd}
473 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
474 """Change current user password. Old password must be provided explicitly
475 to prevent hijacking an existing user session, or for cases where the cleartext
476 password is not used to authenticate requests.
479 :raise: openerp.exceptions.AccessDenied when old password is wrong
480 :raise: except_osv when new password is not set or empty
482 self.check(cr.dbname, uid, old_passwd)
484 return self.write(cr, uid, uid, {'password': new_passwd})
485 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
487 def preference_save(self, cr, uid, ids, context=None):
489 'type': 'ir.actions.client',
493 def preference_change_password(self, cr, uid, ids, context=None):
495 'type': 'ir.actions.client',
496 'tag': 'change_password',
500 def has_group(self, cr, uid, group_ext_id):
501 """Checks whether user belongs to given group.
503 :param str group_ext_id: external ID (XML ID) of the group.
504 Must be provided in fully-qualified form (``module.ext_id``), as there
505 is no implicit module to use..
506 :return: True if the current user is a member of the group with the
507 given external ID (XML ID), else False.
509 assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
510 module, ext_id = group_ext_id.split('.')
511 cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
512 (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
513 (uid, module, ext_id))
514 return bool(cr.fetchone())
516 #----------------------------------------------------------
519 # Extension of res.groups and res.users with a relation for "implied"
520 # or "inherited" groups. Once a user belongs to a group, it
521 # automatically belongs to the implied groups (transitively).
522 #----------------------------------------------------------
525 """ A cset (constrained set) is a set of elements that may be constrained to
526 be a subset of other csets. Elements added to a cset are automatically
527 added to its supersets. Cycles in the subset constraints are supported.
529 def __init__(self, xs):
530 self.supersets = set()
531 self.elements = set(xs)
532 def subsetof(self, other):
533 if other is not self:
534 self.supersets.add(other)
535 other.update(self.elements)
536 def update(self, xs):
537 xs = set(xs) - self.elements
538 if xs: # xs will eventually be empty in case of a cycle
539 self.elements.update(xs)
540 for s in self.supersets:
543 return iter(self.elements)
546 """ return the concatenation of a list of iterables """
548 for l in ls: res.extend(l)
552 class groups_implied(osv.osv):
553 _inherit = 'res.groups'
555 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
556 "computes the transitive closure of relation implied_ids"
557 memo = {} # use a memo for performance and cycle avoidance
560 memo[g] = cset(g.implied_ids)
561 for h in g.implied_ids:
562 computed_set(h).subsetof(memo[g])
566 for g in self.browse(cr, SUPERUSER_ID, ids, context):
567 res[g.id] = map(int, computed_set(g))
571 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
572 string='Inherits', help='Users of this group automatically inherit those groups'),
573 'trans_implied_ids': fields.function(_get_trans_implied,
574 type='many2many', relation='res.groups', string='Transitively inherits'),
577 def create(self, cr, uid, values, context=None):
578 users = values.pop('users', None)
579 gid = super(groups_implied, self).create(cr, uid, values, context)
581 # delegate addition of users to add implied groups
582 self.write(cr, uid, [gid], {'users': users}, context)
585 def write(self, cr, uid, ids, values, context=None):
586 res = super(groups_implied, self).write(cr, uid, ids, values, context)
587 if values.get('users') or values.get('implied_ids'):
588 # add all implied groups (to all users of each group)
589 for g in self.browse(cr, uid, ids):
590 gids = map(int, g.trans_implied_ids)
591 vals = {'users': [(4, u.id) for u in g.users]}
592 super(groups_implied, self).write(cr, uid, gids, vals, context)
595 class users_implied(osv.osv):
596 _inherit = 'res.users'
598 def create(self, cr, uid, values, context=None):
599 groups = values.pop('groups_id', None)
600 user_id = super(users_implied, self).create(cr, uid, values, context)
602 # delegate addition of groups to add implied groups
603 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
604 self.pool['ir.ui.view'].clear_cache()
607 def write(self, cr, uid, ids, values, context=None):
608 if not isinstance(ids,list):
610 res = super(users_implied, self).write(cr, uid, ids, values, context)
611 if values.get('groups_id'):
612 # add implied groups for all users
613 for user in self.browse(cr, uid, ids):
614 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
615 vals = {'groups_id': [(4, g.id) for g in gs]}
616 super(users_implied, self).write(cr, uid, [user.id], vals, context)
617 self.pool['ir.ui.view'].clear_cache()
620 #----------------------------------------------------------
621 # Vitrual checkbox and selection for res.user form view
623 # Extension of res.groups and res.users for the special groups view in the users
624 # form. This extension presents groups with selection and boolean widgets:
625 # - Groups are shown by application, with boolean and/or selection fields.
626 # Selection fields typically defines a role "Name" for the given application.
627 # - Uncategorized groups are presented as boolean fields and grouped in a
630 # The user form view is modified by an inherited view (base.user_groups_view);
631 # the inherited view replaces the field 'groups_id' by a set of reified group
632 # fields (boolean or selection fields). The arch of that view is regenerated
633 # each time groups are changed.
635 # Naming conventions for reified groups fields:
636 # - boolean field 'in_group_ID' is True iff
637 # ID is in 'groups_id'
638 # - boolean field 'in_groups_ID1_..._IDk' is True iff
639 # any of ID1, ..., IDk is in 'groups_id'
640 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
641 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
642 #----------------------------------------------------------
644 def name_boolean_group(id): return 'in_group_' + str(id)
645 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
646 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
648 def is_boolean_group(name): return name.startswith('in_group_')
649 def is_boolean_groups(name): return name.startswith('in_groups_')
650 def is_selection_groups(name): return name.startswith('sel_groups_')
651 def is_reified_group(name):
652 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
654 def get_boolean_group(name): return int(name[9:])
655 def get_boolean_groups(name): return map(int, name[10:].split('_'))
656 def get_selection_groups(name): return map(int, name[11:].split('_'))
658 def partition(f, xs):
659 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
662 (yes if f(x) else nos).append(x)
666 class groups_view(osv.osv):
667 _inherit = 'res.groups'
669 def create(self, cr, uid, values, context=None):
670 res = super(groups_view, self).create(cr, uid, values, context)
671 self.update_user_groups_view(cr, uid, context)
674 def write(self, cr, uid, ids, values, context=None):
675 res = super(groups_view, self).write(cr, uid, ids, values, context)
676 self.update_user_groups_view(cr, uid, context)
679 def unlink(self, cr, uid, ids, context=None):
680 res = super(groups_view, self).unlink(cr, uid, ids, context)
681 self.update_user_groups_view(cr, uid, context)
684 def update_user_groups_view(self, cr, uid, context=None):
685 # the view with id 'base.user_groups_view' inherits the user form view,
686 # and introduces the reified group fields
687 # we have to try-catch this, because at first init the view does not exist
688 # but we are already creating some basic groups
689 view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context)
690 if view and view.exists() and view._table_name == 'ir.ui.view':
692 xml1.append(E.separator(string=_('Application'), colspan="4"))
693 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
694 # hide groups in category 'Hidden' (except to group_no_one)
695 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
696 if kind == 'selection':
697 # application name with a selection field
698 field_name = name_selection_groups(map(int, gs))
699 xml1.append(E.field(name=field_name, **attrs))
700 xml1.append(E.newline())
702 # application separator with boolean fields
703 app_name = app and app.name or _('Other')
704 xml2.append(E.separator(string=app_name, colspan="4", **attrs))
706 field_name = name_boolean_group(g.id)
707 xml2.append(E.field(name=field_name, **attrs))
709 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
710 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
711 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
712 view.write({'arch': xml_content})
715 def get_application_groups(self, cr, uid, domain=None, context=None):
716 return self.search(cr, uid, domain or [])
718 def get_groups_by_application(self, cr, uid, context=None):
719 """ return all groups classified by application (module category), as a list of pairs:
720 [(app, kind, [group, ...]), ...],
721 where app and group are browse records, and kind is either 'boolean' or 'selection'.
722 Applications are given in sequence order. If kind is 'selection', the groups are
723 given in reverse implication order.
727 # determine sequence order: a group should appear after its implied groups
728 order = dict.fromkeys(gs, 0)
730 for h in gs.intersection(g.trans_implied_ids):
732 # check whether order is total, i.e., sequence orders are distinct
733 if len(set(order.itervalues())) == len(gs):
734 return sorted(gs, key=lambda g: order[g])
737 # classify all groups by application
738 gids = self.get_application_groups(cr, uid, context=context)
739 by_app, others = {}, []
740 for g in self.browse(cr, uid, gids, context):
742 by_app.setdefault(g.category_id, []).append(g)
747 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
749 gs = linearized(by_app[app])
751 res.append((app, 'selection', gs))
753 res.append((app, 'boolean', by_app[app]))
755 res.append((False, 'boolean', others))
758 class users_view(osv.osv):
759 _inherit = 'res.users'
761 def create(self, cr, uid, values, context=None):
762 self._set_reified_groups(values)
763 return super(users_view, self).create(cr, uid, values, context)
765 def write(self, cr, uid, ids, values, context=None):
766 self._set_reified_groups(values)
767 return super(users_view, self).write(cr, uid, ids, values, context)
769 def _set_reified_groups(self, values):
770 """ reflect reified group fields in values['groups_id'] """
771 if 'groups_id' in values:
772 # groups are already given, ignore group fields
773 for f in filter(is_reified_group, values.iterkeys()):
778 for f in values.keys():
779 if is_boolean_group(f):
780 target = add if values.pop(f) else remove
781 target.append(get_boolean_group(f))
782 elif is_boolean_groups(f):
783 if not values.pop(f):
784 remove.extend(get_boolean_groups(f))
785 elif is_selection_groups(f):
786 remove.extend(get_selection_groups(f))
787 selected = values.pop(f)
790 # update values *only* if groups are being modified, otherwise
791 # we introduce spurious changes that might break the super.write() call.
793 # remove groups in 'remove' and add groups in 'add'
794 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
796 def default_get(self, cr, uid, fields, context=None):
797 group_fields, fields = partition(is_reified_group, fields)
798 fields1 = (fields + ['groups_id']) if group_fields else fields
799 values = super(users_view, self).default_get(cr, uid, fields1, context)
800 self._get_reified_groups(group_fields, values)
802 # add "default_groups_ref" inside the context to set default value for group_id with xml values
803 if 'groups_id' in fields and isinstance(context.get("default_groups_ref"), list):
805 ir_model_data = self.pool.get('ir.model.data')
806 for group_xml_id in context["default_groups_ref"]:
807 group_split = group_xml_id.split('.')
808 if len(group_split) != 2:
809 raise osv.except_osv(_('Invalid context value'), _('Invalid context default_groups_ref value (model.name_id) : "%s"') % group_xml_id)
811 temp, group_id = ir_model_data.get_object_reference(cr, uid, group_split[0], group_split[1])
815 values['groups_id'] = groups
818 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
819 fields_get = fields if fields is not None else self.fields_get(cr, uid, context=context).keys()
820 group_fields, _ = partition(is_reified_group, fields_get)
822 inject_groups_id = group_fields and fields and 'groups_id' not in fields
824 fields.append('groups_id')
825 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
827 if res and group_fields:
828 for values in (res if isinstance(res, list) else [res]):
829 self._get_reified_groups(group_fields, values)
831 values.pop('groups_id', None)
834 def _get_reified_groups(self, fields, values):
835 """ compute the given reified group fields from values['groups_id'] """
836 gids = set(values.get('groups_id') or [])
838 if is_boolean_group(f):
839 values[f] = get_boolean_group(f) in gids
840 elif is_boolean_groups(f):
841 values[f] = not gids.isdisjoint(get_boolean_groups(f))
842 elif is_selection_groups(f):
843 selected = [gid for gid in get_selection_groups(f) if gid in gids]
844 values[f] = selected and selected[-1] or False
846 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
847 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
848 # add reified groups fields
849 for app, kind, gs in self.pool['res.groups'].get_groups_by_application(cr, uid, context):
850 if kind == 'selection':
851 # selection group field
852 tips = ['%s: %s' % (g.name, g.comment) for g in gs if g.comment]
853 res[name_selection_groups(map(int, gs))] = {
855 'string': app and app.name or _('Other'),
856 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
857 'help': '\n'.join(tips),
862 # boolean group fields
864 res[name_boolean_group(g.id)] = {
873 #----------------------------------------------------------
874 # change password wizard
875 #----------------------------------------------------------
877 class change_password_wizard(osv.TransientModel):
879 A wizard to manage the change of users' passwords
882 _name = "change.password.wizard"
883 _description = "Change Password Wizard"
885 'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'),
888 def default_get(self, cr, uid, fields, context=None):
891 user_ids = context.get('active_ids', [])
892 wiz_id = context.get('active_id', None)
894 users = self.pool.get('res.users').browse(cr, uid, user_ids, context=context)
899 'user_login': user.login,
901 return {'user_ids': res}
903 def change_password_button(self, cr, uid, id, context=None):
904 wizard = self.browse(cr, uid, id, context=context)[0]
905 need_reload = any(uid == user.user_id.id for user in wizard.user_ids)
906 line_ids = [user.id for user in wizard.user_ids]
908 self.pool.get('change.password.user').change_password_button(cr, uid, line_ids, context=context)
909 # don't keep temporary password copies in the database longer than necessary
910 self.pool.get('change.password.user').write(cr, uid, line_ids, {'new_passwd': False}, context=context)
914 'type': 'ir.actions.client',
918 return {'type': 'ir.actions.act_window_close'}
920 class change_password_user(osv.TransientModel):
922 A model to configure users in the change password wizard
925 _name = 'change.password.user'
926 _description = 'Change Password Wizard User'
928 'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True),
929 'user_id': fields.many2one('res.users', string='User', required=True),
930 'user_login': fields.char('User Login', readonly=True),
931 'new_passwd': fields.char('New Password'),
937 def change_password_button(self, cr, uid, ids, context=None):
938 for user in self.browse(cr, uid, ids, context=context):
939 self.pool.get('res.users').write(cr, uid, user.user_id.id, {'password': user.new_passwd})
942 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: