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
38 _logger = logging.getLogger(__name__)
40 class groups(osv.osv):
42 _description = "Access Groups"
43 _rec_name = 'full_name'
45 def _get_full_name(self, cr, uid, ids, field, arg, context=None):
47 for g in self.browse(cr, uid, ids, context):
49 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
55 'name': fields.char('Name', size=64, required=True, translate=True),
56 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
57 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
58 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
59 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
60 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
61 'comment' : fields.text('Comment', size=250, translate=True),
62 'category_id': fields.many2one('ir.module.category', 'Application', select=True),
63 'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
67 ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
70 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
71 # add explicit ordering if search is sorted on full_name
72 if order and order.startswith('full_name'):
73 ids = super(groups, self).search(cr, uid, args, context=context)
74 gs = self.browse(cr, uid, ids, context)
75 gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
76 gs = gs[offset:offset+limit] if limit else gs[offset:]
78 return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
80 def copy(self, cr, uid, id, default=None, context=None):
81 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
82 default.update({'name': _('%s (copy)')%group_name})
83 return super(groups, self).copy(cr, uid, id, default, context)
85 def write(self, cr, uid, ids, vals, context=None):
87 if vals['name'].startswith('-'):
88 raise osv.except_osv(_('Error'),
89 _('The name of the group can not start with "-"'))
90 res = super(groups, self).write(cr, uid, ids, vals, context=context)
91 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
94 def create(self, cr, uid, vals, context=None):
96 if vals['name'].startswith('-'):
97 raise osv.except_osv(_('Error'),
98 _('The name of the group can not start with "-"'))
99 gid = super(groups, self).create(cr, uid, vals, context=context)
100 if context and context.get('noadmin', False):
103 # assign this new group to user_root
104 user_obj = self.pool.get('res.users')
105 aid = user_obj.browse(cr, 1, user_obj._get_admin_id(cr))
107 aid.write({'groups_id': [(4, gid)]})
110 def unlink(self, cr, uid, ids, context=None):
112 for record in self.read(cr, uid, ids, ['users'], context=context):
114 group_users.extend(record['users'])
116 user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
117 user_names = list(set(user_names))
118 if len(user_names) >= 5:
119 user_names = user_names[:5] + ['...']
120 raise osv.except_osv(_('Warning !'),
121 _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
122 ', '.join(user_names))
123 return super(groups, self).unlink(cr, uid, ids, context=context)
125 def get_extended_interface_group(self, cr, uid, context=None):
126 data_obj = self.pool.get('ir.model.data')
127 extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
128 return data_obj.browse(cr, uid, extended_group_data_id, context=context).res_id
132 def _lang_get(self, cr, uid, context=None):
133 obj = self.pool.get('res.lang')
134 ids = obj.search(cr, uid, [('translatable','=',True)])
135 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
136 res = [(r['code'], r['name']) for r in res]
139 def _tz_get(self,cr,uid, context=None):
140 return [(x, x) for x in pytz.all_timezones]
142 class users(osv.osv):
148 WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
149 WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
150 "\"%(name)s\".\n\nYour login is %(login)s, "\
151 "you should ask your supervisor or system administrator if you "\
152 "haven't been given your password yet.\n\n"\
153 "If you aren't %(name)s, this email reached you errorneously, "\
156 def get_welcome_mail_subject(self, cr, uid, context=None):
157 """ Returns the subject of the mail new users receive (when
158 created via the res.config.users wizard), default implementation
159 is to return config_users.WELCOME_MAIL_SUBJECT
161 return self.WELCOME_MAIL_SUBJECT
162 def get_welcome_mail_body(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_BODY
167 return self.WELCOME_MAIL_BODY
169 def get_current_company(self, cr, uid):
170 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)
173 def send_welcome_email(self, cr, uid, id, context=None):
174 if isinstance(id,list): id = id[0]
175 user = self.read(cr, uid, id, ['email','login','name', 'user_email'], context=context)
176 email = user['email'] or user['user_email']
178 ir_mail_server = self.pool.get('ir.mail_server')
179 msg = ir_mail_server.build_email(email_from=None, # take config default
181 subject=self.get_welcome_mail_subject(cr, uid, context=context),
182 body=(self.get_welcome_mail_body(cr, uid, context=context) % user))
183 return ir_mail_server.send_email(cr, uid, msg, context=context)
185 def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
186 """Implementation of 'view' function field setter, sets the type of interface of the users.
187 @param name: Name of the field
188 @param arg: User defined argument
189 @param value: new value returned
192 if not value or value not in ['simple','extended']:
194 group_obj = self.pool.get('res.groups')
195 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
196 # First always remove the users from the group (avoids duplication if called twice)
197 self.write(cr, uid, ids, {'groups_id': [(3, extended_group_id)]}, context=context)
198 # Then add them back if requested
199 if value == 'extended':
200 self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
204 def _get_interface_type(self, cr, uid, ids, name, args, context=None):
205 """Implementation of 'view' function field getter, returns the type of interface of the users.
206 @param field_name: Name of the field
207 @param arg: User defined argument
208 @return: Dictionary of values
210 group_obj = self.pool.get('res.groups')
211 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
212 extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
213 return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
215 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
217 # Do not update the password if no value is provided, ignore silently.
218 # For example web client submits False values for all empty fields.
221 # To change their own password users must use the client-specific change password wizard,
222 # so that the new password is immediately used for further RPC requests, otherwise the user
223 # will face unexpected 'Access Denied' exceptions.
224 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
225 self.write(cr, uid, id, {'password': value})
227 def _get_password(self, cr, uid, ids, arg, karg, context=None):
228 return dict.fromkeys(ids, '')
231 'id': fields.integer('ID'),
232 'name': fields.char('User Name', size=64, required=True, select=True,
233 help="The new user's real name, used for searching"
234 " and most listings"),
235 'login': fields.char('Login', size=64, required=True,
236 help="Used to log into the system"),
237 '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."),
238 'new_password': fields.function(_get_password, type='char', size=64,
239 fnct_inv=_set_new_password,
240 string='Set password', help="Specify a value only when creating a user or if you're changing the user's password, "
241 "otherwise leave empty. After a change of password, the user has to login again."),
242 'user_email': fields.char('Email', size=64),
243 'signature': fields.text('Signature', size=64),
244 'active': fields.boolean('Active'),
245 '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."),
246 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
247 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
249 # Special behavior for this field: res.company.search() will only return the companies
250 # available to the current user (should be the user's companies?), when the user_preference
252 'company_id': fields.many2one('res.company', 'Company', required=True,
253 help="The company this user is currently working for.", context={'user_preference': True}),
255 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
256 'context_lang': fields.selection(_lang_get, 'Language', required=True,
257 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."),
258 'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
259 help="The user's timezone, used to output proper date and time values inside printed reports. "
260 "It is important to set a value for this field. You should use the same timezone "
261 "that is otherwise used to pick and render date and time values: your computer's timezone."),
262 'view': fields.function(_get_interface_type, type='selection', fnct_inv=_set_interface_type,
263 selection=[('simple','Simplified'),('extended','Extended')],
264 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."),
265 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
266 'date': fields.datetime('Latest Connection', readonly=True),
269 def on_change_company_id(self, cr, uid, ids, company_id):
272 'title': _("Company Switch Warning"),
273 '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)"),
277 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
278 def override_password(o):
279 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
280 o['password'] = '********'
282 result = super(users, self).read(cr, uid, ids, fields, context, load)
283 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
285 if isinstance(ids, (int, float)):
286 result = override_password(result)
288 result = map(override_password, result)
292 def _check_company(self, cr, uid, ids, context=None):
293 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
296 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
300 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
303 def _get_email_from(self, cr, uid, ids, context=None):
304 if not isinstance(ids, list):
306 res = dict.fromkeys(ids, False)
307 for user in self.browse(cr, uid, ids, context=context):
309 res[user.id] = "%s <%s>" % (user.name, user.user_email)
312 def _get_admin_id(self, cr):
313 if self.__admin_ids.get(cr.dbname) is None:
314 ir_model_data_obj = self.pool.get('ir.model.data')
315 mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
316 self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
317 return self.__admin_ids[cr.dbname]
319 def _get_company(self,cr, uid, context=None, uid2=False):
322 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
323 company_id = user.get('company_id', False)
324 return company_id and company_id[0] or False
326 def _get_companies(self, cr, uid, context=None):
327 c = self._get_company(cr, uid, context)
332 def _get_menu(self,cr, uid, context=None):
333 dataobj = self.pool.get('ir.model.data')
335 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
336 if model != 'ir.actions.act_window':
342 def _get_group(self,cr, uid, context=None):
343 dataobj = self.pool.get('ir.model.data')
346 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
347 result.append(group_id)
348 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
349 result.append(group_id)
351 # If these groups does not exists anymore
357 'context_lang': 'en_US',
359 'menu_id': _get_menu,
360 'company_id': _get_company,
361 'company_ids': _get_companies,
362 'groups_id': _get_group,
366 # User can write to a few of her own fields (but not her groups for example)
367 SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
369 def write(self, cr, uid, ids, values, context=None):
370 if not hasattr(ids, '__iter__'):
373 for key in values.keys():
374 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
377 if 'company_id' in values:
378 if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
379 del values['company_id']
380 uid = 1 # safe fields only, so we write as super-user to bypass access rights
382 res = super(users, self).write(cr, uid, ids, values, context=context)
384 # clear caches linked to the users
385 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
386 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
389 if db in self._uid_cache:
391 if id in self._uid_cache[db]:
392 del self._uid_cache[db][id]
396 def unlink(self, cr, uid, ids, context=None):
398 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, ...)'))
400 if db in self._uid_cache:
402 if id in self._uid_cache[db]:
403 del self._uid_cache[db][id]
404 return super(users, self).unlink(cr, uid, ids, context=context)
406 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
413 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
415 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
416 return self.name_get(cr, user, ids)
418 def copy(self, cr, uid, id, default=None, context=None):
419 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
422 copy_pattern = _("%s (copy)")
423 copydef = dict(login=(copy_pattern % user2copy['login']),
424 name=(copy_pattern % user2copy['name']),
426 copydef.update(default)
427 return super(users, self).copy(cr, uid, id, copydef, context)
429 def context_get(self, cr, uid, context=None):
430 user = self.browse(cr, uid, uid, context)
432 for k in self._columns.keys():
433 if k.startswith('context_'):
434 res = getattr(user,k) or False
435 if isinstance(res, browse_record):
437 result[k[8:]] = res or False
440 def action_get(self, cr, uid, context=None):
441 dataobj = self.pool.get('ir.model.data')
442 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
443 return dataobj.browse(cr, uid, data_id, context=context).res_id
445 def authenticate(self, db, login, password, user_agent_env):
446 """Verifies and returns the user ID corresponding to the given
447 ``login`` and ``password`` combination, or False if there was
450 :param str db: the database on which user is trying to authenticate
451 :param str login: username
452 :param str password: user password
453 :param dict user_agent_env: environment dictionary describing any
454 relevant environment attributes
456 uid = self.login(db, login, password)
457 if uid == openerp.SUPERUSER_ID:
458 # Successfully logged in as admin!
459 # Attempt to guess the web base url...
460 if user_agent_env and user_agent_env.get('base_location'):
461 cr = pooler.get_db(db).cursor()
463 self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url',
464 user_agent_env['base_location'])
467 _logger.exception("Failed to update web.base.url configuration parameter")
472 def login(self, db, login, password):
475 cr = pooler.get_db(db).cursor()
477 # We autocommit: our single request will be performed atomically.
478 # (In this way, there is no opportunity to have two transactions
479 # interleaving ther cr.execute()..cr.commit() calls and have one
480 # of them rollbacked due to a concurrent access.)
481 # We effectively unconditionally write the res_users line.
483 cr.execute("""UPDATE res_users
484 SET date = now() AT TIME ZONE 'UTC'
485 WHERE login=%s AND password=%s AND active RETURNING id""",
486 (tools.ustr(login), tools.ustr(password)))
495 def check_super(self, passwd):
496 if passwd == tools.config['admin_passwd']:
499 raise openerp.exceptions.AccessDenied()
501 def check(self, db, uid, passwd):
502 """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
503 raise an exception if it is not."""
505 # empty passwords disallowed for obvious security reasons
506 raise openerp.exceptions.AccessDenied()
507 if self._uid_cache.get(db, {}).get(uid) == passwd:
509 cr = pooler.get_db(db).cursor()
511 cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
512 (int(uid), passwd, True))
513 res = cr.fetchone()[0]
515 raise openerp.exceptions.AccessDenied()
516 if self._uid_cache.has_key(db):
517 ulist = self._uid_cache[db]
520 self._uid_cache[db] = {uid:passwd}
524 def access(self, db, uid, passwd, sec_level, ids):
527 cr = pooler.get_db(db).cursor()
529 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
532 raise openerp.exceptions.AccessDenied()
537 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
538 """Change current user password. Old password must be provided explicitly
539 to prevent hijacking an existing user session, or for cases where the cleartext
540 password is not used to authenticate requests.
543 :raise: openerp.exceptions.AccessDenied when old password is wrong
544 :raise: except_osv when new password is not set or empty
546 self.check(cr.dbname, uid, old_passwd)
548 return self.write(cr, uid, uid, {'password': new_passwd})
549 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
556 # Extension of res.groups and res.users with a relation for "implied" or
557 # "inherited" groups. Once a user belongs to a group, it automatically belongs
558 # to the implied groups (transitively).
562 """ A cset (constrained set) is a set of elements that may be constrained to
563 be a subset of other csets. Elements added to a cset are automatically
564 added to its supersets. Cycles in the subset constraints are supported.
566 def __init__(self, xs):
567 self.supersets = set()
568 self.elements = set(xs)
569 def subsetof(self, other):
570 if other is not self:
571 self.supersets.add(other)
572 other.update(self.elements)
573 def update(self, xs):
574 xs = set(xs) - self.elements
575 if xs: # xs will eventually be empty in case of a cycle
576 self.elements.update(xs)
577 for s in self.supersets:
580 return iter(self.elements)
583 """ return the concatenation of a list of iterables """
585 for l in ls: res.extend(l)
590 class groups_implied(osv.osv):
591 _inherit = 'res.groups'
593 def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
594 "computes the transitive closure of relation implied_ids"
595 memo = {} # use a memo for performance and cycle avoidance
598 memo[g] = cset(g.implied_ids)
599 for h in g.implied_ids:
600 computed_set(h).subsetof(memo[g])
604 for g in self.browse(cr, 1, ids, context):
605 res[g.id] = map(int, computed_set(g))
609 'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
610 string='Inherits', help='Users of this group automatically inherit those groups'),
611 'trans_implied_ids': fields.function(_get_trans_implied,
612 type='many2many', relation='res.groups', string='Transitively inherits'),
615 def create(self, cr, uid, values, context=None):
616 users = values.pop('users', None)
617 gid = super(groups_implied, self).create(cr, uid, values, context)
619 # delegate addition of users to add implied groups
620 self.write(cr, uid, [gid], {'users': users}, context)
623 def write(self, cr, uid, ids, values, context=None):
624 res = super(groups_implied, self).write(cr, uid, ids, values, context)
625 if values.get('users') or values.get('implied_ids'):
626 # add all implied groups (to all users of each group)
627 for g in self.browse(cr, uid, ids):
628 gids = map(int, g.trans_implied_ids)
629 vals = {'users': [(4, u.id) for u in g.users]}
630 super(groups_implied, self).write(cr, uid, gids, vals, context)
635 class users_implied(osv.osv):
636 _inherit = 'res.users'
638 def create(self, cr, uid, values, context=None):
639 groups = values.pop('groups_id', None)
640 user_id = super(users_implied, self).create(cr, uid, values, context)
642 # delegate addition of groups to add implied groups
643 self.write(cr, uid, [user_id], {'groups_id': groups}, context)
646 def write(self, cr, uid, ids, values, context=None):
647 if not isinstance(ids,list):
649 res = super(users_implied, self).write(cr, uid, ids, values, context)
650 if values.get('groups_id'):
651 # add implied groups for all users
652 for user in self.browse(cr, uid, ids):
653 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
654 vals = {'groups_id': [(4, g.id) for g in gs]}
655 super(users_implied, self).write(cr, uid, [user.id], vals, context)
663 # Extension of res.groups and res.users for the special groups view in the users
664 # form. This extension presents groups with selection and boolean widgets:
665 # - Groups are shown by application, with boolean and/or selection fields.
666 # Selection fields typically defines a role "Name" for the given application.
667 # - Uncategorized groups are presented as boolean fields and grouped in a
670 # The user form view is modified by an inherited view (base.user_groups_view);
671 # the inherited view replaces the field 'groups_id' by a set of reified group
672 # fields (boolean or selection fields). The arch of that view is regenerated
673 # each time groups are changed.
675 # Naming conventions for reified groups fields:
676 # - boolean field 'in_group_ID' is True iff
677 # ID is in 'groups_id'
678 # - boolean field 'in_groups_ID1_..._IDk' is True iff
679 # any of ID1, ..., IDk is in 'groups_id'
680 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
681 # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
683 def name_boolean_group(id): return 'in_group_' + str(id)
684 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
685 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
687 def is_boolean_group(name): return name.startswith('in_group_')
688 def is_boolean_groups(name): return name.startswith('in_groups_')
689 def is_selection_groups(name): return name.startswith('sel_groups_')
690 def is_reified_group(name):
691 return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
693 def get_boolean_group(name): return int(name[9:])
694 def get_boolean_groups(name): return map(int, name[10:].split('_'))
695 def get_selection_groups(name): return map(int, name[11:].split('_'))
697 def partition(f, xs):
698 "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
701 (yes if f(x) else nos).append(x)
706 class groups_view(osv.osv):
707 _inherit = 'res.groups'
709 def create(self, cr, uid, values, context=None):
710 res = super(groups_view, self).create(cr, uid, values, context)
711 self.update_user_groups_view(cr, uid, context)
714 def write(self, cr, uid, ids, values, context=None):
715 res = super(groups_view, self).write(cr, uid, ids, values, context)
716 self.update_user_groups_view(cr, uid, context)
719 def unlink(self, cr, uid, ids, context=None):
720 res = super(groups_view, self).unlink(cr, uid, ids, context)
721 self.update_user_groups_view(cr, uid, context)
724 def update_user_groups_view(self, cr, uid, context=None):
725 # the view with id 'base.user_groups_view' inherits the user form view,
726 # and introduces the reified group fields
727 view = self.get_user_groups_view(cr, uid, context)
729 xml = u"""<?xml version="1.0" encoding="utf-8"?>
730 <!-- GENERATED AUTOMATICALLY BY GROUPS -->
731 <field name="groups_id" position="replace">
737 xml1.append('<separator string="%s" colspan="4"/>' % _('Applications'))
738 for app, kind, gs in self.get_groups_by_application(cr, uid, context):
739 if kind == 'selection':
740 # application name with a selection field
741 field_name = name_selection_groups(map(int, gs))
742 xml1.append('<field name="%s"/>' % field_name)
743 xml1.append('<newline/>')
745 # application separator with boolean fields
746 app_name = app and app.name or _('Other')
747 xml2.append('<separator string="%s" colspan="4"/>' % app_name)
749 field_name = name_boolean_group(g.id)
750 xml2.append('<field name="%s"/>' % field_name)
751 view.write({'arch': xml % ('\n'.join(xml1), '\n'.join(xml2))})
754 def get_user_groups_view(self, cr, uid, context=None):
756 view = self.pool.get('ir.model.data').get_object(cr, 1, 'base', 'user_groups_view', context)
757 assert view and view._table_name == 'ir.ui.view'
762 def get_application_groups(self, cr, uid, domain=None, context=None):
763 return self.search(cr, uid, domain or [])
765 def get_groups_by_application(self, cr, uid, context=None):
766 """ return all groups classified by application (module category), as a list of pairs:
767 [(app, kind, [group, ...]), ...],
768 where app and group are browse records, and kind is either 'boolean' or 'selection'.
769 Applications are given in sequence order. If kind is 'selection', the groups are
770 given in reverse implication order.
774 # determine sequence order: a group should appear after its implied groups
775 order = dict.fromkeys(gs, 0)
777 for h in gs.intersection(g.trans_implied_ids):
779 # check whether order is total, i.e., sequence orders are distinct
780 if len(set(order.itervalues())) == len(gs):
781 return sorted(gs, key=lambda g: order[g])
784 # classify all groups by application
785 gids = self.get_application_groups(cr, uid, context=context)
786 by_app, others = {}, []
787 for g in self.browse(cr, uid, gids, context):
789 by_app.setdefault(g.category_id, []).append(g)
794 apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
796 gs = linearized(by_app[app])
798 res.append((app, 'selection', gs))
800 res.append((app, 'boolean', by_app[app]))
802 res.append((False, 'boolean', others))
807 class users_view(osv.osv):
808 _inherit = 'res.users'
810 def create(self, cr, uid, values, context=None):
811 self._set_reified_groups(values)
812 return super(users_view, self).create(cr, uid, values, context)
814 def write(self, cr, uid, ids, values, context=None):
815 self._set_reified_groups(values)
816 return super(users_view, self).write(cr, uid, ids, values, context)
818 def _set_reified_groups(self, values):
819 """ reflect reified group fields in values['groups_id'] """
820 if 'groups_id' in values:
821 # groups are already given, ignore group fields
822 for f in filter(is_reified_group, values.iterkeys()):
827 for f in values.keys():
828 if is_boolean_group(f):
829 target = add if values.pop(f) else remove
830 target.append(get_boolean_group(f))
831 elif is_boolean_groups(f):
832 if not values.pop(f):
833 remove.extend(get_boolean_groups(f))
834 elif is_selection_groups(f):
835 remove.extend(get_selection_groups(f))
836 selected = values.pop(f)
839 # update values *only* if groups are being modified, otherwise
840 # we introduce spurious changes that might break the super.write() call.
842 # remove groups in 'remove' and add groups in 'add'
843 values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
845 def default_get(self, cr, uid, fields, context=None):
846 group_fields, fields = partition(is_reified_group, fields)
847 fields1 = (fields + ['groups_id']) if group_fields else fields
848 values = super(users_view, self).default_get(cr, uid, fields1, context)
849 self._get_reified_groups(group_fields, values)
852 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
854 fields = self.fields_get(cr, uid, context=context).keys()
855 group_fields, fields = partition(is_reified_group, fields)
856 fields.append('groups_id')
857 res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
858 for values in (res if isinstance(res, list) else [res]):
859 self._get_reified_groups(group_fields, values)
862 def _get_reified_groups(self, fields, values):
863 """ compute the given reified group fields from values['groups_id'] """
864 gids = set(values.get('groups_id') or [])
866 if is_boolean_group(f):
867 values[f] = get_boolean_group(f) in gids
868 elif is_boolean_groups(f):
869 values[f] = not gids.isdisjoint(get_boolean_groups(f))
870 elif is_selection_groups(f):
871 selected = [gid for gid in get_selection_groups(f) if gid in gids]
872 values[f] = selected and selected[-1] or False
874 def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
875 res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
876 # add reified groups fields
877 for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
878 if kind == 'selection':
879 # selection group field
880 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
881 res[name_selection_groups(map(int, gs))] = {
883 'string': app and app.name or _('Other'),
884 'selection': [(False, '')] + [(g.id, g.name) for g in gs],
885 'help': '\n'.join(tips),
888 # boolean group fields
890 res[name_boolean_group(g.id)] = {
899 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: