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
37 from lxml import etree
38 from lxml.builder import E
45 _logger = logging.getLogger(__name__)
47 class groups(osv.osv):
49 _description = "Access Groups"
50 _rec_name = 'full_name'
52 def _get_full_name(self, cr, uid, ids, field, arg, context=None):
54 for g in self.browse(cr, uid, ids, context):
56 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
62 'name': fields.char('Name', size=64, required=True, translate=True),
63 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
64 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
65 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
66 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
67 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
68 'comment' : fields.text('Comment', size=250, translate=True),
69 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
70 'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
74 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
77 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
78 # add explicit ordering if search is sorted on full_name
79 if order and order.startswith('full_name'):
80 ids = super(groups, self).search(cr, uid, args, context=context)
81 gs = self.browse(cr, uid, ids, context)
82 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
83 gs = gs[offset:offset+limit] if limit else gs[offset:]
85 return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
87 def copy(self, cr, uid, id, default=None, context=None):
88 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
89 default.update({'name': _('%s (copy)')%group_name})
90 return super(groups, self).copy(cr, uid, id, default, context)
92 def write(self, cr, uid, ids, vals, context=None):
94 if vals['name'].startswith('-'):
95 raise osv.except_osv(_('Error'),
96 _('The name of the group can not start with "-"'))
97 res = super(groups, self).write(cr, uid, ids, vals, context=context)
98 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
101 def create(self, cr, uid, vals, context=None):
103 if vals['name'].startswith('-'):
104 raise osv.except_osv(_('Error'),
105 _('The name of the group can not start with "-"'))
106 gid = super(groups, self).create(cr, uid, vals, context=context)
107 if context and context.get('noadmin', False):
110 # assign this new group to user_root
111 user_obj = self.pool.get('res.users')
112 aid = user_obj.browse(cr, 1, user_obj._get_admin_id(cr))
114 aid.write({'groups_id': [(4, gid)]})
117 def unlink(self, cr, uid, ids, context=None):
119 for record in self.read(cr, uid, ids, ['users'], context=context):
121 group_users.extend(record['users'])
123 user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
124 user_names = list(set(user_names))
125 if len(user_names) >= 5:
126 user_names = user_names[:5] + ['...']
127 raise osv.except_osv(_('Warning !'),
128 _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
129 ', '.join(user_names))
130 return super(groups, self).unlink(cr, uid, ids, context=context)
132 def get_extended_interface_group(self, cr, uid, context=None):
133 data_obj = self.pool.get('ir.model.data')
134 extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
135 return data_obj.browse(cr, uid, extended_group_data_id, context=context).res_id
139 def _lang_get(self, cr, uid, context=None):
140 obj = self.pool.get('res.lang')
141 ids = obj.search(cr, uid, [('translatable','=',True)])
142 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
143 res = [(r['code'], r['name']) for r in res]
146 def _tz_get(self,cr,uid, context=None):
147 return [(x, x) for x in pytz.all_timezones]
149 class users(osv.osv):
155 WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
156 WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
157 "\"%(name)s\".\n\nYour login is %(login)s, "\
158 "you should ask your supervisor or system administrator if you "\
159 "haven't been given your password yet.\n\n"\
160 "If you aren't %(name)s, this email reached you errorneously, "\
163 def get_welcome_mail_subject(self, cr, uid, context=None):
164 """ Returns the subject of the mail new users receive (when
165 created via the res.config.users wizard), default implementation
166 is to return config_users.WELCOME_MAIL_SUBJECT
168 return self.WELCOME_MAIL_SUBJECT
169 def get_welcome_mail_body(self, cr, uid, context=None):
170 """ Returns the subject of the mail new users receive (when
171 created via the res.config.users wizard), default implementation
172 is to return config_users.WELCOME_MAIL_BODY
174 return self.WELCOME_MAIL_BODY
176 def get_current_company(self, cr, uid):
177 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)
180 def send_welcome_email(self, cr, uid, id, context=None):
181 if isinstance(id,list): id = id[0]
182 user = self.read(cr, uid, id, ['email','login','name', 'user_email'], context=context)
183 email = user['email'] or user['user_email']
185 ir_mail_server = self.pool.get('ir.mail_server')
186 msg = ir_mail_server.build_email(email_from=None, # take config default
188 subject=self.get_welcome_mail_subject(cr, uid, context=context),
189 body=(self.get_welcome_mail_body(cr, uid, context=context) % user))
190 return ir_mail_server.send_email(cr, uid, msg, context=context)
192 def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
193 """Implementation of 'view' function field setter, sets the type of interface of the users.
194 @param name: Name of the field
195 @param arg: User defined argument
196 @param value: new value returned
199 if not value or value not in ['simple','extended']:
201 group_obj = self.pool.get('res.groups')
202 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
203 # First always remove the users from the group (avoids duplication if called twice)
204 self.write(cr, uid, ids, {'groups_id': [(3, extended_group_id)]}, context=context)
205 # Then add them back if requested
206 if value == 'extended':
207 self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
210 def _get_interface_type(self, cr, uid, ids, name, args, context=None):
211 """Implementation of 'view' function field getter, returns the type of interface of the users.
212 @param field_name: Name of the field
213 @param arg: User defined argument
214 @return: Dictionary of values
216 group_obj = self.pool.get('res.groups')
217 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
218 extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
219 return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
221 def onchange_avatar(self, cr, uid, ids, value, context=None):
222 return {'value': {'avatar_stored': self._avatar_resize(cr, uid, value, 360, 300, context=context), 'avatar': self._avatar_resize(cr, uid, value, context=context) } }
224 def _set_avatar(self, cr, uid, id, name, value, args, context=None):
225 return self.write(cr, uid, [id], {'avatar_stored': value}, context=context)
227 def _avatar_resize(self, cr, uid, avatar, height=180, width=150, context=None):
228 image_stream = io.BytesIO(avatar.decode('base64'))
229 img = Image.open(image_stream)
230 img.thumbnail((height, width), Image.ANTIALIAS)
231 img_stream = StringIO.StringIO()
232 img.save(img_stream, "JPEG")
233 return img_stream.getvalue().encode('base64')
235 def _get_avatar(self, cr, uid, ids, name, args, context=None):
236 result = dict.fromkeys(ids, False)
237 for user in self.browse(cr, uid, ids, context=context):
238 if user.avatar_stored:
239 result[user.id] = self._avatar_resize(cr, uid, user.avatar_stored, context=context)
242 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
244 # Do not update the password if no value is provided, ignore silently.
245 # For example web client submits False values for all empty fields.
248 # To change their own password users must use the client-specific change password wizard,
249 # so that the new password is immediately used for further RPC requests, otherwise the user
250 # will face unexpected 'Access Denied' exceptions.
251 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
252 self.write(cr, uid, id, {'password': value})
254 def _get_password(self, cr, uid, ids, arg, karg, context=None):
255 return dict.fromkeys(ids, '')
258 'id': fields.integer('ID'),
259 'name': fields.char('User Name', size=64, required=True, select=True,
260 help="The new user's real name, used for searching"
261 " and most listings"),
262 'login': fields.char('Login', size=64, required=True,
263 help="Used to log into the system"),
264 '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."),
265 'new_password': fields.function(_get_password, type='char', size=64,
266 fnct_inv=_set_new_password,
267 string='Set password', help="Specify a value only when creating a user or if you're changing the user's password, "
268 "otherwise leave empty. After a change of password, the user has to login again."),
269 'user_email': fields.char('Email', size=64),
270 'signature': fields.text('Signature', size=64),
271 '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."),
272 'avatar': fields.function(_get_avatar, fnct_inv=_set_avatar, string='Avatar', type="binary",
274 'res.users': (lambda self, cr, uid, ids, c={}: ids, ['avatar_stored'], 10),
275 }, help="Image used as avatar for the user. It is automatically resized as a 180x150 px image."),
276 'active': fields.boolean('Active'),
277 '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."),
278 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
279 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
281 # Special behavior for this field: res.company.search() will only return the companies
282 # available to the current user (should be the user's companies?), when the user_preference
284 'company_id': fields.many2one('res.company', 'Company', required=True,
285 help="The company this user is currently working for.", context={'user_preference': True}),
287 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
288 'context_lang': fields.selection(_lang_get, 'Language', required=True,
289 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."),
290 'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
291 help="The user's timezone, used to output proper date and time values inside printed reports. "
292 "It is important to set a value for this field. You should use the same timezone "
293 "that is otherwise used to pick and render date and time values: your computer's timezone."),
294 'view': fields.function(_get_interface_type, type='selection', fnct_inv=_set_interface_type,
295 selection=[('simple','Simplified'),('extended','Extended')],
296 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."),
297 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
298 'date': fields.datetime('Latest Connection', readonly=True),
301 def on_change_company_id(self, cr, uid, ids, company_id):
304 'title': _("Company Switch Warning"),
305 '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)"),
309 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
310 def override_password(o):
311 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
312 o['password'] = '********'
314 result = super(users, self).read(cr, uid, ids, fields, context, load)
315 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
317 if isinstance(ids, (int, float)):
318 result = override_password(result)
320 result = map(override_password, result)
324 def _check_company(self, cr, uid, ids, context=None):
325 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
328 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
332 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
335 def _get_email_from(self, cr, uid, ids, context=None):
336 if not isinstance(ids, list):
338 res = dict.fromkeys(ids, False)
339 for user in self.browse(cr, uid, ids, context=context):
341 res[user.id] = "%s <%s>" % (user.name, user.user_email)
344 def _get_admin_id(self, cr):
345 if self.__admin_ids.get(cr.dbname) is None:
346 ir_model_data_obj = self.pool.get('ir.model.data')
347 mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
348 self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
349 return self.__admin_ids[cr.dbname]
351 def _get_company(self,cr, uid, context=None, uid2=False):
354 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
355 company_id = user.get('company_id', False)
356 return company_id and company_id[0] or False
358 def _get_companies(self, cr, uid, context=None):
359 c = self._get_company(cr, uid, context)
364 def _get_menu(self,cr, uid, context=None):
365 dataobj = self.pool.get('ir.model.data')
367 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
368 if model != 'ir.actions.act_window':
374 def _get_group(self,cr, uid, context=None):
375 dataobj = self.pool.get('ir.model.data')
378 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
379 result.append(group_id)
380 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
381 result.append(group_id)
383 # If these groups does not exists anymore
387 def _get_avatar(self, cr, uid, context=None):
388 # default avatar file name: avatar0 -> avatar6.jpg, choose randomly
389 avatar_path = openerp.modules.get_module_resource('base', 'images', 'avatar%d.jpg' % random.randint(0, 6))
390 return self._avatar_resize(cr, uid, open(avatar_path, 'rb').read().encode('base64'), context=context)
394 'context_lang': 'en_US',
395 'avatar': _get_avatar,
397 'menu_id': _get_menu,
398 'company_id': _get_company,
399 'company_ids': _get_companies,
400 'groups_id': _get_group,
404 # User can write to a few of her own fields (but not her groups for example)
405 SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
407 def write(self, cr, uid, ids, values, context=None):
408 if not hasattr(ids, '__iter__'):
411 for key in values.keys():
412 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
415 if 'company_id' in values:
416 if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
417 del values['company_id']
418 uid = 1 # safe fields only, so we write as super-user to bypass access rights
420 res = super(users, self).write(cr, uid, ids, values, context=context)
422 # clear caches linked to the users
423 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
424 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
427 if db in self._uid_cache:
429 if id in self._uid_cache[db]:
430 del self._uid_cache[db][id]
434 def unlink(self, cr, uid, ids, context=None):
436 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, ...)'))
438 if db in self._uid_cache:
440 if id in self._uid_cache[db]:
441 del self._uid_cache[db][id]
442 return super(users, self).unlink(cr, uid, ids, context=context)
444 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
451 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
453 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
454 return self.name_get(cr, user, ids)
456 def copy(self, cr, uid, id, default=None, context=None):
457 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
460 copy_pattern = _("%s (copy)")
461 copydef = dict(login=(copy_pattern % user2copy['login']),
462 name=(copy_pattern % user2copy['name']),
464 copydef.update(default)
465 return super(users, self).copy(cr, uid, id, copydef, context)
467 def context_get(self, cr, uid, context=None):
468 user = self.browse(cr, uid, uid, context)
470 for k in self._columns.keys():
471 if k.startswith('context_'):
472 res = getattr(user,k) or False
473 if isinstance(res, browse_record):
475 result[k[8:]] = res or False
478 def action_get(self, cr, uid, context=None):
479 dataobj = self.pool.get('ir.model.data')
480 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
481 return dataobj.browse(cr, uid, data_id, context=context).res_id
483 def authenticate(self, db, login, password, user_agent_env):
484 """Verifies and returns the user ID corresponding to the given
485 ``login`` and ``password`` combination, or False if there was
488 :param str db: the database on which user is trying to authenticate
489 :param str login: username
490 :param str password: user password
491 :param dict user_agent_env: environment dictionary describing any
492 relevant environment attributes
494 uid = self.login(db, login, password)
495 if uid == openerp.SUPERUSER_ID:
496 # Successfully logged in as admin!
497 # Attempt to guess the web base url...
498 if user_agent_env and user_agent_env.get('base_location'):
499 cr = pooler.get_db(db).cursor()
501 self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url',
502 user_agent_env['base_location'])
505 _logger.exception("Failed to update web.base.url configuration parameter")
510 def login(self, db, login, password):
513 cr = pooler.get_db(db).cursor()
515 # autocommit: our single request will be performed atomically.
516 # (In this way, there is no opportunity to have two transactions
517 # interleaving their cr.execute()..cr.commit() calls and have one
518 # of them rolled back due to a concurrent access.)
519 # We effectively unconditionally write the res_users line.
521 # Even w/ autocommit there's a chance the user row will be locked,
522 # in which case we can't delay the login just for the purpose of
523 # update the last login date - hence we use FOR UPDATE NOWAIT to
524 # try to get the lock - fail-fast
525 cr.execute("""SELECT id from res_users
526 WHERE login=%s AND password=%s
527 AND active FOR UPDATE NOWAIT""",
528 (tools.ustr(login), tools.ustr(password)))
529 cr.execute("""UPDATE res_users
530 SET date = now() AT TIME ZONE 'UTC'
531 WHERE login=%s AND password=%s AND active
533 (tools.ustr(login), tools.ustr(password)))
535 # Failing to acquire the lock on the res_users row probably means
536 # another request is holding it. No big deal, we don't want to
537 # prevent/delay login in that case. It will also have been logged
538 # as a SQL error, if anyone cares.
539 cr.execute("""SELECT id from res_users
540 WHERE login=%s AND password=%s
542 (tools.ustr(login), tools.ustr(password)))
550 def check_super(self, passwd):
551 if passwd == tools.config['admin_passwd']:
554 raise openerp.exceptions.AccessDenied()
556 def check(self, db, uid, passwd):
557 """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
558 raise an exception if it is not."""
560 # empty passwords disallowed for obvious security reasons
561 raise openerp.exceptions.AccessDenied()
562 if self._uid_cache.get(db, {}).get(uid) == passwd:
564 cr = pooler.get_db(db).cursor()
566 cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
567 (int(uid), passwd, True))
568 res = cr.fetchone()[0]
570 raise openerp.exceptions.AccessDenied()
571 if self._uid_cache.has_key(db):
572 ulist = self._uid_cache[db]
575 self._uid_cache[db] = {uid:passwd}
579 def access(self, db, uid, passwd, sec_level, ids):
582 cr = pooler.get_db(db).cursor()
584 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
587 raise openerp.exceptions.AccessDenied()
592 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
593 """Change current user password. Old password must be provided explicitly
594 to prevent hijacking an existing user session, or for cases where the cleartext
595 password is not used to authenticate requests.
598 :raise: openerp.exceptions.AccessDenied when old password is wrong
599 :raise: except_osv when new password is not set or empty
601 self.check(cr.dbname, uid, old_passwd)
603 return self.write(cr, uid, uid, {'password': new_passwd})
604 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
611 # Extension of res.groups and res.users with a relation for "implied" or
612 # "inherited" groups. Once a user belongs to a group, it automatically belongs
613 # to the implied groups (transitively).
617 """ A cset (constrained set) is a set of elements that may be constrained to
618 be a subset of other csets. Elements added to a cset are automatically
619 added to its supersets. Cycles in the subset constraints are supported.
621 def __init__(self, xs):
622 self.supersets = set()
623 self.elements = set(xs)
624 def subsetof(self, other):
625 if other is not self:
626 self.supersets.add(other)
627 other.update(self.elements)
628 def update(self, xs):
629 xs = set(xs) - self.elements
630 if xs: # xs will eventually be empty in case of a cycle
631 self.elements.update(xs)
632 for s in self.supersets:
635 return iter(self.elements)
638 """ return the concatenation of a list of iterables """
640 for l in ls: res.extend(l)
645 class groups_implied(osv.osv):
646 _inherit = 'res.groups'
648 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
649 "computes the transitive closure of relation implied_ids"
650 memo = {} # use a memo for performance and cycle avoidance
653 memo[g] = cset(g.implied_ids)
654 for h in g.implied_ids:
655 computed_set(h).subsetof(memo[g])
659 for g in self.browse(cr, 1, ids, context):
660 res[g.id] = map(int, computed_set(g))
664 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
665 string='Inherits', help='Users of this group automatically inherit those groups'),
666 'trans_implied_ids': fields.function(_get_trans_implied,
667 type='many2many', relation='res.groups', string='Transitively inherits'),
670 def create(self, cr, uid, values, context=None):
671 users = values.pop('users', None)
672 gid = super(groups_implied, self).create(cr, uid, values, context)
674 # delegate addition of users to add implied groups
675 self.write(cr, uid, [gid], {'users': users}, context)
678 def write(self, cr, uid, ids, values, context=None):
679 res = super(groups_implied, self).write(cr, uid, ids, values, context)
680 if values.get('users') or values.get('implied_ids'):
681 # add all implied groups (to all users of each group)
682 for g in self.browse(cr, uid, ids):
683 gids = map(int, g.trans_implied_ids)
684 vals = {'users': [(4, u.id) for u in g.users]}
685 super(groups_implied, self).write(cr, uid, gids, vals, context)
690 class users_implied(osv.osv):
691 _inherit = 'res.users'
693 def create(self, cr, uid, values, context=None):
694 groups = values.pop('groups_id', None)
695 user_id = super(users_implied, self).create(cr, uid, values, context)
697 # delegate addition of groups to add implied groups
698 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
701 def write(self, cr, uid, ids, values, context=None):
702 if not isinstance(ids,list):
704 res = super(users_implied, self).write(cr, uid, ids, values, context)
705 if values.get('groups_id'):
706 # add implied groups for all users
707 for user in self.browse(cr, uid, ids):
708 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
709 vals = {'groups_id': [(4, g.id) for g in gs]}
710 super(users_implied, self).write(cr, uid, [user.id], vals, context)
718 # Extension of res.groups and res.users for the special groups view in the users
719 # form. This extension presents groups with selection and boolean widgets:
720 # - Groups are shown by application, with boolean and/or selection fields.
721 # Selection fields typically defines a role "Name" for the given application.
722 # - Uncategorized groups are presented as boolean fields and grouped in a
725 # The user form view is modified by an inherited view (base.user_groups_view);
726 # the inherited view replaces the field 'groups_id' by a set of reified group
727 # fields (boolean or selection fields). The arch of that view is regenerated
728 # each time groups are changed.
730 # Naming conventions for reified groups fields:
731 # - boolean field 'in_group_ID' is True iff
732 # ID is in 'groups_id'
733 # - boolean field 'in_groups_ID1_..._IDk' is True iff
734 # any of ID1, ..., IDk is in 'groups_id'
735 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
736 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
738 def name_boolean_group(id): return 'in_group_' + str(id)
739 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
740 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
742 def is_boolean_group(name): return name.startswith('in_group_')
743 def is_boolean_groups(name): return name.startswith('in_groups_')
744 def is_selection_groups(name): return name.startswith('sel_groups_')
745 def is_reified_group(name):
746 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
748 def get_boolean_group(name): return int(name[9:])
749 def get_boolean_groups(name): return map(int, name[10:].split('_'))
750 def get_selection_groups(name): return map(int, name[11:].split('_'))
752 def partition(f, xs):
753 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
756 (yes if f(x) else nos).append(x)
761 class groups_view(osv.osv):
762 _inherit = 'res.groups'
764 def create(self, cr, uid, values, context=None):
765 res = super(groups_view, self).create(cr, uid, values, context)
766 self.update_user_groups_view(cr, uid, context)
769 def write(self, cr, uid, ids, values, context=None):
770 res = super(groups_view, self).write(cr, uid, ids, values, context)
771 self.update_user_groups_view(cr, uid, context)
774 def unlink(self, cr, uid, ids, context=None):
775 res = super(groups_view, self).unlink(cr, uid, ids, context)
776 self.update_user_groups_view(cr, uid, context)
779 def update_user_groups_view(self, cr, uid, context=None):
780 # the view with id 'base.user_groups_view' inherits the user form view,
781 # and introduces the reified group fields
782 view = self.get_user_groups_view(cr, uid, context)
786 xml1.append(E.separator(string=_('Application'), colspan="4"))
787 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
788 if kind == 'selection':
789 # application name with a selection field
790 field_name = name_selection_groups(map(int, gs))
791 xml1.append(E.field(name=field_name))
792 xml1.append(E.newline())
794 # application separator with boolean fields
795 app_name = app and app.name or _('Other')
796 xml2.append(E.separator(string=app_name, colspan="4"))
798 field_name = name_boolean_group(g.id)
799 xml2.append(E.field(name=field_name))
801 xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
802 xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
803 xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
804 view.write({'arch': xml_content})
807 def get_user_groups_view(self, cr, uid, context=None):
809 view = self.pool.get('ir.model.data').get_object(cr, 1, 'base', 'user_groups_view', context)
810 assert view and view._table_name == 'ir.ui.view'
815 def get_application_groups(self, cr, uid, domain=None, context=None):
816 return self.search(cr, uid, domain or [])
818 def get_groups_by_application(self, cr, uid, context=None):
819 """ return all groups classified by application (module category), as a list of pairs:
820 [(app, kind, [group, ...]), ...],
821 where app and group are browse records, and kind is either 'boolean' or 'selection'.
822 Applications are given in sequence order. If kind is 'selection', the groups are
823 given in reverse implication order.
827 # determine sequence order: a group should appear after its implied groups
828 order = dict.fromkeys(gs, 0)
830 for h in gs.intersection(g.trans_implied_ids):
832 # check whether order is total, i.e., sequence orders are distinct
833 if len(set(order.itervalues())) == len(gs):
834 return sorted(gs, key=lambda g: order[g])
837 # classify all groups by application
838 gids = self.get_application_groups(cr, uid, context=context)
839 by_app, others = {}, []
840 for g in self.browse(cr, uid, gids, context):
842 by_app.setdefault(g.category_id, []).append(g)
847 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
849 gs = linearized(by_app[app])
851 res.append((app, 'selection', gs))
853 res.append((app, 'boolean', by_app[app]))
855 res.append((False, 'boolean', others))
860 class users_view(osv.osv):
861 _inherit = 'res.users'
863 def create(self, cr, uid, values, context=None):
864 self._set_reified_groups(values)
865 return super(users_view, self).create(cr, uid, values, context)
867 def write(self, cr, uid, ids, values, context=None):
868 self._set_reified_groups(values)
869 return super(users_view, self).write(cr, uid, ids, values, context)
871 def _set_reified_groups(self, values):
872 """ reflect reified group fields in values['groups_id'] """
873 if 'groups_id' in values:
874 # groups are already given, ignore group fields
875 for f in filter(is_reified_group, values.iterkeys()):
880 for f in values.keys():
881 if is_boolean_group(f):
882 target = add if values.pop(f) else remove
883 target.append(get_boolean_group(f))
884 elif is_boolean_groups(f):
885 if not values.pop(f):
886 remove.extend(get_boolean_groups(f))
887 elif is_selection_groups(f):
888 remove.extend(get_selection_groups(f))
889 selected = values.pop(f)
892 # update values *only* if groups are being modified, otherwise
893 # we introduce spurious changes that might break the super.write() call.
895 # remove groups in 'remove' and add groups in 'add'
896 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
898 def default_get(self, cr, uid, fields, context=None):
899 group_fields, fields = partition(is_reified_group, fields)
900 fields1 = (fields + ['groups_id']) if group_fields else fields
901 values = super(users_view, self).default_get(cr, uid, fields1, context)
902 self._get_reified_groups(group_fields, values)
905 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
907 fields = self.fields_get(cr, uid, context=context).keys()
908 group_fields, fields = partition(is_reified_group, fields)
909 if not 'groups_id' in fields:
910 fields.append('groups_id')
911 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
912 for values in (res if isinstance(res, list) else [res]):
913 self._get_reified_groups(group_fields, values)
916 def _get_reified_groups(self, fields, values):
917 """ compute the given reified group fields from values['groups_id'] """
918 gids = set(values.get('groups_id') or [])
920 if is_boolean_group(f):
921 values[f] = get_boolean_group(f) in gids
922 elif is_boolean_groups(f):
923 values[f] = not gids.isdisjoint(get_boolean_groups(f))
924 elif is_selection_groups(f):
925 selected = [gid for gid in get_selection_groups(f) if gid in gids]
926 values[f] = selected and selected[-1] or False
928 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
929 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
930 # add reified groups fields
931 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
932 if kind == 'selection':
933 # selection group field
934 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
935 res[name_selection_groups(map(int, gs))] = {
937 'string': app and app.name or _('Other'),
938 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
939 'help': '\n'.join(tips),
942 # boolean group fields
944 res[name_boolean_group(g.id)] = {
953 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: