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, expression
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):
60 if isinstance(operand, bool):
61 domains = [[('name', operator, operand)], [('category_id.name', operator, operand)]]
62 if operator in expression.NEGATIVE_TERM_OPERATORS == (not operand):
63 return expression.AND(domains)
65 return expression.OR(domains)
66 if isinstance(operand, basestring):
71 values = filter(bool, group.split('/'))
72 group_name = values.pop().strip()
73 category_name = values and '/'.join(values).strip() or group_name
74 group_domain = [('name', operator, lst and [group_name] or group_name)]
75 category_domain = [('category_id.name', operator, lst and [category_name] or category_name)]
76 if operator in expression.NEGATIVE_TERM_OPERATORS and not values:
77 category_domain = expression.OR([category_domain, [('category_id', '=', False)]])
78 if (operator in expression.NEGATIVE_TERM_OPERATORS) == (not values):
79 sub_where = expression.AND([group_domain, category_domain])
81 sub_where = expression.OR([group_domain, category_domain])
82 if operator in expression.NEGATIVE_TERM_OPERATORS:
83 where = expression.AND([where, sub_where])
85 where = expression.OR([where, sub_where])
89 'name': fields.char('Name', size=64, required=True, translate=True),
90 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
91 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
92 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
93 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
94 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
95 'view_access': fields.many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', 'Views'),
96 'comment' : fields.text('Comment', size=250, translate=True),
97 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
98 'full_name': fields.function(_get_full_name, type='char', string='Group Name', fnct_search=_search_group),
102 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
105 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
106 # add explicit ordering if search is sorted on full_name
107 if order and order.startswith('full_name'):
108 ids = super(res_groups, self).search(cr, uid, args, context=context)
109 gs = self.browse(cr, uid, ids, context)
110 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
111 gs = gs[offset:offset+limit] if limit else gs[offset:]
113 return super(res_groups, self).search(cr, uid, args, offset, limit, order, context, count)
115 def copy(self, cr, uid, id, default=None, context=None):
116 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
117 default.update({'name': _('%s (copy)')%group_name})
118 return super(res_groups, self).copy(cr, uid, id, default, context)
120 def write(self, cr, uid, ids, vals, context=None):
122 if vals['name'].startswith('-'):
123 raise osv.except_osv(_('Error'),
124 _('The name of the group can not start with "-"'))
125 res = super(res_groups, self).write(cr, uid, ids, vals, context=context)
126 self.pool['ir.model.access'].call_cache_clearing_methods(cr)
129 class res_users(osv.osv):
130 """ User class. A res.users record models an OpenERP user and is different
133 res.users class now inherits from res.partner. The partner model is
134 used to store the data related to the partner: lang, name, address,
135 avatar, ... The user model is now dedicated to technical data.
140 'res.partner': 'partner_id',
143 _description = 'Users'
145 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
147 # Do not update the password if no value is provided, ignore silently.
148 # For example web client submits False values for all empty fields.
151 # To change their own password users must use the client-specific change password wizard,
152 # so that the new password is immediately used for further RPC requests, otherwise the user
153 # will face unexpected 'Access Denied' exceptions.
154 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
155 self.write(cr, uid, id, {'password': value})
157 def _get_password(self, cr, uid, ids, arg, karg, context=None):
158 return dict.fromkeys(ids, '')
161 'id': fields.integer('ID'),
162 'login_date': fields.date('Latest connection', select=1),
163 'partner_id': fields.many2one('res.partner', required=True,
164 string='Related Partner', ondelete='restrict',
165 help='Partner-related data of the user'),
166 'login': fields.char('Login', size=64, required=True,
167 help="Used to log into the system"),
168 'password': fields.char('Password', size=64, invisible=True,
169 help="Keep empty if you don't want the user to be able to connect on the system."),
170 'new_password': fields.function(_get_password, type='char', size=64,
171 fnct_inv=_set_new_password, string='Set Password',
172 help="Specify a value only when creating a user or if you're "\
173 "changing the user's password, otherwise leave empty. After "\
174 "a change of password, the user has to login again."),
175 'signature': fields.text('Signature'),
176 'active': fields.boolean('Active'),
177 '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."),
178 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
179 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
180 # Special behavior for this field: res.company.search() will only return the companies
181 # available to the current user (should be the user's companies?), when the user_preference
183 'company_id': fields.many2one('res.company', 'Company', required=True,
184 help='The company this user is currently working for.', context={'user_preference': True}),
185 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
186 # backward compatibility fields
187 'user_email': fields.related('email', type='char',
188 deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'),
191 def on_change_login(self, cr, uid, ids, login, context=None):
192 if login and tools.single_email_re.match(login):
193 return {'value': {'email': login}}
196 def onchange_state(self, cr, uid, ids, state_id, context=None):
197 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
198 return self.pool.get('res.partner').onchange_state(cr, uid, partner_ids, state_id, context=context)
200 def onchange_type(self, cr, uid, ids, is_company, context=None):
201 """ Wrapper on the user.partner onchange_type, because some calls to the
202 partner form view applied to the user may trigger the
203 partner.onchange_type method, but applied to the user object.
205 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
206 return self.pool['res.partner'].onchange_type(cr, uid, partner_ids, is_company, context=context)
208 def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
209 """ Wrapper on the user.partner onchange_address, because some calls to the
210 partner form view applied to the user may trigger the
211 partner.onchange_type method, but applied to the user object.
213 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
214 return self.pool['res.partner'].onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
216 def _check_company(self, cr, uid, ids, context=None):
217 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
220 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
224 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
227 def _get_company(self,cr, uid, context=None, uid2=False):
230 user = self.pool['res.users'].read(cr, uid, uid2, ['company_id'], context)
231 company_id = user.get('company_id', False)
232 return company_id and company_id[0] or False
234 def _get_companies(self, cr, uid, context=None):
235 c = self._get_company(cr, uid, context)
240 def _get_menu(self,cr, uid, context=None):
241 dataobj = self.pool.get('ir.model.data')
243 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
244 if model != 'ir.actions.act_window':
250 def _get_group(self,cr, uid, context=None):
251 dataobj = self.pool.get('ir.model.data')
254 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_user')
255 result.append(group_id)
256 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_partner_manager')
257 result.append(group_id)
259 # If these groups does not exists anymore
267 'menu_id': _get_menu,
268 'company_id': _get_company,
269 'company_ids': _get_companies,
270 'groups_id': _get_group,
271 'image': lambda self, cr, uid, ctx={}: self.pool['res.partner']._get_default_image(cr, uid, False, ctx, colorize=True),
274 # User can write on a few of his own fields (but not his groups for example)
275 SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz']
276 # User can read a few of his own fields
277 SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update']
279 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
280 def override_password(o):
281 if 'password' in o and ('id' not in o or o['id'] != uid):
282 o['password'] = '********'
285 if fields and (ids == [uid] or ids == uid):
287 if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')):
290 # safe fields only, so we read as super-user to bypass access rights
293 result = super(res_users, self).read(cr, uid, ids, fields=fields, context=context, load=load)
294 canwrite = self.pool['ir.model.access'].check(cr, uid, 'res.users', 'write', False)
296 if isinstance(ids, (int, long)):
297 result = override_password(result)
299 result = map(override_password, result)
303 def create(self, cr, uid, vals, context=None):
304 user_id = super(res_users, self).create(cr, uid, vals, context=context)
305 user = self.browse(cr, uid, user_id, context=context)
306 if user.partner_id.company_id:
307 user.partner_id.write({'company_id': user.company_id.id})
310 def write(self, cr, uid, ids, values, context=None):
311 if not hasattr(ids, '__iter__'):
314 for key in values.keys():
315 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
318 if 'company_id' in values:
319 if not (values['company_id'] in self.read(cr, SUPERUSER_ID, uid, ['company_ids'], context=context)['company_ids']):
320 del values['company_id']
321 uid = 1 # safe fields only, so we write as super-user to bypass access rights
323 res = super(res_users, self).write(cr, uid, ids, values, context=context)
324 if 'company_id' in values:
325 for user in self.browse(cr, uid, ids, context=context):
326 # if partner is global we keep it that way
327 if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']:
328 user.partner_id.write({'company_id': user.company_id.id})
329 # clear caches linked to the users
330 self.pool['ir.model.access'].call_cache_clearing_methods(cr)
331 clear = partial(self.pool['ir.rule'].clear_cache, cr)
334 if db in self._uid_cache:
336 if id in self._uid_cache[db]:
337 del self._uid_cache[db][id]
338 self.context_get.clear_cache(self)
341 def unlink(self, cr, uid, ids, context=None):
343 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, ...)'))
345 if db in self._uid_cache:
347 if id in self._uid_cache[db]:
348 del self._uid_cache[db][id]
349 return super(res_users, self).unlink(cr, uid, ids, context=context)
351 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
358 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit, context=context)
360 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
361 return self.name_get(cr, user, ids, context=context)
363 def copy(self, cr, uid, id, default=None, context=None):
364 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
365 default = dict(default or {})
366 if ('name' not in default) and ('partner_id' not in default):
367 default['name'] = _("%s (copy)") % user2copy['name']
368 if 'login' not in default:
369 default['login'] = _("%s (copy)") % user2copy['login']
370 return super(res_users, self).copy(cr, uid, id, default, context)
372 @tools.ormcache(skiparg=2)
373 def context_get(self, cr, uid, context=None):
374 user = self.browse(cr, SUPERUSER_ID, uid, context)
376 for k in self._all_columns.keys():
377 if k.startswith('context_'):
379 elif k in ['lang', 'tz']:
384 res = getattr(user,k) or False
385 if isinstance(res, browse_record):
387 result[context_key] = res or False
390 def action_get(self, cr, uid, context=None):
391 dataobj = self.pool['ir.model.data']
392 data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
393 return dataobj.browse(cr, uid, data_id, context=context).res_id
395 def check_super(self, passwd):
396 if passwd == tools.config['admin_passwd']:
399 raise openerp.exceptions.AccessDenied()
401 def check_credentials(self, cr, uid, password):
402 """ Override this method to plug additional authentication methods"""
403 res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
405 raise openerp.exceptions.AccessDenied()
407 def login(self, db, login, password):
411 cr = self.pool.db.cursor()
413 # autocommit: our single update request will be performed atomically.
414 # (In this way, there is no opportunity to have two transactions
415 # interleaving their cr.execute()..cr.commit() calls and have one
416 # of them rolled back due to a concurrent access.)
418 # check if user exists
419 res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
423 self.check_credentials(cr, user_id, password)
424 # We effectively unconditionally write the res_users line.
425 # Even w/ autocommit there's a chance the user row will be locked,
426 # in which case we can't delay the login just for the purpose of
427 # update the last login date - hence we use FOR UPDATE NOWAIT to
428 # try to get the lock - fail-fast
429 # Failing to acquire the lock on the res_users row probably means
430 # another request is holding it. No big deal, we don't want to
431 # prevent/delay login in that case. It will also have been logged
432 # as a SQL error, if anyone cares.
434 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
435 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
437 _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
438 except openerp.exceptions.AccessDenied:
439 _logger.info("Login failed for db:%s login:%s", db, login)
446 def authenticate(self, db, login, password, user_agent_env):
447 """Verifies and returns the user ID corresponding to the given
448 ``login`` and ``password`` combination, or False if there was
451 :param str db: the database on which user is trying to authenticate
452 :param str login: username
453 :param str password: user password
454 :param dict user_agent_env: environment dictionary describing any
455 relevant environment attributes
457 uid = self.login(db, login, password)
458 if uid == openerp.SUPERUSER_ID:
459 # Successfully logged in as admin!
460 # Attempt to guess the web base url...
461 if user_agent_env and user_agent_env.get('base_location'):
462 cr = self.pool.db.cursor()
464 base = user_agent_env['base_location']
465 ICP = self.pool['ir.config_parameter']
466 if not ICP.get_param(cr, uid, 'web.base.url.freeze'):
467 ICP.set_param(cr, uid, 'web.base.url', base)
470 _logger.exception("Failed to update web.base.url configuration parameter")
475 def check(self, db, uid, passwd):
476 """Verifies that the given (uid, password) is authorized for the database ``db`` and
477 raise an exception if it is not."""
479 # empty passwords disallowed for obvious security reasons
480 raise openerp.exceptions.AccessDenied()
481 if self._uid_cache.get(db, {}).get(uid) == passwd:
483 cr = self.pool.db.cursor()
485 self.check_credentials(cr, uid, passwd)
486 if self._uid_cache.has_key(db):
487 self._uid_cache[db][uid] = passwd
489 self._uid_cache[db] = {uid:passwd}
493 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
494 """Change current user password. Old password must be provided explicitly
495 to prevent hijacking an existing user session, or for cases where the cleartext
496 password is not used to authenticate requests.
499 :raise: openerp.exceptions.AccessDenied when old password is wrong
500 :raise: except_osv when new password is not set or empty
502 self.check(cr.dbname, uid, old_passwd)
504 return self.write(cr, uid, uid, {'password': new_passwd})
505 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
507 def preference_save(self, cr, uid, ids, context=None):
509 'type': 'ir.actions.client',
513 def preference_change_password(self, cr, uid, ids, context=None):
515 'type': 'ir.actions.client',
516 'tag': 'change_password',
520 def has_group(self, cr, uid, group_ext_id):
521 """Checks whether user belongs to given group.
523 :param str group_ext_id: external ID (XML ID) of the group.
524 Must be provided in fully-qualified form (``module.ext_id``), as there
525 is no implicit module to use..
526 :return: True if the current user is a member of the group with the
527 given external ID (XML ID), else False.
529 assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
530 module, ext_id = group_ext_id.split('.')
531 cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
532 (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
533 (uid, module, ext_id))
534 return bool(cr.fetchone())
536 #----------------------------------------------------------
539 # Extension of res.groups and res.users with a relation for "implied"
540 # or "inherited" groups. Once a user belongs to a group, it
541 # automatically belongs to the implied groups (transitively).
542 #----------------------------------------------------------
545 """ A cset (constrained set) is a set of elements that may be constrained to
546 be a subset of other csets. Elements added to a cset are automatically
547 added to its supersets. Cycles in the subset constraints are supported.
549 def __init__(self, xs):
550 self.supersets = set()
551 self.elements = set(xs)
552 def subsetof(self, other):
553 if other is not self:
554 self.supersets.add(other)
555 other.update(self.elements)
556 def update(self, xs):
557 xs = set(xs) - self.elements
558 if xs: # xs will eventually be empty in case of a cycle
559 self.elements.update(xs)
560 for s in self.supersets:
563 return iter(self.elements)
566 """ return the concatenation of a list of iterables """
568 for l in ls: res.extend(l)
572 class groups_implied(osv.osv):
573 _inherit = 'res.groups'
575 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
576 "computes the transitive closure of relation implied_ids"
577 memo = {} # use a memo for performance and cycle avoidance
580 memo[g] = cset(g.implied_ids)
581 for h in g.implied_ids:
582 computed_set(h).subsetof(memo[g])
586 for g in self.browse(cr, SUPERUSER_ID, ids, context):
587 res[g.id] = map(int, computed_set(g))
591 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
592 string='Inherits', help='Users of this group automatically inherit those groups'),
593 'trans_implied_ids': fields.function(_get_trans_implied,
594 type='many2many', relation='res.groups', string='Transitively inherits'),
597 def create(self, cr, uid, values, context=None):
598 users = values.pop('users', None)
599 gid = super(groups_implied, self).create(cr, uid, values, context)
601 # delegate addition of users to add implied groups
602 self.write(cr, uid, [gid], {'users': users}, context)
605 def write(self, cr, uid, ids, values, context=None):
606 res = super(groups_implied, self).write(cr, uid, ids, values, context)
607 if values.get('users') or values.get('implied_ids'):
608 # add all implied groups (to all users of each group)
609 for g in self.browse(cr, uid, ids):
610 gids = map(int, g.trans_implied_ids)
611 vals = {'users': [(4, u.id) for u in g.users]}
612 super(groups_implied, self).write(cr, uid, gids, vals, context)
615 class users_implied(osv.osv):
616 _inherit = 'res.users'
618 def create(self, cr, uid, values, context=None):
619 groups = values.pop('groups_id', None)
620 user_id = super(users_implied, self).create(cr, uid, values, context)
622 # delegate addition of groups to add implied groups
623 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
624 self.pool['ir.ui.view'].clear_cache()
627 def write(self, cr, uid, ids, values, context=None):
628 if not isinstance(ids,list):
630 res = super(users_implied, self).write(cr, uid, ids, values, context)
631 if values.get('groups_id'):
632 # add implied groups for all users
633 for user in self.browse(cr, uid, ids):
634 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
635 vals = {'groups_id': [(4, g.id) for g in gs]}
636 super(users_implied, self).write(cr, uid, [user.id], vals, context)
637 self.pool['ir.ui.view'].clear_cache()
640 #----------------------------------------------------------
641 # Vitrual checkbox and selection for res.user form view
643 # Extension of res.groups and res.users for the special groups view in the users
644 # form. This extension presents groups with selection and boolean widgets:
645 # - Groups are shown by application, with boolean and/or selection fields.
646 # Selection fields typically defines a role "Name" for the given application.
647 # - Uncategorized groups are presented as boolean fields and grouped in a
650 # The user form view is modified by an inherited view (base.user_groups_view);
651 # the inherited view replaces the field 'groups_id' by a set of reified group
652 # fields (boolean or selection fields). The arch of that view is regenerated
653 # each time groups are changed.
655 # Naming conventions for reified groups fields:
656 # - boolean field 'in_group_ID' is True iff
657 # ID is in 'groups_id'
658 # - boolean field 'in_groups_ID1_..._IDk' is True iff
659 # any of ID1, ..., IDk is in 'groups_id'
660 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
661 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
662 #----------------------------------------------------------
664 def name_boolean_group(id): return 'in_group_' + str(id)
665 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
666 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
668 def is_boolean_group(name): return name.startswith('in_group_')
669 def is_boolean_groups(name): return name.startswith('in_groups_')
670 def is_selection_groups(name): return name.startswith('sel_groups_')
671 def is_reified_group(name):
672 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
674 def get_boolean_group(name): return int(name[9:])
675 def get_boolean_groups(name): return map(int, name[10:].split('_'))
676 def get_selection_groups(name): return map(int, name[11:].split('_'))
678 def partition(f, xs):
679 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
682 (yes if f(x) else nos).append(x)
686 class groups_view(osv.osv):
687 _inherit = 'res.groups'
689 def create(self, cr, uid, values, context=None):
690 res = super(groups_view, self).create(cr, uid, values, context)
691 self.update_user_groups_view(cr, uid, context)
694 def write(self, cr, uid, ids, values, context=None):
695 res = super(groups_view, self).write(cr, uid, ids, values, context)
696 self.update_user_groups_view(cr, uid, context)
699 def unlink(self, cr, uid, ids, context=None):
700 res = super(groups_view, self).unlink(cr, uid, ids, context)
701 self.update_user_groups_view(cr, uid, context)
704 def update_user_groups_view(self, cr, uid, context=None):
705 # the view with id 'base.user_groups_view' inherits the user form view,
706 # and introduces the reified group fields
707 # we have to try-catch this, because at first init the view does not exist
708 # but we are already creating some basic groups
709 view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context)
710 if view and view.exists() and view._table_name == 'ir.ui.view':
712 xml1.append(E.separator(string=_('Application'), colspan="4"))
713 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
714 # hide groups in category 'Hidden' (except to group_no_one)
715 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
716 if kind == 'selection':
717 # application name with a selection field
718 field_name = name_selection_groups(map(int, gs))
719 xml1.append(E.field(name=field_name, **attrs))
720 xml1.append(E.newline())
722 # application separator with boolean fields
723 app_name = app and app.name or _('Other')
724 xml2.append(E.separator(string=app_name, colspan="4", **attrs))
726 field_name = name_boolean_group(g.id)
727 xml2.append(E.field(name=field_name, **attrs))
729 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
730 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
731 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
732 view.write({'arch': xml_content})
735 def get_application_groups(self, cr, uid, domain=None, context=None):
736 return self.search(cr, uid, domain or [])
738 def get_groups_by_application(self, cr, uid, context=None):
739 """ return all groups classified by application (module category), as a list of pairs:
740 [(app, kind, [group, ...]), ...],
741 where app and group are browse records, and kind is either 'boolean' or 'selection'.
742 Applications are given in sequence order. If kind is 'selection', the groups are
743 given in reverse implication order.
747 # determine sequence order: a group should appear after its implied groups
748 order = dict.fromkeys(gs, 0)
750 for h in gs.intersection(g.trans_implied_ids):
752 # check whether order is total, i.e., sequence orders are distinct
753 if len(set(order.itervalues())) == len(gs):
754 return sorted(gs, key=lambda g: order[g])
757 # classify all groups by application
758 gids = self.get_application_groups(cr, uid, context=context)
759 by_app, others = {}, []
760 for g in self.browse(cr, uid, gids, context):
762 by_app.setdefault(g.category_id, []).append(g)
767 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
769 gs = linearized(by_app[app])
771 res.append((app, 'selection', gs))
773 res.append((app, 'boolean', by_app[app]))
775 res.append((False, 'boolean', others))
778 class users_view(osv.osv):
779 _inherit = 'res.users'
781 def create(self, cr, uid, values, context=None):
782 self._set_reified_groups(values)
783 return super(users_view, self).create(cr, uid, values, context)
785 def write(self, cr, uid, ids, values, context=None):
786 self._set_reified_groups(values)
787 return super(users_view, self).write(cr, uid, ids, values, context)
789 def _set_reified_groups(self, values):
790 """ reflect reified group fields in values['groups_id'] """
791 if 'groups_id' in values:
792 # groups are already given, ignore group fields
793 for f in filter(is_reified_group, values.iterkeys()):
798 for f in values.keys():
799 if is_boolean_group(f):
800 target = add if values.pop(f) else remove
801 target.append(get_boolean_group(f))
802 elif is_boolean_groups(f):
803 if not values.pop(f):
804 remove.extend(get_boolean_groups(f))
805 elif is_selection_groups(f):
806 remove.extend(get_selection_groups(f))
807 selected = values.pop(f)
810 # update values *only* if groups are being modified, otherwise
811 # we introduce spurious changes that might break the super.write() call.
813 # remove groups in 'remove' and add groups in 'add'
814 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
816 def default_get(self, cr, uid, fields, context=None):
817 group_fields, fields = partition(is_reified_group, fields)
818 fields1 = (fields + ['groups_id']) if group_fields else fields
819 values = super(users_view, self).default_get(cr, uid, fields1, context)
820 self._get_reified_groups(group_fields, values)
822 # add "default_groups_ref" inside the context to set default value for group_id with xml values
823 if 'groups_id' in fields and isinstance(context.get("default_groups_ref"), list):
825 ir_model_data = self.pool.get('ir.model.data')
826 for group_xml_id in context["default_groups_ref"]:
827 group_split = group_xml_id.split('.')
828 if len(group_split) != 2:
829 raise osv.except_osv(_('Invalid context value'), _('Invalid context default_groups_ref value (model.name_id) : "%s"') % group_xml_id)
831 temp, group_id = ir_model_data.get_object_reference(cr, uid, group_split[0], group_split[1])
835 values['groups_id'] = groups
838 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
839 fields_get = fields if fields is not None else self.fields_get(cr, uid, context=context).keys()
840 group_fields, _ = partition(is_reified_group, fields_get)
842 inject_groups_id = group_fields and fields and 'groups_id' not in fields
844 fields.append('groups_id')
845 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
847 if res and group_fields:
848 for values in (res if isinstance(res, list) else [res]):
849 self._get_reified_groups(group_fields, values)
851 values.pop('groups_id', None)
854 def _get_reified_groups(self, fields, values):
855 """ compute the given reified group fields from values['groups_id'] """
856 gids = set(values.get('groups_id') or [])
858 if is_boolean_group(f):
859 values[f] = get_boolean_group(f) in gids
860 elif is_boolean_groups(f):
861 values[f] = not gids.isdisjoint(get_boolean_groups(f))
862 elif is_selection_groups(f):
863 selected = [gid for gid in get_selection_groups(f) if gid in gids]
864 values[f] = selected and selected[-1] or False
866 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
867 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
868 # add reified groups fields
869 for app, kind, gs in self.pool['res.groups'].get_groups_by_application(cr, uid, context):
870 if kind == 'selection':
871 # selection group field
872 tips = ['%s: %s' % (g.name, g.comment) for g in gs if g.comment]
873 res[name_selection_groups(map(int, gs))] = {
875 'string': app and app.name or _('Other'),
876 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
877 'help': '\n'.join(tips),
882 # boolean group fields
884 res[name_boolean_group(g.id)] = {
893 #----------------------------------------------------------
894 # change password wizard
895 #----------------------------------------------------------
897 class change_password_wizard(osv.TransientModel):
899 A wizard to manage the change of users' passwords
902 _name = "change.password.wizard"
903 _description = "Change Password Wizard"
905 'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'),
908 def default_get(self, cr, uid, fields, context=None):
911 user_ids = context.get('active_ids', [])
912 wiz_id = context.get('active_id', None)
914 users = self.pool.get('res.users').browse(cr, uid, user_ids, context=context)
919 'user_login': user.login,
921 return {'user_ids': res}
923 def change_password_button(self, cr, uid, id, context=None):
924 wizard = self.browse(cr, uid, id, context=context)[0]
925 need_reload = any(uid == user.user_id.id for user in wizard.user_ids)
926 line_ids = [user.id for user in wizard.user_ids]
928 self.pool.get('change.password.user').change_password_button(cr, uid, line_ids, context=context)
929 # don't keep temporary password copies in the database longer than necessary
930 self.pool.get('change.password.user').write(cr, uid, line_ids, {'new_passwd': False}, context=context)
934 'type': 'ir.actions.client',
938 return {'type': 'ir.actions.act_window_close'}
940 class change_password_user(osv.TransientModel):
942 A model to configure users in the change password wizard
945 _name = 'change.password.user'
946 _description = 'Change Password Wizard User'
948 'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True),
949 'user_id': fields.many2one('res.users', string='User', required=True),
950 'user_login': fields.char('User Login', readonly=True),
951 'new_passwd': fields.char('New Password'),
957 def change_password_button(self, cr, uid, ids, context=None):
958 for user in self.browse(cr, uid, ids, context=context):
959 self.pool.get('res.users').write(cr, uid, user.user_id.id, {'password': user.new_passwd})
962 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: