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-2011 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 import openerp.exceptions
31 from osv import fields,osv
32 from osv.orm import browse_record
35 from service import security
37 from tools.translate import _
39 _logger = logging.getLogger(__name__)
41 class groups(osv.osv):
43 _description = "Access Groups"
44 _rec_name = 'full_name'
46 def _get_full_name(self, cr, uid, ids, field, arg, context=None):
48 for g in self.browse(cr, uid, ids, context):
50 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
56 'name': fields.char('Name', size=64, required=True, translate=True),
57 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
58 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
59 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
60 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
61 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
62 'comment' : fields.text('Comment', size=250, translate=True),
63 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
64 'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
68 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
71 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
72 # add explicit ordering if search is sorted on full_name
73 if order and order.startswith('full_name'):
74 ids = super(groups, self).search(cr, uid, args, context=context)
75 gs = self.browse(cr, uid, ids, context)
76 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
77 gs = gs[offset:offset+limit] if limit else gs[offset:]
79 return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
81 def copy(self, cr, uid, id, default=None, context=None):
82 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
83 default.update({'name': _('%s (copy)')%group_name})
84 return super(groups, self).copy(cr, uid, id, default, context)
86 def write(self, cr, uid, ids, vals, context=None):
88 if vals['name'].startswith('-'):
89 raise osv.except_osv(_('Error'),
90 _('The name of the group can not start with "-"'))
91 res = super(groups, self).write(cr, uid, ids, vals, context=context)
92 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
97 class res_users(osv.osv):
98 """ User class. A res.users record models an OpenERP user and is different
101 res.users class now inherits from res.partner. The partner model is
102 used to store the data related to the partner: lang, name, address,
103 avatar, ... The user model is now dedicated to technical data.
108 'res.partner': 'partner_id',
111 _description = 'Users'
114 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
116 # Do not update the password if no value is provided, ignore silently.
117 # For example web client submits False values for all empty fields.
120 # To change their own password users must use the client-specific change password wizard,
121 # so that the new password is immediately used for further RPC requests, otherwise the user
122 # will face unexpected 'Access Denied' exceptions.
123 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
124 self.write(cr, uid, id, {'password': value})
126 def _get_password(self, cr, uid, ids, arg, karg, context=None):
127 return dict.fromkeys(ids, '')
130 'id': fields.integer('ID'),
131 'login_date': fields.date('Latest connection', select=1),
132 'partner_id': fields.many2one('res.partner', required=True,
133 string='Related Partner', ondelete='cascade',
134 help='Partner-related data of the user'),
135 'login': fields.char('Login', size=64, required=True,
136 help="Used to log into the system"),
137 'password': fields.char('Password', size=64, invisible=True,
138 help="Keep empty if you don't want the user to be able to connect on the system."),
139 'new_password': fields.function(_get_password, type='char', size=64,
140 fnct_inv=_set_new_password, string='Set Password',
141 help="Specify a value only when creating a user or if you're "\
142 "changing the user's password, otherwise leave empty. After "\
143 "a change of password, the user has to login again."),
144 'signature': fields.text('Signature', size=64),
145 'active': fields.boolean('Active'),
146 '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."),
147 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
148 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
149 # Special behavior for this field: res.company.search() will only return the companies
150 # available to the current user (should be the user's companies?), when the user_preference
152 'company_id': fields.many2one('res.company', 'Company', required=True,
153 help='The company this user is currently working for.', context={'user_preference': True}),
154 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
155 # backward compatibility fields
156 'user_email': fields.related('email', type='char',
157 deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'),
160 def on_change_company_id(self, cr, uid, ids, company_id):
161 return {'warning' : {
162 'title': _("Company Switch Warning"),
163 'message': _("Please keep in mind that documents currently displayed may not be relevant after switching to another company. If you have unsaved changes, please make sure to save and close all forms before switching to a different company. (You can click on Cancel in the User Preferences now)"),
167 def onchange_type(self, cr, uid, ids, is_company, context=None):
168 """ Wrapper on the user.partner onchange_type, because some calls to the
169 partner form view applied to the user may trigger the
170 partner.onchange_type method, but applied to the user object.
172 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
173 return self.pool.get('res.partner').onchange_type(cr, uid, partner_ids, is_company, context=context)
175 def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
176 """ Wrapper on the user.partner onchange_address, because some calls to the
177 partner form view applied to the user may trigger the
178 partner.onchange_type method, but applied to the user object.
180 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
181 return self.pool.get('res.partner').onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
183 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
184 def override_password(o):
185 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
186 o['password'] = '********'
188 result = super(res_users, self).read(cr, uid, ids, fields, context, load)
189 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
191 if isinstance(ids, (int, float)):
192 result = override_password(result)
194 result = map(override_password, result)
198 def _check_company(self, cr, uid, ids, context=None):
199 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
202 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
206 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
209 def _get_default_image(self, cr, uid, is_company, context=None):
210 """ Override of res.partner: multicolor avatars ! """
211 image_path = openerp.modules.get_module_resource('base', 'static/src/img', 'avatar%d.png' % random.randint(0, 6))
212 return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
214 def _get_company(self,cr, uid, context=None, uid2=False):
217 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
218 company_id = user.get('company_id', False)
219 return company_id and company_id[0] or False
221 def _get_companies(self, cr, uid, context=None):
222 c = self._get_company(cr, uid, context)
227 def _get_menu(self,cr, uid, context=None):
228 dataobj = self.pool.get('ir.model.data')
230 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
231 if model != 'ir.actions.act_window':
237 def _get_group(self,cr, uid, context=None):
238 dataobj = self.pool.get('ir.model.data')
241 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
242 result.append(group_id)
243 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
244 result.append(group_id)
246 # If these groups does not exists anymore
254 'menu_id': _get_menu,
255 'company_id': _get_company,
256 'company_ids': _get_companies,
257 'groups_id': _get_group,
258 'image': lambda self, cr, uid, context: self._get_default_image(cr, uid, False, context),
261 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
262 """ Override of res.users fields_view_get.
263 - if the view is specified: resume with normal behavior
264 - else: the default view is overrided and redirected to the partner
267 if not view_id and view_type == 'form':
268 return self.pool.get('res.partner').fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
269 return super(res_users, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
271 # User can write to a few of her own fields (but not her groups for example)
272 SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small']
274 def write(self, cr, uid, ids, values, context=None):
275 if not hasattr(ids, '__iter__'):
278 for key in values.keys():
279 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
282 if 'company_id' in values:
283 if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
284 del values['company_id']
285 uid = 1 # safe fields only, so we write as super-user to bypass access rights
287 res = super(res_users, self).write(cr, uid, ids, values, context=context)
289 # clear caches linked to the users
290 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
291 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
294 if db in self._uid_cache:
296 if id in self._uid_cache[db]:
297 del self._uid_cache[db][id]
301 def unlink(self, cr, uid, ids, context=None):
303 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, ...)'))
305 if db in self._uid_cache:
307 if id in self._uid_cache[db]:
308 del self._uid_cache[db][id]
309 return super(res_users, self).unlink(cr, uid, ids, context=context)
311 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
318 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
320 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
321 return self.name_get(cr, user, ids)
323 def copy(self, cr, uid, id, default=None, context=None):
324 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
327 copy_pattern = _("%s (copy)")
328 copydef = dict(login=(copy_pattern % user2copy['login']),
329 name=(copy_pattern % user2copy['name']),
331 copydef.update(default)
332 return super(res_users, self).copy(cr, uid, id, copydef, context)
334 def context_get(self, cr, uid, context=None):
335 user = self.browse(cr, uid, uid, context)
337 for k in self._all_columns.keys():
338 if k.startswith('context_'):
340 elif k in ['lang', 'tz']:
345 res = getattr(user,k) or False
346 if isinstance(res, browse_record):
348 result[context_key] = res or False
351 def action_get(self, cr, uid, context=None):
352 dataobj = self.pool.get('ir.model.data')
353 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
354 return dataobj.browse(cr, uid, data_id, context=context).res_id
356 def authenticate(self, db, login, password, user_agent_env):
357 """Verifies and returns the user ID corresponding to the given
358 ``login`` and ``password`` combination, or False if there was
361 :param str db: the database on which user is trying to authenticate
362 :param str login: username
363 :param str password: user password
364 :param dict user_agent_env: environment dictionary describing any
365 relevant environment attributes
367 uid = self.login(db, login, password)
368 if uid == openerp.SUPERUSER_ID:
369 # Successfully logged in as admin!
370 # Attempt to guess the web base url...
371 if user_agent_env and user_agent_env.get('base_location'):
372 cr = pooler.get_db(db).cursor()
374 self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url',
375 user_agent_env['base_location'])
378 _logger.exception("Failed to update web.base.url configuration parameter")
383 def login(self, db, login, password):
386 cr = pooler.get_db(db).cursor()
388 # autocommit: our single request will be performed atomically.
389 # (In this way, there is no opportunity to have two transactions
390 # interleaving their cr.execute()..cr.commit() calls and have one
391 # of them rolled back due to a concurrent access.)
392 # We effectively unconditionally write the res_users line.
394 # Even w/ autocommit there's a chance the user row will be locked,
395 # in which case we can't delay the login just for the purpose of
396 # update the last login date - hence we use FOR UPDATE NOWAIT to
397 # try to get the lock - fail-fast
398 cr.execute("""SELECT id from res_users
399 WHERE login=%s AND password=%s
400 AND active FOR UPDATE NOWAIT""",
401 (tools.ustr(login), tools.ustr(password)))
402 cr.execute("""UPDATE res_users
403 SET login_date = now() AT TIME ZONE 'UTC'
404 WHERE login=%s AND password=%s AND active
406 (tools.ustr(login), tools.ustr(password)))
408 # Failing to acquire the lock on the res_users row probably means
409 # another request is holding it. No big deal, we don't want to
410 # prevent/delay login in that case. It will also have been logged
411 # as a SQL error, if anyone cares.
412 cr.execute("""SELECT id from res_users
413 WHERE login=%s AND password=%s
415 (tools.ustr(login), tools.ustr(password)))
423 def check_super(self, passwd):
424 if passwd == tools.config['admin_passwd']:
427 raise openerp.exceptions.AccessDenied()
429 def check(self, db, uid, passwd):
430 """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
431 raise an exception if it is not."""
433 # empty passwords disallowed for obvious security reasons
434 raise openerp.exceptions.AccessDenied()
435 if self._uid_cache.get(db, {}).get(uid) == passwd:
437 cr = pooler.get_db(db).cursor()
439 cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
440 (int(uid), passwd, True))
441 res = cr.fetchone()[0]
443 raise openerp.exceptions.AccessDenied()
444 if self._uid_cache.has_key(db):
445 ulist = self._uid_cache[db]
448 self._uid_cache[db] = {uid:passwd}
452 def access(self, db, uid, passwd, sec_level, ids):
455 cr = pooler.get_db(db).cursor()
457 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
460 raise openerp.exceptions.AccessDenied()
465 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
466 """Change current user password. Old password must be provided explicitly
467 to prevent hijacking an existing user session, or for cases where the cleartext
468 password is not used to authenticate requests.
471 :raise: openerp.exceptions.AccessDenied when old password is wrong
472 :raise: except_osv when new password is not set or empty
474 self.check(cr.dbname, uid, old_passwd)
476 return self.write(cr, uid, uid, {'password': new_passwd})
477 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
479 def preference_save(self, cr, uid, ids, context=None):
481 'type': 'ir.actions.client',
485 def preference_change_password(self, cr, uid, ids, context=None):
487 'type': 'ir.actions.client',
488 'tag': 'change_password',
492 def has_group(self, cr, uid, group_ext_id):
493 """Checks whether user belongs to given group.
495 :param str group_ext_id: external ID (XML ID) of the group.
496 Must be provided in fully-qualified form (``module.ext_id``), as there
497 is no implicit module to use..
498 :return: True if the current user is a member of the group with the
499 given external ID (XML ID), else False.
501 assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
502 module, ext_id = group_ext_id.split('.')
503 cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
504 (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
505 (uid, module, ext_id))
506 return bool(cr.fetchone())
510 # Extension of res.groups and res.users with a relation for "implied" or
511 # "inherited" groups. Once a user belongs to a group, it automatically belongs
512 # to the implied groups (transitively).
516 """ A cset (constrained set) is a set of elements that may be constrained to
517 be a subset of other csets. Elements added to a cset are automatically
518 added to its supersets. Cycles in the subset constraints are supported.
520 def __init__(self, xs):
521 self.supersets = set()
522 self.elements = set(xs)
523 def subsetof(self, other):
524 if other is not self:
525 self.supersets.add(other)
526 other.update(self.elements)
527 def update(self, xs):
528 xs = set(xs) - self.elements
529 if xs: # xs will eventually be empty in case of a cycle
530 self.elements.update(xs)
531 for s in self.supersets:
534 return iter(self.elements)
537 """ return the concatenation of a list of iterables """
539 for l in ls: res.extend(l)
544 class groups_implied(osv.osv):
545 _inherit = 'res.groups'
547 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
548 "computes the transitive closure of relation implied_ids"
549 memo = {} # use a memo for performance and cycle avoidance
552 memo[g] = cset(g.implied_ids)
553 for h in g.implied_ids:
554 computed_set(h).subsetof(memo[g])
558 for g in self.browse(cr, 1, ids, context):
559 res[g.id] = map(int, computed_set(g))
563 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
564 string='Inherits', help='Users of this group automatically inherit those groups'),
565 'trans_implied_ids': fields.function(_get_trans_implied,
566 type='many2many', relation='res.groups', string='Transitively inherits'),
569 def create(self, cr, uid, values, context=None):
570 users = values.pop('users', None)
571 gid = super(groups_implied, self).create(cr, uid, values, context)
573 # delegate addition of users to add implied groups
574 self.write(cr, uid, [gid], {'users': users}, context)
577 def write(self, cr, uid, ids, values, context=None):
578 res = super(groups_implied, self).write(cr, uid, ids, values, context)
579 if values.get('users') or values.get('implied_ids'):
580 # add all implied groups (to all users of each group)
581 for g in self.browse(cr, uid, ids):
582 gids = map(int, g.trans_implied_ids)
583 vals = {'users': [(4, u.id) for u in g.users]}
584 super(groups_implied, self).write(cr, uid, gids, vals, context)
589 class users_implied(osv.osv):
590 _inherit = 'res.users'
592 def create(self, cr, uid, values, context=None):
593 groups = values.pop('groups_id', None)
594 user_id = super(users_implied, self).create(cr, uid, values, context)
596 # delegate addition of groups to add implied groups
597 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
600 def write(self, cr, uid, ids, values, context=None):
601 if not isinstance(ids,list):
603 res = super(users_implied, self).write(cr, uid, ids, values, context)
604 if values.get('groups_id'):
605 # add implied groups for all users
606 for user in self.browse(cr, uid, ids):
607 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
608 vals = {'groups_id': [(4, g.id) for g in gs]}
609 super(users_implied, self).write(cr, uid, [user.id], vals, context)
617 # Extension of res.groups and res.users for the special groups view in the users
618 # form. This extension presents groups with selection and boolean widgets:
619 # - Groups are shown by application, with boolean and/or selection fields.
620 # Selection fields typically defines a role "Name" for the given application.
621 # - Uncategorized groups are presented as boolean fields and grouped in a
624 # The user form view is modified by an inherited view (base.user_groups_view);
625 # the inherited view replaces the field 'groups_id' by a set of reified group
626 # fields (boolean or selection fields). The arch of that view is regenerated
627 # each time groups are changed.
629 # Naming conventions for reified groups fields:
630 # - boolean field 'in_group_ID' is True iff
631 # ID is in 'groups_id'
632 # - boolean field 'in_groups_ID1_..._IDk' is True iff
633 # any of ID1, ..., IDk is in 'groups_id'
634 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
635 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
637 def name_boolean_group(id): return 'in_group_' + str(id)
638 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
639 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
641 def is_boolean_group(name): return name.startswith('in_group_')
642 def is_boolean_groups(name): return name.startswith('in_groups_')
643 def is_selection_groups(name): return name.startswith('sel_groups_')
644 def is_reified_group(name):
645 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
647 def get_boolean_group(name): return int(name[9:])
648 def get_boolean_groups(name): return map(int, name[10:].split('_'))
649 def get_selection_groups(name): return map(int, name[11:].split('_'))
651 def partition(f, xs):
652 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
655 (yes if f(x) else nos).append(x)
660 class groups_view(osv.osv):
661 _inherit = 'res.groups'
663 def create(self, cr, uid, values, context=None):
664 res = super(groups_view, self).create(cr, uid, values, context)
665 self.update_user_groups_view(cr, uid, context)
668 def write(self, cr, uid, ids, values, context=None):
669 res = super(groups_view, self).write(cr, uid, ids, values, context)
670 self.update_user_groups_view(cr, uid, context)
673 def unlink(self, cr, uid, ids, context=None):
674 res = super(groups_view, self).unlink(cr, uid, ids, context)
675 self.update_user_groups_view(cr, uid, context)
678 def update_user_groups_view(self, cr, uid, context=None):
679 # the view with id 'base.user_groups_view' inherits the user form view,
680 # and introduces the reified group fields
681 view = self.get_user_groups_view(cr, uid, context)
684 xml1.append(E.separator(string=_('Application'), colspan="4"))
685 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
686 # hide groups in category 'Hidden' (except to group_no_one)
687 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
688 if kind == 'selection':
689 # application name with a selection field
690 field_name = name_selection_groups(map(int, gs))
691 xml1.append(E.field(name=field_name, **attrs))
692 xml1.append(E.newline())
694 # application separator with boolean fields
695 app_name = app and app.name or _('Other')
696 xml2.append(E.separator(string=app_name, colspan="4", **attrs))
698 field_name = name_boolean_group(g.id)
699 xml2.append(E.field(name=field_name, **attrs))
701 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
702 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
703 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
704 view.write({'arch': xml_content})
707 def get_user_groups_view(self, cr, uid, context=None):
709 view = self.pool.get('ir.model.data').get_object(cr, 1, 'base', 'user_groups_view', context)
710 assert view and view._table_name == 'ir.ui.view'
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))
760 class users_view(osv.osv):
761 _inherit = 'res.users'
763 def create(self, cr, uid, values, context=None):
764 self._set_reified_groups(values)
765 return super(users_view, self).create(cr, uid, values, context)
767 def write(self, cr, uid, ids, values, context=None):
768 self._set_reified_groups(values)
769 return super(users_view, self).write(cr, uid, ids, values, context)
771 def _set_reified_groups(self, values):
772 """ reflect reified group fields in values['groups_id'] """
773 if 'groups_id' in values:
774 # groups are already given, ignore group fields
775 for f in filter(is_reified_group, values.iterkeys()):
780 for f in values.keys():
781 if is_boolean_group(f):
782 target = add if values.pop(f) else remove
783 target.append(get_boolean_group(f))
784 elif is_boolean_groups(f):
785 if not values.pop(f):
786 remove.extend(get_boolean_groups(f))
787 elif is_selection_groups(f):
788 remove.extend(get_selection_groups(f))
789 selected = values.pop(f)
792 # update values *only* if groups are being modified, otherwise
793 # we introduce spurious changes that might break the super.write() call.
795 # remove groups in 'remove' and add groups in 'add'
796 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
798 def default_get(self, cr, uid, fields, context=None):
799 group_fields, fields = partition(is_reified_group, fields)
800 fields1 = (fields + ['groups_id']) if group_fields else fields
801 values = super(users_view, self).default_get(cr, uid, fields1, context)
802 self._get_reified_groups(group_fields, values)
805 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
807 fields = self.fields_get(cr, uid, context=context).keys()
808 group_fields, fields = partition(is_reified_group, fields)
809 if not 'groups_id' in fields:
810 fields.append('groups_id')
811 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
812 for values in (res if isinstance(res, list) else [res]):
813 self._get_reified_groups(group_fields, values)
816 def _get_reified_groups(self, fields, values):
817 """ compute the given reified group fields from values['groups_id'] """
818 gids = set(values.get('groups_id') or [])
820 if is_boolean_group(f):
821 values[f] = get_boolean_group(f) in gids
822 elif is_boolean_groups(f):
823 values[f] = not gids.isdisjoint(get_boolean_groups(f))
824 elif is_selection_groups(f):
825 selected = [gid for gid in get_selection_groups(f) if gid in gids]
826 values[f] = selected and selected[-1] or False
828 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
829 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
830 # add reified groups fields
831 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
832 if kind == 'selection':
833 # selection group field
834 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
835 res[name_selection_groups(map(int, gs))] = {
837 'string': app and app.name or _('Other'),
838 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
839 'help': '\n'.join(tips),
842 # boolean group fields
844 res[name_boolean_group(g.id)] = {
853 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: