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
29 from lxml import etree
30 from lxml.builder import E
33 import openerp.exceptions
34 from osv import fields,osv
35 from osv.orm import browse_record
39 from service import security
41 from tools.translate import _
44 _logger = logging.getLogger(__name__)
46 class groups(osv.osv):
48 _description = "Access Groups"
49 _rec_name = 'full_name'
51 def _get_full_name(self, cr, uid, ids, field, arg, context=None):
53 for g in self.browse(cr, uid, ids, context):
55 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
61 'name': fields.char('Name', size=64, required=True, translate=True),
62 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
63 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
64 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
65 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
66 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
67 'comment' : fields.text('Comment', size=250, translate=True),
68 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
69 'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
73 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
76 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
77 # add explicit ordering if search is sorted on full_name
78 if order and order.startswith('full_name'):
79 ids = super(groups, self).search(cr, uid, args, context=context)
80 gs = self.browse(cr, uid, ids, context)
81 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
82 gs = gs[offset:offset+limit] if limit else gs[offset:]
84 return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
86 def copy(self, cr, uid, id, default=None, context=None):
87 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
88 default.update({'name': _('%s (copy)')%group_name})
89 return super(groups, self).copy(cr, uid, id, default, context)
91 def write(self, cr, uid, ids, vals, context=None):
93 if vals['name'].startswith('-'):
94 raise osv.except_osv(_('Error'),
95 _('The name of the group can not start with "-"'))
96 res = super(groups, self).write(cr, uid, ids, vals, context=context)
97 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
100 def create(self, cr, uid, vals, context=None):
102 if vals['name'].startswith('-'):
103 raise osv.except_osv(_('Error'),
104 _('The name of the group can not start with "-"'))
105 gid = super(groups, self).create(cr, uid, vals, context=context)
106 if context and context.get('noadmin', False):
109 # assign this new group to user_root
110 user_obj = self.pool.get('res.users')
111 aid = user_obj.browse(cr, 1, user_obj._get_admin_id(cr))
113 aid.write({'groups_id': [(4, gid)]})
116 def unlink(self, cr, uid, ids, context=None):
118 for record in self.read(cr, uid, ids, ['users'], context=context):
120 group_users.extend(record['users'])
122 user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
123 user_names = list(set(user_names))
124 if len(user_names) >= 5:
125 user_names = user_names[:5] + ['...']
126 raise osv.except_osv(_('Warning !'),
127 _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
128 ', '.join(user_names))
129 return super(groups, self).unlink(cr, uid, ids, context=context)
131 def get_extended_interface_group(self, cr, uid, context=None):
132 data_obj = self.pool.get('ir.model.data')
133 extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
134 return data_obj.browse(cr, uid, extended_group_data_id, context=context).res_id
138 def _lang_get(self, cr, uid, context=None):
139 obj = self.pool.get('res.lang')
140 ids = obj.search(cr, uid, [('translatable','=',True)])
141 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
142 res = [(r['code'], r['name']) for r in res]
145 def _tz_get(self,cr,uid, context=None):
146 return [(x, x) for x in pytz.all_timezones]
148 class users(osv.osv):
154 WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
155 WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
156 "\"%(name)s\".\n\nYour login is %(login)s, "\
157 "you should ask your supervisor or system administrator if you "\
158 "haven't been given your password yet.\n\n"\
159 "If you aren't %(name)s, this email reached you errorneously, "\
162 def get_welcome_mail_subject(self, cr, uid, context=None):
163 """ Returns the subject of the mail new users receive (when
164 created via the res.config.users wizard), default implementation
165 is to return config_users.WELCOME_MAIL_SUBJECT
167 return self.WELCOME_MAIL_SUBJECT
168 def get_welcome_mail_body(self, cr, uid, context=None):
169 """ Returns the subject of the mail new users receive (when
170 created via the res.config.users wizard), default implementation
171 is to return config_users.WELCOME_MAIL_BODY
173 return self.WELCOME_MAIL_BODY
175 def get_current_company(self, cr, uid):
176 cr.execute('select company_id, res_company.name from res_users left join res_company on res_company.id = company_id where res_users.id=%s' %uid)
179 def send_welcome_email(self, cr, uid, id, context=None):
180 if isinstance(id,list): id = id[0]
181 user = self.read(cr, uid, id, ['email','login','name', 'user_email'], context=context)
182 email = user['email'] or user['user_email']
184 ir_mail_server = self.pool.get('ir.mail_server')
185 msg = ir_mail_server.build_email(email_from=None, # take config default
187 subject=self.get_welcome_mail_subject(cr, uid, context=context),
188 body=(self.get_welcome_mail_body(cr, uid, context=context) % user))
189 return ir_mail_server.send_email(cr, uid, msg, context=context)
191 def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
192 """Implementation of 'view' function field setter, sets the type of interface of the users.
193 @param name: Name of the field
194 @param arg: User defined argument
195 @param value: new value returned
198 if not value or value not in ['simple','extended']:
200 group_obj = self.pool.get('res.groups')
201 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
202 # First always remove the users from the group (avoids duplication if called twice)
203 self.write(cr, uid, ids, {'groups_id': [(3, extended_group_id)]}, context=context)
204 # Then add them back if requested
205 if value == 'extended':
206 self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
209 def _get_interface_type(self, cr, uid, ids, name, args, context=None):
210 """Implementation of 'view' function field getter, returns the type of interface of the users.
211 @param field_name: Name of the field
212 @param arg: User defined argument
213 @return: Dictionary of values
215 group_obj = self.pool.get('res.groups')
216 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
217 extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
218 return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
220 def onchange_avatar(self, cr, uid, ids, value, context=None):
222 return {'value': {'avatar_big': value, 'avatar': value} }
223 return {'value': {'avatar_big': self._avatar_resize(cr, uid, value, 540, 450, context=context), 'avatar': self._avatar_resize(cr, uid, value, context=context)} }
225 def _set_avatar(self, cr, uid, id, name, value, args, context=None):
227 return {'value': {'avatar_big': value, 'avatar': value} }
228 return self.write(cr, uid, [id], {'avatar_big': self._avatar_resize(cr, uid, value, 540, 450, context=context)}, context=context)
230 def _avatar_resize(self, cr, uid, avatar, height=180, width=150, context=None):
231 image_stream = io.BytesIO(avatar.decode('base64'))
232 img = Image.open(image_stream)
233 img.thumbnail((height, width), Image.ANTIALIAS)
234 img_stream = StringIO.StringIO()
235 img.save(img_stream, "JPEG")
236 return img_stream.getvalue().encode('base64')
238 def _get_avatar(self, cr, uid, ids, name, args, context=None):
239 result = dict.fromkeys(ids, False)
240 for user in self.browse(cr, uid, ids, context=context):
242 result[user.id] = self._avatar_resize(cr, uid, user.avatar_big, context=context)
245 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
247 # Do not update the password if no value is provided, ignore silently.
248 # For example web client submits False values for all empty fields.
251 # To change their own password users must use the client-specific change password wizard,
252 # so that the new password is immediately used for further RPC requests, otherwise the user
253 # will face unexpected 'Access Denied' exceptions.
254 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
255 self.write(cr, uid, id, {'password': value})
257 def _get_password(self, cr, uid, ids, arg, karg, context=None):
258 return dict.fromkeys(ids, '')
261 'id': fields.integer('ID'),
262 'name': fields.char('User Name', size=64, required=True, select=True,
263 help="The new user's real name, used for searching"
264 " and most listings"),
265 'login': fields.char('Login', size=64, required=True,
266 help="Used to log into the system"),
267 'password': fields.char('Password', size=64, invisible=True, help="Keep empty if you don't want the user to be able to connect on the system."),
268 'new_password': fields.function(_get_password, type='char', size=64,
269 fnct_inv=_set_new_password,
270 string='Set password', help="Specify a value only when creating a user or if you're changing the user's password, "
271 "otherwise leave empty. After a change of password, the user has to login again."),
272 'user_email': fields.char('Email', size=64),
273 'signature': fields.text('Signature', size=64),
274 'avatar_big': fields.binary('Big-sized avatar', help="This field holds the image used as avatar for the user. The avatar field is used as an interface to access this field. The image is base64 encoded, and PIL-supported. It is stored as a 540x450 px image, in case a bigger image must be used."),
275 'avatar': fields.function(_get_avatar, fnct_inv=_set_avatar, string='Avatar', type="binary",
277 'res.users': (lambda self, cr, uid, ids, c={}: ids, ['avatar_big'], 10),
278 }, help="Image used as avatar for the user. It is automatically resized as a 180x150 px image. This field serves as an interface to the avatar_big field."),
279 'active': fields.boolean('Active'),
280 '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."),
281 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
282 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
284 # Special behavior for this field: res.company.search() will only return the companies
285 # available to the current user (should be the user's companies?), when the user_preference
287 'company_id': fields.many2one('res.company', 'Company', required=True,
288 help="The company this user is currently working for.", context={'user_preference': True}),
290 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
291 'context_lang': fields.selection(_lang_get, 'Language', required=True,
292 help="The default language used in the graphical user interface, when translations are available. To add a new language, you can use the 'Load an Official Translation' wizard available from the 'Administration' menu."),
293 'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
294 help="The user's timezone, used to output proper date and time values inside printed reports. "
295 "It is important to set a value for this field. You should use the same timezone "
296 "that is otherwise used to pick and render date and time values: your computer's timezone."),
297 'view': fields.function(_get_interface_type, type='selection', fnct_inv=_set_interface_type,
298 selection=[('simple','Simplified'),('extended','Extended')],
299 string='Interface', help="OpenERP offers a simplified and an extended user interface. If you use OpenERP for the first time we strongly advise you to select the simplified interface, which has less features but is easier to use. You can switch to the other interface from the User/Preferences menu at any time."),
300 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
301 'date': fields.datetime('Latest Connection', readonly=True),
304 def on_change_company_id(self, cr, uid, ids, company_id):
307 'title': _("Company Switch Warning"),
308 '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)"),
312 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
313 def override_password(o):
314 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
315 o['password'] = '********'
317 result = super(users, self).read(cr, uid, ids, fields, context, load)
318 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
320 if isinstance(ids, (int, float)):
321 result = override_password(result)
323 result = map(override_password, result)
327 def _check_company(self, cr, uid, ids, context=None):
328 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
331 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
335 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
338 def _get_email_from(self, cr, uid, ids, context=None):
339 if not isinstance(ids, list):
341 res = dict.fromkeys(ids, False)
342 for user in self.browse(cr, uid, ids, context=context):
344 res[user.id] = "%s <%s>" % (user.name, user.user_email)
347 def _get_admin_id(self, cr):
348 if self.__admin_ids.get(cr.dbname) is None:
349 ir_model_data_obj = self.pool.get('ir.model.data')
350 mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
351 self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
352 return self.__admin_ids[cr.dbname]
354 def _get_company(self,cr, uid, context=None, uid2=False):
357 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
358 company_id = user.get('company_id', False)
359 return company_id and company_id[0] or False
361 def _get_companies(self, cr, uid, context=None):
362 c = self._get_company(cr, uid, context)
367 def _get_menu(self,cr, uid, context=None):
368 dataobj = self.pool.get('ir.model.data')
370 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
371 if model != 'ir.actions.act_window':
377 def _get_group(self,cr, uid, context=None):
378 dataobj = self.pool.get('ir.model.data')
381 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
382 result.append(group_id)
383 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
384 result.append(group_id)
386 # If these groups does not exists anymore
390 def _get_avatar(self, cr, uid, context=None):
391 # default avatar file name: avatar0 -> avatar6.jpg, choose randomly
392 avatar_path = openerp.modules.get_module_resource('base', 'images', 'avatar%d.jpg' % random.randint(0, 6))
393 return self._avatar_resize(cr, uid, open(avatar_path, 'rb').read().encode('base64'), context=context)
397 'context_lang': 'en_US',
398 'avatar': _get_avatar,
400 'menu_id': _get_menu,
401 'company_id': _get_company,
402 'company_ids': _get_companies,
403 'groups_id': _get_group,
407 # User can write to a few of her own fields (but not her groups for example)
408 SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
410 def write(self, cr, uid, ids, values, context=None):
411 if not hasattr(ids, '__iter__'):
414 for key in values.keys():
415 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
418 if 'company_id' in values:
419 if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
420 del values['company_id']
421 uid = 1 # safe fields only, so we write as super-user to bypass access rights
423 res = super(users, self).write(cr, uid, ids, values, context=context)
425 # clear caches linked to the users
426 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
427 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
430 if db in self._uid_cache:
432 if id in self._uid_cache[db]:
433 del self._uid_cache[db][id]
437 def unlink(self, cr, uid, ids, context=None):
439 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, ...)'))
441 if db in self._uid_cache:
443 if id in self._uid_cache[db]:
444 del self._uid_cache[db][id]
445 return super(users, self).unlink(cr, uid, ids, context=context)
447 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
454 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
456 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
457 return self.name_get(cr, user, ids)
459 def copy(self, cr, uid, id, default=None, context=None):
460 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
463 copy_pattern = _("%s (copy)")
464 copydef = dict(login=(copy_pattern % user2copy['login']),
465 name=(copy_pattern % user2copy['name']),
467 copydef.update(default)
468 return super(users, self).copy(cr, uid, id, copydef, context)
470 def context_get(self, cr, uid, context=None):
471 user = self.browse(cr, uid, uid, context)
473 for k in self._columns.keys():
474 if k.startswith('context_'):
475 res = getattr(user,k) or False
476 if isinstance(res, browse_record):
478 result[k[8:]] = res or False
481 def action_get(self, cr, uid, context=None):
482 dataobj = self.pool.get('ir.model.data')
483 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
484 return dataobj.browse(cr, uid, data_id, context=context).res_id
486 def authenticate(self, db, login, password, user_agent_env):
487 """Verifies and returns the user ID corresponding to the given
488 ``login`` and ``password`` combination, or False if there was
491 :param str db: the database on which user is trying to authenticate
492 :param str login: username
493 :param str password: user password
494 :param dict user_agent_env: environment dictionary describing any
495 relevant environment attributes
497 uid = self.login(db, login, password)
498 if uid == openerp.SUPERUSER_ID:
499 # Successfully logged in as admin!
500 # Attempt to guess the web base url...
501 if user_agent_env and user_agent_env.get('base_location'):
502 cr = pooler.get_db(db).cursor()
504 self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url',
505 user_agent_env['base_location'])
508 _logger.exception("Failed to update web.base.url configuration parameter")
513 def login(self, db, login, password):
516 cr = pooler.get_db(db).cursor()
518 # autocommit: our single request will be performed atomically.
519 # (In this way, there is no opportunity to have two transactions
520 # interleaving their cr.execute()..cr.commit() calls and have one
521 # of them rolled back due to a concurrent access.)
522 # We effectively unconditionally write the res_users line.
524 # Even w/ autocommit there's a chance the user row will be locked,
525 # in which case we can't delay the login just for the purpose of
526 # update the last login date - hence we use FOR UPDATE NOWAIT to
527 # try to get the lock - fail-fast
528 cr.execute("""SELECT id from res_users
529 WHERE login=%s AND password=%s
530 AND active FOR UPDATE NOWAIT""",
531 (tools.ustr(login), tools.ustr(password)))
532 cr.execute("""UPDATE res_users
533 SET date = now() AT TIME ZONE 'UTC'
534 WHERE login=%s AND password=%s AND active
536 (tools.ustr(login), tools.ustr(password)))
538 # Failing to acquire the lock on the res_users row probably means
539 # another request is holding it. No big deal, we don't want to
540 # prevent/delay login in that case. It will also have been logged
541 # as a SQL error, if anyone cares.
542 cr.execute("""SELECT id from res_users
543 WHERE login=%s AND password=%s
545 (tools.ustr(login), tools.ustr(password)))
553 def check_super(self, passwd):
554 if passwd == tools.config['admin_passwd']:
557 raise openerp.exceptions.AccessDenied()
559 def check(self, db, uid, passwd):
560 """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
561 raise an exception if it is not."""
563 # empty passwords disallowed for obvious security reasons
564 raise openerp.exceptions.AccessDenied()
565 if self._uid_cache.get(db, {}).get(uid) == passwd:
567 cr = pooler.get_db(db).cursor()
569 cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
570 (int(uid), passwd, True))
571 res = cr.fetchone()[0]
573 raise openerp.exceptions.AccessDenied()
574 if self._uid_cache.has_key(db):
575 ulist = self._uid_cache[db]
578 self._uid_cache[db] = {uid:passwd}
582 def access(self, db, uid, passwd, sec_level, ids):
585 cr = pooler.get_db(db).cursor()
587 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
590 raise openerp.exceptions.AccessDenied()
595 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
596 """Change current user password. Old password must be provided explicitly
597 to prevent hijacking an existing user session, or for cases where the cleartext
598 password is not used to authenticate requests.
601 :raise: openerp.exceptions.AccessDenied when old password is wrong
602 :raise: except_osv when new password is not set or empty
604 self.check(cr.dbname, uid, old_passwd)
606 return self.write(cr, uid, uid, {'password': new_passwd})
607 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
614 # Extension of res.groups and res.users with a relation for "implied" or
615 # "inherited" groups. Once a user belongs to a group, it automatically belongs
616 # to the implied groups (transitively).
620 """ A cset (constrained set) is a set of elements that may be constrained to
621 be a subset of other csets. Elements added to a cset are automatically
622 added to its supersets. Cycles in the subset constraints are supported.
624 def __init__(self, xs):
625 self.supersets = set()
626 self.elements = set(xs)
627 def subsetof(self, other):
628 if other is not self:
629 self.supersets.add(other)
630 other.update(self.elements)
631 def update(self, xs):
632 xs = set(xs) - self.elements
633 if xs: # xs will eventually be empty in case of a cycle
634 self.elements.update(xs)
635 for s in self.supersets:
638 return iter(self.elements)
641 """ return the concatenation of a list of iterables """
643 for l in ls: res.extend(l)
648 class groups_implied(osv.osv):
649 _inherit = 'res.groups'
651 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
652 "computes the transitive closure of relation implied_ids"
653 memo = {} # use a memo for performance and cycle avoidance
656 memo[g] = cset(g.implied_ids)
657 for h in g.implied_ids:
658 computed_set(h).subsetof(memo[g])
662 for g in self.browse(cr, 1, ids, context):
663 res[g.id] = map(int, computed_set(g))
667 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
668 string='Inherits', help='Users of this group automatically inherit those groups'),
669 'trans_implied_ids': fields.function(_get_trans_implied,
670 type='many2many', relation='res.groups', string='Transitively inherits'),
673 def create(self, cr, uid, values, context=None):
674 users = values.pop('users', None)
675 gid = super(groups_implied, self).create(cr, uid, values, context)
677 # delegate addition of users to add implied groups
678 self.write(cr, uid, [gid], {'users': users}, context)
681 def write(self, cr, uid, ids, values, context=None):
682 res = super(groups_implied, self).write(cr, uid, ids, values, context)
683 if values.get('users') or values.get('implied_ids'):
684 # add all implied groups (to all users of each group)
685 for g in self.browse(cr, uid, ids):
686 gids = map(int, g.trans_implied_ids)
687 vals = {'users': [(4, u.id) for u in g.users]}
688 super(groups_implied, self).write(cr, uid, gids, vals, context)
693 class users_implied(osv.osv):
694 _inherit = 'res.users'
696 def create(self, cr, uid, values, context=None):
697 groups = values.pop('groups_id', None)
698 user_id = super(users_implied, self).create(cr, uid, values, context)
700 # delegate addition of groups to add implied groups
701 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
704 def write(self, cr, uid, ids, values, context=None):
705 if not isinstance(ids,list):
707 res = super(users_implied, self).write(cr, uid, ids, values, context)
708 if values.get('groups_id'):
709 # add implied groups for all users
710 for user in self.browse(cr, uid, ids):
711 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
712 vals = {'groups_id': [(4, g.id) for g in gs]}
713 super(users_implied, self).write(cr, uid, [user.id], vals, context)
721 # Extension of res.groups and res.users for the special groups view in the users
722 # form. This extension presents groups with selection and boolean widgets:
723 # - Groups are shown by application, with boolean and/or selection fields.
724 # Selection fields typically defines a role "Name" for the given application.
725 # - Uncategorized groups are presented as boolean fields and grouped in a
728 # The user form view is modified by an inherited view (base.user_groups_view);
729 # the inherited view replaces the field 'groups_id' by a set of reified group
730 # fields (boolean or selection fields). The arch of that view is regenerated
731 # each time groups are changed.
733 # Naming conventions for reified groups fields:
734 # - boolean field 'in_group_ID' is True iff
735 # ID is in 'groups_id'
736 # - boolean field 'in_groups_ID1_..._IDk' is True iff
737 # any of ID1, ..., IDk is in 'groups_id'
738 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
739 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
741 def name_boolean_group(id): return 'in_group_' + str(id)
742 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
743 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
745 def is_boolean_group(name): return name.startswith('in_group_')
746 def is_boolean_groups(name): return name.startswith('in_groups_')
747 def is_selection_groups(name): return name.startswith('sel_groups_')
748 def is_reified_group(name):
749 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
751 def get_boolean_group(name): return int(name[9:])
752 def get_boolean_groups(name): return map(int, name[10:].split('_'))
753 def get_selection_groups(name): return map(int, name[11:].split('_'))
755 def partition(f, xs):
756 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
759 (yes if f(x) else nos).append(x)
764 class groups_view(osv.osv):
765 _inherit = 'res.groups'
767 def create(self, cr, uid, values, context=None):
768 res = super(groups_view, self).create(cr, uid, values, context)
769 self.update_user_groups_view(cr, uid, context)
772 def write(self, cr, uid, ids, values, context=None):
773 res = super(groups_view, self).write(cr, uid, ids, values, context)
774 self.update_user_groups_view(cr, uid, context)
777 def unlink(self, cr, uid, ids, context=None):
778 res = super(groups_view, self).unlink(cr, uid, ids, context)
779 self.update_user_groups_view(cr, uid, context)
782 def update_user_groups_view(self, cr, uid, context=None):
783 # the view with id 'base.user_groups_view' inherits the user form view,
784 # and introduces the reified group fields
785 view = self.get_user_groups_view(cr, uid, context)
789 xml1.append(E.separator(string=_('Application'), colspan="4"))
790 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
791 if kind == 'selection':
792 # application name with a selection field
793 field_name = name_selection_groups(map(int, gs))
794 xml1.append(E.field(name=field_name))
795 xml1.append(E.newline())
797 # application separator with boolean fields
798 app_name = app and app.name or _('Other')
799 xml2.append(E.separator(string=app_name, colspan="4"))
801 field_name = name_boolean_group(g.id)
802 xml2.append(E.field(name=field_name))
804 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
805 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
806 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
807 view.write({'arch': xml_content})
810 def get_user_groups_view(self, cr, uid, context=None):
812 view = self.pool.get('ir.model.data').get_object(cr, 1, 'base', 'user_groups_view', context)
813 assert view and view._table_name == 'ir.ui.view'
818 def get_application_groups(self, cr, uid, domain=None, context=None):
819 return self.search(cr, uid, domain or [])
821 def get_groups_by_application(self, cr, uid, context=None):
822 """ return all groups classified by application (module category), as a list of pairs:
823 [(app, kind, [group, ...]), ...],
824 where app and group are browse records, and kind is either 'boolean' or 'selection'.
825 Applications are given in sequence order. If kind is 'selection', the groups are
826 given in reverse implication order.
830 # determine sequence order: a group should appear after its implied groups
831 order = dict.fromkeys(gs, 0)
833 for h in gs.intersection(g.trans_implied_ids):
835 # check whether order is total, i.e., sequence orders are distinct
836 if len(set(order.itervalues())) == len(gs):
837 return sorted(gs, key=lambda g: order[g])
840 # classify all groups by application
841 gids = self.get_application_groups(cr, uid, context=context)
842 by_app, others = {}, []
843 for g in self.browse(cr, uid, gids, context):
845 by_app.setdefault(g.category_id, []).append(g)
850 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
852 gs = linearized(by_app[app])
854 res.append((app, 'selection', gs))
856 res.append((app, 'boolean', by_app[app]))
858 res.append((False, 'boolean', others))
863 class users_view(osv.osv):
864 _inherit = 'res.users'
866 def create(self, cr, uid, values, context=None):
867 self._set_reified_groups(values)
868 return super(users_view, self).create(cr, uid, values, context)
870 def write(self, cr, uid, ids, values, context=None):
871 self._set_reified_groups(values)
872 return super(users_view, self).write(cr, uid, ids, values, context)
874 def _set_reified_groups(self, values):
875 """ reflect reified group fields in values['groups_id'] """
876 if 'groups_id' in values:
877 # groups are already given, ignore group fields
878 for f in filter(is_reified_group, values.iterkeys()):
883 for f in values.keys():
884 if is_boolean_group(f):
885 target = add if values.pop(f) else remove
886 target.append(get_boolean_group(f))
887 elif is_boolean_groups(f):
888 if not values.pop(f):
889 remove.extend(get_boolean_groups(f))
890 elif is_selection_groups(f):
891 remove.extend(get_selection_groups(f))
892 selected = values.pop(f)
895 # update values *only* if groups are being modified, otherwise
896 # we introduce spurious changes that might break the super.write() call.
898 # remove groups in 'remove' and add groups in 'add'
899 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
901 def default_get(self, cr, uid, fields, context=None):
902 group_fields, fields = partition(is_reified_group, fields)
903 fields1 = (fields + ['groups_id']) if group_fields else fields
904 values = super(users_view, self).default_get(cr, uid, fields1, context)
905 self._get_reified_groups(group_fields, values)
908 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
910 fields = self.fields_get(cr, uid, context=context).keys()
911 group_fields, fields = partition(is_reified_group, fields)
912 if not 'groups_id' in fields:
913 fields.append('groups_id')
914 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
915 for values in (res if isinstance(res, list) else [res]):
916 self._get_reified_groups(group_fields, values)
919 def _get_reified_groups(self, fields, values):
920 """ compute the given reified group fields from values['groups_id'] """
921 gids = set(values.get('groups_id') or [])
923 if is_boolean_group(f):
924 values[f] = get_boolean_group(f) in gids
925 elif is_boolean_groups(f):
926 values[f] = not gids.isdisjoint(get_boolean_groups(f))
927 elif is_selection_groups(f):
928 selected = [gid for gid in get_selection_groups(f) if gid in gids]
929 values[f] = selected and selected[-1] or False
931 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
932 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
933 # add reified groups fields
934 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
935 if kind == 'selection':
936 # selection group field
937 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
938 res[name_selection_groups(map(int, gs))] = {
940 'string': app and app.name or _('Other'),
941 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
942 'help': '\n'.join(tips),
945 # boolean group fields
947 res[name_boolean_group(g.id)] = {
956 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: