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 ##############################################################################
24 from functools import partial
26 from lxml import etree
27 from lxml.builder import E
30 from openerp import SUPERUSER_ID, models
31 from openerp import tools
32 import openerp.exceptions
33 from openerp.osv import fields, osv, expression
34 from openerp.tools.translate import _
35 from openerp.http import request
37 _logger = logging.getLogger(__name__)
39 #----------------------------------------------------------
40 # Basic res.groups and res.users
41 #----------------------------------------------------------
43 class res_groups(osv.osv):
45 _description = "Access Groups"
46 _rec_name = 'full_name'
49 def _get_full_name(self, cr, uid, ids, field, arg, context=None):
51 for g in self.browse(cr, uid, ids, context):
53 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
58 def _search_group(self, cr, uid, obj, name, args, context=None):
62 if isinstance(operand, bool):
63 domains = [[('name', operator, operand)], [('category_id.name', operator, operand)]]
64 if operator in expression.NEGATIVE_TERM_OPERATORS == (not operand):
65 return expression.AND(domains)
67 return expression.OR(domains)
68 if isinstance(operand, basestring):
73 values = filter(bool, group.split('/'))
74 group_name = values.pop().strip()
75 category_name = values and '/'.join(values).strip() or group_name
76 group_domain = [('name', operator, lst and [group_name] or group_name)]
77 category_domain = [('category_id.name', operator, lst and [category_name] or category_name)]
78 if operator in expression.NEGATIVE_TERM_OPERATORS and not values:
79 category_domain = expression.OR([category_domain, [('category_id', '=', False)]])
80 if (operator in expression.NEGATIVE_TERM_OPERATORS) == (not values):
81 sub_where = expression.AND([group_domain, category_domain])
83 sub_where = expression.OR([group_domain, category_domain])
84 if operator in expression.NEGATIVE_TERM_OPERATORS:
85 where = expression.AND([where, sub_where])
87 where = expression.OR([where, sub_where])
91 'name': fields.char('Name', required=True, translate=True),
92 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
93 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
94 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
95 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
96 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
97 'view_access': fields.many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', 'Views'),
98 'comment' : fields.text('Comment', size=250, translate=True),
99 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
100 'full_name': fields.function(_get_full_name, type='char', string='Group Name', fnct_search=_search_group),
104 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique within an application!')
107 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
108 # add explicit ordering if search is sorted on full_name
109 if order and order.startswith('full_name'):
110 ids = super(res_groups, self).search(cr, uid, args, context=context)
111 gs = self.browse(cr, uid, ids, context)
112 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
113 gs = gs[offset:offset+limit] if limit else gs[offset:]
115 return super(res_groups, self).search(cr, uid, args, offset, limit, order, context, count)
117 def copy(self, cr, uid, id, default=None, context=None):
118 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
119 default.update({'name': _('%s (copy)')%group_name})
120 return super(res_groups, self).copy(cr, uid, id, default, context)
122 def write(self, cr, uid, ids, vals, context=None):
124 if vals['name'].startswith('-'):
125 raise osv.except_osv(_('Error'),
126 _('The name of the group can not start with "-"'))
127 res = super(res_groups, self).write(cr, uid, ids, vals, context=context)
128 self.pool['ir.model.access'].call_cache_clearing_methods(cr)
129 self.pool['res.users'].has_group.clear_cache(self.pool['res.users'])
132 class res_users(osv.osv):
133 """ User class. A res.users record models an OpenERP user and is different
136 res.users class now inherits from res.partner. The partner model is
137 used to store the data related to the partner: lang, name, address,
138 avatar, ... The user model is now dedicated to technical data.
143 'res.partner': 'partner_id',
146 _description = 'Users'
148 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
150 # Do not update the password if no value is provided, ignore silently.
151 # For example web client submits False values for all empty fields.
154 # To change their own password users must use the client-specific change password wizard,
155 # so that the new password is immediately used for further RPC requests, otherwise the user
156 # will face unexpected 'Access Denied' exceptions.
157 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
158 self.write(cr, uid, id, {'password': value})
160 def _get_password(self, cr, uid, ids, arg, karg, context=None):
161 return dict.fromkeys(ids, '')
164 'id': fields.integer('ID'),
165 'login_date': fields.date('Latest connection', select=1),
166 'partner_id': fields.many2one('res.partner', required=True,
167 string='Related Partner', ondelete='restrict',
168 help='Partner-related data of the user'),
169 'login': fields.char('Login', size=64, required=True,
170 help="Used to log into the system"),
171 'password': fields.char('Password', size=64, invisible=True, copy=False,
172 help="Keep empty if you don't want the user to be able to connect on the system."),
173 'new_password': fields.function(_get_password, type='char', size=64,
174 fnct_inv=_set_new_password, string='Set Password',
175 help="Specify a value only when creating a user or if you're "\
176 "changing the user's password, otherwise leave empty. After "\
177 "a change of password, the user has to login again."),
178 'signature': fields.html('Signature'),
179 'active': fields.boolean('Active'),
180 'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at log on for this user, in addition to the standard menu."),
181 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
182 # Special behavior for this field: res.company.search() will only return the companies
183 # available to the current user (should be the user's companies?), when the user_preference
185 'company_id': fields.many2one('res.company', 'Company', required=True,
186 help='The company this user is currently working for.', context={'user_preference': True}),
187 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
190 def on_change_login(self, cr, uid, ids, login, context=None):
191 if login and tools.single_email_re.match(login):
192 return {'value': {'email': login}}
195 def onchange_state(self, cr, uid, ids, state_id, context=None):
196 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
197 return self.pool.get('res.partner').onchange_state(cr, uid, partner_ids, state_id, context=context)
199 def onchange_type(self, cr, uid, ids, is_company, context=None):
200 """ Wrapper on the user.partner onchange_type, because some calls to the
201 partner form view applied to the user may trigger the
202 partner.onchange_type method, but applied to the user object.
204 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
205 return self.pool['res.partner'].onchange_type(cr, uid, partner_ids, is_company, context=context)
207 def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
208 """ Wrapper on the user.partner onchange_address, because some calls to the
209 partner form view applied to the user may trigger the
210 partner.onchange_type method, but applied to the user object.
212 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
213 return self.pool['res.partner'].onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
215 def _check_company(self, cr, uid, ids, context=None):
216 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
219 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
223 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
226 def _get_company(self,cr, uid, context=None, uid2=False):
229 # Use read() to compute default company, and pass load=_classic_write to
230 # avoid useless name_get() calls. This will avoid prefetching fields
231 # while computing default values for new db columns, as the
232 # db backend may not be fully initialized yet.
233 user_data = self.pool['res.users'].read(cr, uid, uid2, ['company_id'],
234 context=context, load='_classic_write')
235 comp_id = user_data['company_id']
236 return comp_id or False
238 def _get_companies(self, cr, uid, context=None):
239 c = self._get_company(cr, uid, context)
244 def _get_group(self,cr, uid, context=None):
245 dataobj = self.pool.get('ir.model.data')
248 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_user')
249 result.append(group_id)
250 dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_partner_manager')
251 result.append(group_id)
253 # If these groups does not exists anymore
257 def _get_default_image(self, cr, uid, context=None):
258 return self.pool['res.partner']._get_default_image(cr, uid, False, colorize=True, context=context)
264 'company_id': _get_company,
265 'company_ids': _get_companies,
266 'groups_id': _get_group,
267 'image': _get_default_image,
270 # User can write on a few of his own fields (but not his groups for example)
271 SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz']
272 # User can read a few of his own fields
273 SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update']
275 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
276 def override_password(o):
277 if 'password' in o and ('id' not in o or o['id'] != uid):
278 o['password'] = '********'
281 if fields and (ids == [uid] or ids == uid):
283 if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')):
286 # safe fields only, so we read as super-user to bypass access rights
289 result = super(res_users, self).read(cr, uid, ids, fields=fields, context=context, load=load)
290 canwrite = self.pool['ir.model.access'].check(cr, uid, 'res.users', 'write', False)
292 if isinstance(ids, (int, long)):
293 result = override_password(result)
295 result = map(override_password, result)
299 def create(self, cr, uid, vals, context=None):
300 user_id = super(res_users, self).create(cr, uid, vals, context=context)
301 user = self.browse(cr, uid, user_id, context=context)
302 if user.partner_id.company_id:
303 user.partner_id.write({'company_id': user.company_id.id})
306 def write(self, cr, uid, ids, values, context=None):
307 if not hasattr(ids, '__iter__'):
310 for key in values.keys():
311 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
314 if 'company_id' in values:
315 user = self.browse(cr, SUPERUSER_ID, uid, context=context)
316 if not (values['company_id'] in user.company_ids.ids):
317 del values['company_id']
318 uid = 1 # safe fields only, so we write as super-user to bypass access rights
320 res = super(res_users, self).write(cr, uid, ids, values, context=context)
321 if 'company_id' in values:
322 for user in self.browse(cr, uid, ids, context=context):
323 # if partner is global we keep it that way
324 if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']:
325 user.partner_id.write({'company_id': user.company_id.id})
326 # clear caches linked to the users
327 self.pool['ir.model.access'].call_cache_clearing_methods(cr)
328 clear = partial(self.pool['ir.rule'].clear_cache, cr)
331 if db in self._uid_cache:
333 if id in self._uid_cache[db]:
334 del self._uid_cache[db][id]
335 self.context_get.clear_cache(self)
336 self.has_group.clear_cache(self)
339 def unlink(self, cr, uid, ids, context=None):
341 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 Odoo (updates, module installation, ...)'))
343 if db in self._uid_cache:
345 if id in self._uid_cache[db]:
346 del self._uid_cache[db][id]
347 return super(res_users, self).unlink(cr, uid, ids, context=context)
349 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
356 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit, context=context)
358 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
359 return self.name_get(cr, user, ids, context=context)
361 def copy(self, cr, uid, id, default=None, context=None):
362 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
363 default = dict(default or {})
364 if ('name' not in default) and ('partner_id' not in default):
365 default['name'] = _("%s (copy)") % user2copy['name']
366 if 'login' not in default:
367 default['login'] = _("%s (copy)") % user2copy['login']
368 return super(res_users, self).copy(cr, uid, id, default, context)
370 @tools.ormcache(skiparg=2)
371 def context_get(self, cr, uid, context=None):
372 user = self.browse(cr, SUPERUSER_ID, uid, context)
374 for k in self._all_columns.keys():
375 if k.startswith('context_'):
377 elif k in ['lang', 'tz']:
382 res = getattr(user, k) or False
383 if isinstance(res, models.BaseModel):
385 result[context_key] = res or False
388 def action_get(self, cr, uid, context=None):
389 dataobj = self.pool['ir.model.data']
390 data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
391 return dataobj.browse(cr, uid, data_id, context=context).res_id
393 def check_super(self, passwd):
394 if passwd == tools.config['admin_passwd']:
397 raise openerp.exceptions.AccessDenied()
399 def check_credentials(self, cr, uid, password):
400 """ Override this method to plug additional authentication methods"""
401 res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
403 raise openerp.exceptions.AccessDenied()
405 def _login(self, db, login, password):
409 cr = self.pool.cursor()
411 # autocommit: our single update request will be performed atomically.
412 # (In this way, there is no opportunity to have two transactions
413 # interleaving their cr.execute()..cr.commit() calls and have one
414 # of them rolled back due to a concurrent access.)
416 # check if user exists
417 res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
421 self.check_credentials(cr, user_id, password)
422 # We effectively unconditionally write the res_users line.
423 # Even w/ autocommit there's a chance the user row will be locked,
424 # in which case we can't delay the login just for the purpose of
425 # update the last login date - hence we use FOR UPDATE NOWAIT to
426 # try to get the lock - fail-fast
427 # Failing to acquire the lock on the res_users row probably means
428 # another request is holding it. No big deal, we don't want to
429 # prevent/delay login in that case. It will also have been logged
430 # as a SQL error, if anyone cares.
432 cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
433 cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
434 self.invalidate_cache(cr, user_id, ['login_date'], [user_id])
436 _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
437 except openerp.exceptions.AccessDenied:
438 _logger.info("Login failed for db:%s login:%s", db, login)
445 def authenticate(self, db, login, password, user_agent_env):
446 """Verifies and returns the user ID corresponding to the given
447 ``login`` and ``password`` combination, or False if there was
450 :param str db: the database on which user is trying to authenticate
451 :param str login: username
452 :param str password: user password
453 :param dict user_agent_env: environment dictionary describing any
454 relevant environment attributes
456 uid = self._login(db, login, password)
457 if uid == openerp.SUPERUSER_ID:
458 # Successfully logged in as admin!
459 # Attempt to guess the web base url...
460 if user_agent_env and user_agent_env.get('base_location'):
461 cr = self.pool.cursor()
463 base = user_agent_env['base_location']
464 ICP = self.pool['ir.config_parameter']
465 if not ICP.get_param(cr, uid, 'web.base.url.freeze'):
466 ICP.set_param(cr, uid, 'web.base.url', base)
469 _logger.exception("Failed to update web.base.url configuration parameter")
474 def check(self, db, uid, passwd):
475 """Verifies that the given (uid, password) is authorized for the database ``db`` and
476 raise an exception if it is not."""
478 # empty passwords disallowed for obvious security reasons
479 raise openerp.exceptions.AccessDenied()
480 if self._uid_cache.get(db, {}).get(uid) == passwd:
482 cr = self.pool.cursor()
484 self.check_credentials(cr, uid, passwd)
485 if self._uid_cache.has_key(db):
486 self._uid_cache[db][uid] = passwd
488 self._uid_cache[db] = {uid:passwd}
492 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
493 """Change current user password. Old password must be provided explicitly
494 to prevent hijacking an existing user session, or for cases where the cleartext
495 password is not used to authenticate requests.
498 :raise: openerp.exceptions.AccessDenied when old password is wrong
499 :raise: except_osv when new password is not set or empty
501 self.check(cr.dbname, uid, old_passwd)
503 return self.write(cr, uid, uid, {'password': new_passwd})
504 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
506 def preference_save(self, cr, uid, ids, context=None):
508 'type': 'ir.actions.client',
509 'tag': 'reload_context',
512 def preference_change_password(self, cr, uid, ids, context=None):
514 'type': 'ir.actions.client',
515 'tag': 'change_password',
519 @tools.ormcache(skiparg=2)
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)
565 concat = itertools.chain.from_iterable
567 class groups_implied(osv.osv):
568 _inherit = 'res.groups'
570 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
571 "computes the transitive closure of relation implied_ids"
572 memo = {} # use a memo for performance and cycle avoidance
575 memo[g] = cset(g.implied_ids)
576 for h in g.implied_ids:
577 computed_set(h).subsetof(memo[g])
581 for g in self.browse(cr, SUPERUSER_ID, ids, context):
582 res[g.id] = map(int, computed_set(g))
586 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
587 string='Inherits', help='Users of this group automatically inherit those groups'),
588 'trans_implied_ids': fields.function(_get_trans_implied,
589 type='many2many', relation='res.groups', string='Transitively inherits'),
592 def create(self, cr, uid, values, context=None):
593 users = values.pop('users', None)
594 gid = super(groups_implied, self).create(cr, uid, values, context)
596 # delegate addition of users to add implied groups
597 self.write(cr, uid, [gid], {'users': users}, context)
600 def write(self, cr, uid, ids, values, context=None):
601 res = super(groups_implied, self).write(cr, uid, ids, values, context)
602 if values.get('users') or values.get('implied_ids'):
603 # add all implied groups (to all users of each group)
604 for g in self.browse(cr, uid, ids, context=context):
605 gids = map(int, g.trans_implied_ids)
606 vals = {'users': [(4, u.id) for u in g.users]}
607 super(groups_implied, self).write(cr, uid, gids, vals, context)
610 class users_implied(osv.osv):
611 _inherit = 'res.users'
613 def create(self, cr, uid, values, context=None):
614 groups = values.pop('groups_id', None)
615 user_id = super(users_implied, self).create(cr, uid, values, context)
617 # delegate addition of groups to add implied groups
618 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
619 self.pool['ir.ui.view'].clear_cache()
622 def write(self, cr, uid, ids, values, context=None):
623 if not isinstance(ids,list):
625 res = super(users_implied, self).write(cr, uid, ids, values, context)
626 if values.get('groups_id'):
627 # add implied groups for all users
628 for user in self.browse(cr, uid, ids):
629 gs = set(concat(g.trans_implied_ids for g in user.groups_id))
630 vals = {'groups_id': [(4, g.id) for g in gs]}
631 super(users_implied, self).write(cr, uid, [user.id], vals, context)
632 self.pool['ir.ui.view'].clear_cache()
635 #----------------------------------------------------------
636 # Vitrual checkbox and selection for res.user form view
638 # Extension of res.groups and res.users for the special groups view in the users
639 # form. This extension presents groups with selection and boolean widgets:
640 # - Groups are shown by application, with boolean and/or selection fields.
641 # Selection fields typically defines a role "Name" for the given application.
642 # - Uncategorized groups are presented as boolean fields and grouped in a
645 # The user form view is modified by an inherited view (base.user_groups_view);
646 # the inherited view replaces the field 'groups_id' by a set of reified group
647 # fields (boolean or selection fields). The arch of that view is regenerated
648 # each time groups are changed.
650 # Naming conventions for reified groups fields:
651 # - boolean field 'in_group_ID' is True iff
652 # ID is in 'groups_id'
653 # - boolean field 'in_groups_ID1_..._IDk' is True iff
654 # any of ID1, ..., IDk is in 'groups_id'
655 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
656 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
657 #----------------------------------------------------------
659 def name_boolean_group(id): return 'in_group_' + str(id)
660 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
661 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
663 def is_boolean_group(name): return name.startswith('in_group_')
664 def is_boolean_groups(name): return name.startswith('in_groups_')
665 def is_selection_groups(name): return name.startswith('sel_groups_')
666 def is_reified_group(name):
667 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
669 def get_boolean_group(name): return int(name[9:])
670 def get_boolean_groups(name): return map(int, name[10:].split('_'))
671 def get_selection_groups(name): return map(int, name[11:].split('_'))
673 def partition(f, xs):
674 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
677 (yes if f(x) else nos).append(x)
680 def parse_m2m(commands):
681 "return a list of ids corresponding to a many2many value"
683 for command in commands:
684 if isinstance(command, (tuple, list)):
685 if command[0] in (1, 4):
686 ids.append(command[2])
687 elif command[0] == 5:
689 elif command[0] == 6:
690 ids = list(command[2])
696 class groups_view(osv.osv):
697 _inherit = 'res.groups'
699 def create(self, cr, uid, values, context=None):
700 res = super(groups_view, self).create(cr, uid, values, context)
701 self.update_user_groups_view(cr, uid, context)
704 def write(self, cr, uid, ids, values, context=None):
705 res = super(groups_view, self).write(cr, uid, ids, values, context)
706 self.update_user_groups_view(cr, uid, context)
709 def unlink(self, cr, uid, ids, context=None):
710 res = super(groups_view, self).unlink(cr, uid, ids, context)
711 self.update_user_groups_view(cr, uid, context)
714 def update_user_groups_view(self, cr, uid, context=None):
715 # the view with id 'base.user_groups_view' inherits the user form view,
716 # and introduces the reified group fields
717 # we have to try-catch this, because at first init the view does not exist
718 # but we are already creating some basic groups
719 view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context)
720 if view and view.exists() and view._name == 'ir.ui.view':
722 xml1.append(E.separator(string=_('Application'), colspan="4"))
723 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
724 # hide groups in category 'Hidden' (except to group_no_one)
725 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
726 if kind == 'selection':
727 # application name with a selection field
728 field_name = name_selection_groups(map(int, gs))
729 xml1.append(E.field(name=field_name, **attrs))
730 xml1.append(E.newline())
732 # application separator with boolean fields
733 app_name = app and app.name or _('Other')
734 xml2.append(E.separator(string=app_name, colspan="4", **attrs))
736 field_name = name_boolean_group(g.id)
737 xml2.append(E.field(name=field_name, **attrs))
739 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
740 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
741 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
742 view.write({'arch': xml_content})
745 def get_application_groups(self, cr, uid, domain=None, context=None):
746 return self.search(cr, uid, domain or [])
748 def get_groups_by_application(self, cr, uid, context=None):
749 """ return all groups classified by application (module category), as a list of pairs:
750 [(app, kind, [group, ...]), ...],
751 where app and group are browse records, and kind is either 'boolean' or 'selection'.
752 Applications are given in sequence order. If kind is 'selection', the groups are
753 given in reverse implication order.
757 # determine sequence order: a group should appear after its implied groups
758 order = dict.fromkeys(gs, 0)
760 for h in gs.intersection(g.trans_implied_ids):
762 # check whether order is total, i.e., sequence orders are distinct
763 if len(set(order.itervalues())) == len(gs):
764 return sorted(gs, key=lambda g: order[g])
767 # classify all groups by application
768 gids = self.get_application_groups(cr, uid, context=context)
769 by_app, others = {}, []
770 for g in self.browse(cr, uid, gids, context):
772 by_app.setdefault(g.category_id, []).append(g)
777 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
779 gs = linearized(by_app[app])
781 res.append((app, 'selection', gs))
783 res.append((app, 'boolean', by_app[app]))
785 res.append((False, 'boolean', others))
788 class users_view(osv.osv):
789 _inherit = 'res.users'
791 def create(self, cr, uid, values, context=None):
792 self._set_reified_groups(values)
793 return super(users_view, self).create(cr, uid, values, context)
795 def write(self, cr, uid, ids, values, context=None):
796 self._set_reified_groups(values)
797 return super(users_view, self).write(cr, uid, ids, values, context)
799 def _set_reified_groups(self, values):
800 """ reflect reified group fields in values['groups_id'] """
801 if 'groups_id' in values:
802 # groups are already given, ignore group fields
803 for f in filter(is_reified_group, values.iterkeys()):
808 for f in values.keys():
809 if is_boolean_group(f):
810 target = add if values.pop(f) else remove
811 target.append(get_boolean_group(f))
812 elif is_boolean_groups(f):
813 if not values.pop(f):
814 remove.extend(get_boolean_groups(f))
815 elif is_selection_groups(f):
816 remove.extend(get_selection_groups(f))
817 selected = values.pop(f)
820 # update values *only* if groups are being modified, otherwise
821 # we introduce spurious changes that might break the super.write() call.
823 # remove groups in 'remove' and add groups in 'add'
824 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
826 def default_get(self, cr, uid, fields, context=None):
827 group_fields, fields = partition(is_reified_group, fields)
828 fields1 = (fields + ['groups_id']) if group_fields else fields
829 values = super(users_view, self).default_get(cr, uid, fields1, context)
830 self._get_reified_groups(group_fields, values)
832 # add "default_groups_ref" inside the context to set default value for group_id with xml values
833 if 'groups_id' in fields and isinstance(context.get("default_groups_ref"), list):
835 ir_model_data = self.pool.get('ir.model.data')
836 for group_xml_id in context["default_groups_ref"]:
837 group_split = group_xml_id.split('.')
838 if len(group_split) != 2:
839 raise osv.except_osv(_('Invalid context value'), _('Invalid context default_groups_ref value (model.name_id) : "%s"') % group_xml_id)
841 temp, group_id = ir_model_data.get_object_reference(cr, uid, group_split[0], group_split[1])
845 values['groups_id'] = groups
848 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
849 fields_get = fields if fields is not None else self.fields_get(cr, uid, context=context).keys()
850 group_fields, _ = partition(is_reified_group, fields_get)
852 inject_groups_id = group_fields and fields and 'groups_id' not in fields
854 fields.append('groups_id')
855 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
857 if res and group_fields:
858 for values in (res if isinstance(res, list) else [res]):
859 self._get_reified_groups(group_fields, values)
861 values.pop('groups_id', None)
864 def _get_reified_groups(self, fields, values):
865 """ compute the given reified group fields from values['groups_id'] """
866 gids = set(parse_m2m(values.get('groups_id') or []))
868 if is_boolean_group(f):
869 values[f] = get_boolean_group(f) in gids
870 elif is_boolean_groups(f):
871 values[f] = not gids.isdisjoint(get_boolean_groups(f))
872 elif is_selection_groups(f):
873 selected = [gid for gid in get_selection_groups(f) if gid in gids]
874 values[f] = selected and selected[-1] or False
876 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
877 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
878 # add reified groups fields
879 for app, kind, gs in self.pool['res.groups'].get_groups_by_application(cr, uid, context):
880 if kind == 'selection':
881 # selection group field
882 tips = ['%s: %s' % (g.name, g.comment) for g in gs if g.comment]
883 res[name_selection_groups(map(int, gs))] = {
885 'string': app and app.name or _('Other'),
886 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
887 'help': '\n'.join(tips),
892 # boolean group fields
894 res[name_boolean_group(g.id)] = {
903 #----------------------------------------------------------
904 # change password wizard
905 #----------------------------------------------------------
907 class change_password_wizard(osv.TransientModel):
909 A wizard to manage the change of users' passwords
912 _name = "change.password.wizard"
913 _description = "Change Password Wizard"
915 'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'),
918 def default_get(self, cr, uid, fields, context=None):
921 user_ids = context.get('active_ids', [])
922 wiz_id = context.get('active_id', None)
924 users = self.pool.get('res.users').browse(cr, uid, user_ids, context=context)
929 'user_login': user.login,
931 return {'user_ids': res}
933 def change_password_button(self, cr, uid, id, context=None):
934 wizard = self.browse(cr, uid, id, context=context)[0]
935 need_reload = any(uid == user.user_id.id for user in wizard.user_ids)
936 line_ids = [user.id for user in wizard.user_ids]
938 self.pool.get('change.password.user').change_password_button(cr, uid, line_ids, context=context)
939 # don't keep temporary password copies in the database longer than necessary
940 self.pool.get('change.password.user').write(cr, uid, line_ids, {'new_passwd': False}, context=context)
944 'type': 'ir.actions.client',
948 return {'type': 'ir.actions.act_window_close'}
950 class change_password_user(osv.TransientModel):
952 A model to configure users in the change password wizard
955 _name = 'change.password.user'
956 _description = 'Change Password Wizard User'
958 'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True),
959 'user_id': fields.many2one('res.users', string='User', required=True),
960 'user_login': fields.char('User Login', readonly=True),
961 'new_passwd': fields.char('New Password'),
967 def change_password_button(self, cr, uid, ids, context=None):
968 for user in self.browse(cr, uid, ids, context=context):
969 self.pool.get('res.users').write(cr, uid, user.user_id.id, {'password': user.new_passwd})
972 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: