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
31 from osv import fields,osv
32 from osv.orm import browse_record
33 from service import security
34 from tools.translate import _
36 import openerp.exceptions
42 _logger = logging.getLogger(__name__)
44 class groups(osv.osv):
46 _description = "Access Groups"
47 _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)
59 'name': fields.char('Name', size=64, required=True, translate=True),
60 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
61 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
62 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
63 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
64 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
65 'comment' : fields.text('Comment', size=250, translate=True),
66 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
67 'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
71 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
74 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
75 # add explicit ordering if search is sorted on full_name
76 if order and order.startswith('full_name'):
77 ids = super(groups, self).search(cr, uid, args, context=context)
78 gs = self.browse(cr, uid, ids, context)
79 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
80 gs = gs[offset:offset+limit] if limit else gs[offset:]
82 return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
84 def copy(self, cr, uid, id, default=None, context=None):
85 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
86 default.update({'name': _('%s (copy)')%group_name})
87 return super(groups, self).copy(cr, uid, id, default, context)
89 def write(self, cr, uid, ids, vals, context=None):
91 if vals['name'].startswith('-'):
92 raise osv.except_osv(_('Error'),
93 _('The name of the group can not start with "-"'))
94 res = super(groups, self).write(cr, uid, ids, vals, context=context)
95 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
98 def create(self, cr, uid, vals, context=None):
100 if vals['name'].startswith('-'):
101 raise osv.except_osv(_('Error'),
102 _('The name of the group can not start with "-"'))
103 gid = super(groups, self).create(cr, uid, vals, context=context)
104 if context and context.get('noadmin', False):
107 # assign this new group to user_root
108 user_obj = self.pool.get('res.users')
109 aid = user_obj.browse(cr, 1, user_obj._get_admin_id(cr))
111 aid.write({'groups_id': [(4, gid)]})
114 def unlink(self, cr, uid, ids, context=None):
116 for record in self.read(cr, uid, ids, ['users'], context=context):
118 group_users.extend(record['users'])
120 user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
121 user_names = list(set(user_names))
122 if len(user_names) >= 5:
123 user_names = user_names[:5] + ['...']
124 raise osv.except_osv(_('Warning !'),
125 _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
126 ', '.join(user_names))
127 return super(groups, self).unlink(cr, uid, ids, context=context)
129 def get_extended_interface_group(self, cr, uid, context=None):
130 data_obj = self.pool.get('ir.model.data')
131 extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
132 return data_obj.browse(cr, uid, extended_group_data_id, context=context).res_id
136 def _lang_get(self, cr, uid, context=None):
137 obj = self.pool.get('res.lang')
138 ids = obj.search(cr, uid, [('translatable','=',True)])
139 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
140 res = [(r['code'], r['name']) for r in res]
143 def _tz_get(self,cr,uid, context=None):
144 return [(x, x) for x in pytz.all_timezones]
146 class users(osv.osv):
152 WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
153 WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
154 "\"%(name)s\".\n\nYour login is %(login)s, "\
155 "you should ask your supervisor or system administrator if you "\
156 "haven't been given your password yet.\n\n"\
157 "If you aren't %(name)s, this email reached you errorneously, "\
160 def get_welcome_mail_subject(self, cr, uid, context=None):
161 """ Returns the subject of the mail new users receive (when
162 created via the res.config.users wizard), default implementation
163 is to return config_users.WELCOME_MAIL_SUBJECT
165 return self.WELCOME_MAIL_SUBJECT
166 def get_welcome_mail_body(self, cr, uid, context=None):
167 """ Returns the subject of the mail new users receive (when
168 created via the res.config.users wizard), default implementation
169 is to return config_users.WELCOME_MAIL_BODY
171 return self.WELCOME_MAIL_BODY
173 def get_current_company(self, cr, uid):
174 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)
177 def send_welcome_email(self, cr, uid, id, context=None):
178 if isinstance(id,list): id = id[0]
179 user = self.read(cr, uid, id, ['email','login','name', 'user_email'], context=context)
180 email = user['email'] or user['user_email']
182 ir_mail_server = self.pool.get('ir.mail_server')
183 msg = ir_mail_server.build_email(email_from=None, # take config default
185 subject=self.get_welcome_mail_subject(cr, uid, context=context),
186 body=(self.get_welcome_mail_body(cr, uid, context=context) % user))
187 return ir_mail_server.send_email(cr, uid, msg, context=context)
189 def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
190 """Implementation of 'view' function field setter, sets the type of interface of the users.
191 @param name: Name of the field
192 @param arg: User defined argument
193 @param value: new value returned
196 if not value or value not in ['simple','extended']:
198 group_obj = self.pool.get('res.groups')
199 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
200 # First always remove the users from the group (avoids duplication if called twice)
201 self.write(cr, uid, ids, {'groups_id': [(3, extended_group_id)]}, context=context)
202 # Then add them back if requested
203 if value == 'extended':
204 self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
207 def _get_interface_type(self, cr, uid, ids, name, args, context=None):
208 """Implementation of 'view' function field getter, returns the type of interface of the users.
209 @param field_name: Name of the field
210 @param arg: User defined argument
211 @return: Dictionary of values
213 group_obj = self.pool.get('res.groups')
214 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
215 extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
216 return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
218 def onchange_avatar(self, cr, uid, ids, value, context=None):
219 return {'value': {'avatar_stored': value, 'avatar': self._avatar_resize(cr, uid, value, context=context) } }
221 def _set_avatar(self, cr, uid, id, name, value, args, context=None):
222 return self.write(cr, uid, [id], {'avatar_stored': value}, context=context)
224 def _avatar_resize(self, cr, uid, avatar, context=None):
225 image_stream = io.BytesIO(avatar_stored.decode('base64'))
226 img = Image.open(image_stream)
227 img.thumbnail((180, 150), Image.ANTIALIAS)
228 img_stream = StringIO.StringIO()
229 img.save(img_stream, "JPEG")
230 return img_stream.getvalue().encode('base64')
232 def _get_avatar(self, cr, uid, ids, name, args, context=None):
234 for user in self.browse(cr, uid, ids, context=context):
236 result[user.id] = False
238 result[user.id] = self._avatar_resize(cr, uid, user.avatar_stored)
241 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
243 # Do not update the password if no value is provided, ignore silently.
244 # For example web client submits False values for all empty fields.
247 # To change their own password users must use the client-specific change password wizard,
248 # so that the new password is immediately used for further RPC requests, otherwise the user
249 # will face unexpected 'Access Denied' exceptions.
250 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
251 self.write(cr, uid, id, {'password': value})
253 def _get_password(self, cr, uid, ids, arg, karg, context=None):
254 return dict.fromkeys(ids, '')
257 'id': fields.integer('ID'),
258 'name': fields.char('User Name', size=64, required=True, select=True,
259 help="The new user's real name, used for searching"
260 " and most listings"),
261 'login': fields.char('Login', size=64, required=True,
262 help="Used to log into the system"),
263 '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."),
264 'new_password': fields.function(_get_password, type='char', size=64,
265 fnct_inv=_set_new_password,
266 string='Set password', help="Specify a value only when creating a user or if you're changing the user's password, "
267 "otherwise leave empty. After a change of password, the user has to login again."),
268 'user_email': fields.char('Email', size=64),
269 'signature': fields.text('Signature', size=64),
270 'avatar_stored': fields.binary('Stored 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."),
271 'avatar': fields.function(_get_avatar, fnct_inv=_set_avatar, string='Avatar', type="binary",
273 'res.users': (lambda self, cr, uid, ids, c={}: ids, ['avatar_stored'], 10),
274 }, help="Image used as avatar for the user. It is automatically resized as a 180x150 px image."),
275 'active': fields.boolean('Active'),
276 '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."),
277 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
278 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
280 # Special behavior for this field: res.company.search() will only return the companies
281 # available to the current user (should be the user's companies?), when the user_preference
283 'company_id': fields.many2one('res.company', 'Company', required=True,
284 help="The company this user is currently working for.", context={'user_preference': True}),
286 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
287 'context_lang': fields.selection(_lang_get, 'Language', required=True,
288 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."),
289 'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
290 help="The user's timezone, used to output proper date and time values inside printed reports. "
291 "It is important to set a value for this field. You should use the same timezone "
292 "that is otherwise used to pick and render date and time values: your computer's timezone."),
293 'view': fields.function(_get_interface_type, type='selection', fnct_inv=_set_interface_type,
294 selection=[('simple','Simplified'),('extended','Extended')],
295 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."),
296 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
297 'date': fields.datetime('Latest Connection', readonly=True),
300 def on_change_company_id(self, cr, uid, ids, company_id):
303 'title': _("Company Switch Warning"),
304 '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)"),
308 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
309 def override_password(o):
310 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
311 o['password'] = '********'
313 result = super(users, self).read(cr, uid, ids, fields, context, load)
314 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
316 if isinstance(ids, (int, float)):
317 result = override_password(result)
319 result = map(override_password, result)
323 def _check_company(self, cr, uid, ids, context=None):
324 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
327 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
331 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
334 def _get_email_from(self, cr, uid, ids, context=None):
335 if not isinstance(ids, list):
337 res = dict.fromkeys(ids, False)
338 for user in self.browse(cr, uid, ids, context=context):
340 res[user.id] = "%s <%s>" % (user.name, user.user_email)
343 def _get_admin_id(self, cr):
344 if self.__admin_ids.get(cr.dbname) is None:
345 ir_model_data_obj = self.pool.get('ir.model.data')
346 mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
347 self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
348 return self.__admin_ids[cr.dbname]
350 def _get_company(self,cr, uid, context=None, uid2=False):
353 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
354 company_id = user.get('company_id', False)
355 return company_id and company_id[0] or False
357 def _get_companies(self, cr, uid, context=None):
358 c = self._get_company(cr, uid, context)
363 def _get_menu(self,cr, uid, context=None):
364 dataobj = self.pool.get('ir.model.data')
366 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
367 if model != 'ir.actions.act_window':
373 def _get_group(self,cr, uid, context=None):
374 dataobj = self.pool.get('ir.model.data')
377 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
378 result.append(group_id)
379 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
380 result.append(group_id)
382 # If these groups does not exists anymore
386 def _get_avatar(self, cr, uid, context=None):
387 # default avatar file name: avatar0 -> avatar6.jpg, choose randomly
388 avatar_path = openerp.modules.get_module_resource('base', 'images', 'avatar%d.jpg' % random.randint(0, 6))
389 return self._avatar_resize(cr, uid, open(avatar_path, 'rb').read().encode('base64'))
393 'context_lang': 'en_US',
394 'avatar': _get_avatar,
396 'menu_id': _get_menu,
397 'company_id': _get_company,
398 'company_ids': _get_companies,
399 'groups_id': _get_group,
403 # User can write to a few of her own fields (but not her groups for example)
404 SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
406 def write(self, cr, uid, ids, values, context=None):
407 if not hasattr(ids, '__iter__'):
410 for key in values.keys():
411 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
414 if 'company_id' in values:
415 if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
416 del values['company_id']
417 uid = 1 # safe fields only, so we write as super-user to bypass access rights
419 res = super(users, self).write(cr, uid, ids, values, context=context)
421 # clear caches linked to the users
422 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
423 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
426 if db in self._uid_cache:
428 if id in self._uid_cache[db]:
429 del self._uid_cache[db][id]
433 def unlink(self, cr, uid, ids, context=None):
435 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, ...)'))
437 if db in self._uid_cache:
439 if id in self._uid_cache[db]:
440 del self._uid_cache[db][id]
441 return super(users, self).unlink(cr, uid, ids, context=context)
443 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
450 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
452 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
453 return self.name_get(cr, user, ids)
455 def copy(self, cr, uid, id, default=None, context=None):
456 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
459 copy_pattern = _("%s (copy)")
460 copydef = dict(login=(copy_pattern % user2copy['login']),
461 name=(copy_pattern % user2copy['name']),
463 copydef.update(default)
464 return super(users, self).copy(cr, uid, id, copydef, context)
466 def context_get(self, cr, uid, context=None):
467 user = self.browse(cr, uid, uid, context)
469 for k in self._columns.keys():
470 if k.startswith('context_'):
471 res = getattr(user,k) or False
472 if isinstance(res, browse_record):
474 result[k[8:]] = res or False
477 def action_get(self, cr, uid, context=None):
478 dataobj = self.pool.get('ir.model.data')
479 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
480 return dataobj.browse(cr, uid, data_id, context=context).res_id
482 def authenticate(self, db, login, password, user_agent_env):
483 """Verifies and returns the user ID corresponding to the given
484 ``login`` and ``password`` combination, or False if there was
487 :param str db: the database on which user is trying to authenticate
488 :param str login: username
489 :param str password: user password
490 :param dict user_agent_env: environment dictionary describing any
491 relevant environment attributes
493 uid = self.login(db, login, password)
494 if uid == openerp.SUPERUSER_ID:
495 # Successfully logged in as admin!
496 # Attempt to guess the web base url...
497 if user_agent_env and user_agent_env.get('base_location'):
498 cr = pooler.get_db(db).cursor()
500 self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url',
501 user_agent_env['base_location'])
504 _logger.exception("Failed to update web.base.url configuration parameter")
509 def login(self, db, login, password):
512 cr = pooler.get_db(db).cursor()
514 # autocommit: our single request will be performed atomically.
515 # (In this way, there is no opportunity to have two transactions
516 # interleaving their cr.execute()..cr.commit() calls and have one
517 # of them rolled back due to a concurrent access.)
518 # We effectively unconditionally write the res_users line.
520 # Even w/ autocommit there's a chance the user row will be locked,
521 # in which case we can't delay the login just for the purpose of
522 # update the last login date - hence we use FOR UPDATE NOWAIT to
523 # try to get the lock - fail-fast
524 cr.execute("""SELECT id from res_users
525 WHERE login=%s AND password=%s
526 AND active FOR UPDATE NOWAIT""",
527 (tools.ustr(login), tools.ustr(password)))
528 cr.execute("""UPDATE res_users
529 SET date = now() AT TIME ZONE 'UTC'
530 WHERE login=%s AND password=%s AND active
532 (tools.ustr(login), tools.ustr(password)))
534 # Failing to acquire the lock on the res_users row probably means
535 # another request is holding it. No big deal, we don't want to
536 # prevent/delay login in that case. It will also have been logged
537 # as a SQL error, if anyone cares.
538 cr.execute("""SELECT id from res_users
539 WHERE login=%s AND password=%s
541 (tools.ustr(login), tools.ustr(password)))
549 def check_super(self, passwd):
550 if passwd == tools.config['admin_passwd']:
553 raise openerp.exceptions.AccessDenied()
555 def check(self, db, uid, passwd):
556 """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
557 raise an exception if it is not."""
559 # empty passwords disallowed for obvious security reasons
560 raise openerp.exceptions.AccessDenied()
561 if self._uid_cache.get(db, {}).get(uid) == passwd:
563 cr = pooler.get_db(db).cursor()
565 cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
566 (int(uid), passwd, True))
567 res = cr.fetchone()[0]
569 raise openerp.exceptions.AccessDenied()
570 if self._uid_cache.has_key(db):
571 ulist = self._uid_cache[db]
574 self._uid_cache[db] = {uid:passwd}
578 def access(self, db, uid, passwd, sec_level, ids):
581 cr = pooler.get_db(db).cursor()
583 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
586 raise openerp.exceptions.AccessDenied()
591 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
592 """Change current user password. Old password must be provided explicitly
593 to prevent hijacking an existing user session, or for cases where the cleartext
594 password is not used to authenticate requests.
597 :raise: openerp.exceptions.AccessDenied when old password is wrong
598 :raise: except_osv when new password is not set or empty
600 self.check(cr.dbname, uid, old_passwd)
602 return self.write(cr, uid, uid, {'password': new_passwd})
603 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
610 # Extension of res.groups and res.users with a relation for "implied" or
611 # "inherited" groups. Once a user belongs to a group, it automatically belongs
612 # to the implied groups (transitively).
616 """ A cset (constrained set) is a set of elements that may be constrained to
617 be a subset of other csets. Elements added to a cset are automatically
618 added to its supersets. Cycles in the subset constraints are supported.
620 def __init__(self, xs):
621 self.supersets = set()
622 self.elements = set(xs)
623 def subsetof(self, other):
624 if other is not self:
625 self.supersets.add(other)
626 other.update(self.elements)
627 def update(self, xs):
628 xs = set(xs) - self.elements
629 if xs: # xs will eventually be empty in case of a cycle
630 self.elements.update(xs)
631 for s in self.supersets:
634 return iter(self.elements)
637 """ return the concatenation of a list of iterables """
639 for l in ls: res.extend(l)
644 class groups_implied(osv.osv):
645 _inherit = 'res.groups'
647 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
648 "computes the transitive closure of relation implied_ids"
649 memo = {} # use a memo for performance and cycle avoidance
652 memo[g] = cset(g.implied_ids)
653 for h in g.implied_ids:
654 computed_set(h).subsetof(memo[g])
658 for g in self.browse(cr, 1, ids, context):
659 res[g.id] = map(int, computed_set(g))
663 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
664 string='Inherits', help='Users of this group automatically inherit those groups'),
665 'trans_implied_ids': fields.function(_get_trans_implied,
666 type='many2many', relation='res.groups', string='Transitively inherits'),
669 def create(self, cr, uid, values, context=None):
670 users = values.pop('users', None)
671 gid = super(groups_implied, self).create(cr, uid, values, context)
673 # delegate addition of users to add implied groups
674 self.write(cr, uid, [gid], {'users': users}, context)
677 def write(self, cr, uid, ids, values, context=None):
678 res = super(groups_implied, self).write(cr, uid, ids, values, context)
679 if values.get('users') or values.get('implied_ids'):
680 # add all implied groups (to all users of each group)
681 for g in self.browse(cr, uid, ids):
682 gids = map(int, g.trans_implied_ids)
683 vals = {'users': [(4, u.id) for u in g.users]}
684 super(groups_implied, self).write(cr, uid, gids, vals, context)
689 class users_implied(osv.osv):
690 _inherit = 'res.users'
692 def create(self, cr, uid, values, context=None):
693 groups = values.pop('groups_id', None)
694 user_id = super(users_implied, self).create(cr, uid, values, context)
696 # delegate addition of groups to add implied groups
697 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
700 def write(self, cr, uid, ids, values, context=None):
701 if not isinstance(ids,list):
703 res = super(users_implied, self).write(cr, uid, ids, values, context)
704 if values.get('groups_id'):
705 # add implied groups for all users
706 for user in self.browse(cr, uid, ids):
707 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
708 vals = {'groups_id': [(4, g.id) for g in gs]}
709 super(users_implied, self).write(cr, uid, [user.id], vals, context)
717 # Extension of res.groups and res.users for the special groups view in the users
718 # form. This extension presents groups with selection and boolean widgets:
719 # - Groups are shown by application, with boolean and/or selection fields.
720 # Selection fields typically defines a role "Name" for the given application.
721 # - Uncategorized groups are presented as boolean fields and grouped in a
724 # The user form view is modified by an inherited view (base.user_groups_view);
725 # the inherited view replaces the field 'groups_id' by a set of reified group
726 # fields (boolean or selection fields). The arch of that view is regenerated
727 # each time groups are changed.
729 # Naming conventions for reified groups fields:
730 # - boolean field 'in_group_ID' is True iff
731 # ID is in 'groups_id'
732 # - boolean field 'in_groups_ID1_..._IDk' is True iff
733 # any of ID1, ..., IDk is in 'groups_id'
734 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
735 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
737 def name_boolean_group(id): return 'in_group_' + str(id)
738 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
739 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
741 def is_boolean_group(name): return name.startswith('in_group_')
742 def is_boolean_groups(name): return name.startswith('in_groups_')
743 def is_selection_groups(name): return name.startswith('sel_groups_')
744 def is_reified_group(name):
745 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
747 def get_boolean_group(name): return int(name[9:])
748 def get_boolean_groups(name): return map(int, name[10:].split('_'))
749 def get_selection_groups(name): return map(int, name[11:].split('_'))
751 def partition(f, xs):
752 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
755 (yes if f(x) else nos).append(x)
760 class groups_view(osv.osv):
761 _inherit = 'res.groups'
763 def create(self, cr, uid, values, context=None):
764 res = super(groups_view, self).create(cr, uid, values, context)
765 self.update_user_groups_view(cr, uid, context)
768 def write(self, cr, uid, ids, values, context=None):
769 res = super(groups_view, self).write(cr, uid, ids, values, context)
770 self.update_user_groups_view(cr, uid, context)
773 def unlink(self, cr, uid, ids, context=None):
774 res = super(groups_view, self).unlink(cr, uid, ids, context)
775 self.update_user_groups_view(cr, uid, context)
778 def update_user_groups_view(self, cr, uid, context=None):
779 # the view with id 'base.user_groups_view' inherits the user form view,
780 # and introduces the reified group fields
781 view = self.get_user_groups_view(cr, uid, context)
783 xml = u"""<?xml version="1.0" encoding="utf-8"?>
784 <!-- GENERATED AUTOMATICALLY BY GROUPS -->
785 <field name="groups_id" position="replace">
791 xml1.append('<separator string="%s" colspan="4"/>' % _('Applications'))
792 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
793 if kind == 'selection':
794 # application name with a selection field
795 field_name = name_selection_groups(map(int, gs))
796 xml1.append('<field name="%s"/>' % field_name)
797 xml1.append('<newline/>')
799 # application separator with boolean fields
800 app_name = app and app.name or _('Other')
801 xml2.append('<separator string="%s" colspan="4"/>' % app_name)
803 field_name = name_boolean_group(g.id)
804 xml2.append('<field name="%s"/>' % field_name)
805 view.write({'arch': xml % ('\n'.join(xml1), '\n'.join(xml2))})
808 def get_user_groups_view(self, cr, uid, context=None):
810 view = self.pool.get('ir.model.data').get_object(cr, 1, 'base', 'user_groups_view', context)
811 assert view and view._table_name == 'ir.ui.view'
816 def get_application_groups(self, cr, uid, domain=None, context=None):
817 return self.search(cr, uid, domain or [])
819 def get_groups_by_application(self, cr, uid, context=None):
820 """ return all groups classified by application (module category), as a list of pairs:
821 [(app, kind, [group, ...]), ...],
822 where app and group are browse records, and kind is either 'boolean' or 'selection'.
823 Applications are given in sequence order. If kind is 'selection', the groups are
824 given in reverse implication order.
828 # determine sequence order: a group should appear after its implied groups
829 order = dict.fromkeys(gs, 0)
831 for h in gs.intersection(g.trans_implied_ids):
833 # check whether order is total, i.e., sequence orders are distinct
834 if len(set(order.itervalues())) == len(gs):
835 return sorted(gs, key=lambda g: order[g])
838 # classify all groups by application
839 gids = self.get_application_groups(cr, uid, context=context)
840 by_app, others = {}, []
841 for g in self.browse(cr, uid, gids, context):
843 by_app.setdefault(g.category_id, []).append(g)
848 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
850 gs = linearized(by_app[app])
852 res.append((app, 'selection', gs))
854 res.append((app, 'boolean', by_app[app]))
856 res.append((False, 'boolean', others))
861 class users_view(osv.osv):
862 _inherit = 'res.users'
864 def create(self, cr, uid, values, context=None):
865 self._set_reified_groups(values)
866 return super(users_view, self).create(cr, uid, values, context)
868 def write(self, cr, uid, ids, values, context=None):
869 self._set_reified_groups(values)
870 return super(users_view, self).write(cr, uid, ids, values, context)
872 def _set_reified_groups(self, values):
873 """ reflect reified group fields in values['groups_id'] """
874 if 'groups_id' in values:
875 # groups are already given, ignore group fields
876 for f in filter(is_reified_group, values.iterkeys()):
881 for f in values.keys():
882 if is_boolean_group(f):
883 target = add if values.pop(f) else remove
884 target.append(get_boolean_group(f))
885 elif is_boolean_groups(f):
886 if not values.pop(f):
887 remove.extend(get_boolean_groups(f))
888 elif is_selection_groups(f):
889 remove.extend(get_selection_groups(f))
890 selected = values.pop(f)
893 # update values *only* if groups are being modified, otherwise
894 # we introduce spurious changes that might break the super.write() call.
896 # remove groups in 'remove' and add groups in 'add'
897 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
899 def default_get(self, cr, uid, fields, context=None):
900 group_fields, fields = partition(is_reified_group, fields)
901 fields1 = (fields + ['groups_id']) if group_fields else fields
902 values = super(users_view, self).default_get(cr, uid, fields1, context)
903 self._get_reified_groups(group_fields, values)
906 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
908 fields = self.fields_get(cr, uid, context=context).keys()
909 group_fields, fields = partition(is_reified_group, fields)
910 if not 'groups_id' in fields:
911 fields.append('groups_id')
912 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
913 for values in (res if isinstance(res, list) else [res]):
914 self._get_reified_groups(group_fields, values)
917 def _get_reified_groups(self, fields, values):
918 """ compute the given reified group fields from values['groups_id'] """
919 gids = set(values.get('groups_id') or [])
921 if is_boolean_group(f):
922 values[f] = get_boolean_group(f) in gids
923 elif is_boolean_groups(f):
924 values[f] = not gids.isdisjoint(get_boolean_groups(f))
925 elif is_selection_groups(f):
926 selected = [gid for gid in get_selection_groups(f) if gid in gids]
927 values[f] = selected and selected[-1] or False
929 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
930 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
931 # add reified groups fields
932 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
933 if kind == 'selection':
934 # selection group field
935 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
936 res[name_selection_groups(map(int, gs))] = {
938 'string': app and app.name or _('Other'),
939 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
940 'help': '\n'.join(tips),
943 # boolean group fields
945 res[name_boolean_group(g.id)] = {
954 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: