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 ##############################################################################
23 from osv import fields,osv
24 from osv.orm import browse_record
26 from functools import partial
29 from tools.translate import _
30 from service import security
33 class groups(osv.osv):
36 _description = "Access Groups"
38 'name': fields.char('Group Name', size=64, required=True),
39 'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
40 'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
41 'group_id', 'rule_group_id', 'Rules'),
42 'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
43 'comment' : fields.text('Comment',size=250),
46 ('name_uniq', 'unique (name)', 'The name of the group must be unique !')
49 def copy(self, cr, uid, id, default=None, context=None):
50 group_name = self.read(cr, uid, [id], ['name'])[0]['name']
51 default.update({'name': _('%s (copy)')%group_name})
52 return super(groups, self).copy(cr, uid, id, default, context)
54 def write(self, cr, uid, ids, vals, context=None):
56 if vals['name'].startswith('-'):
57 raise osv.except_osv(_('Error'),
58 _('The name of the group can not start with "-"'))
59 res = super(groups, self).write(cr, uid, ids, vals, context=context)
60 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
63 def create(self, cr, uid, vals, context=None):
65 if vals['name'].startswith('-'):
66 raise osv.except_osv(_('Error'),
67 _('The name of the group can not start with "-"'))
68 gid = super(groups, self).create(cr, uid, vals, context=context)
69 if context and context.get('noadmin', False):
72 # assign this new group to user_root
73 user_obj = self.pool.get('res.users')
74 aid = user_obj.browse(cr, 1, user_obj._get_admin_id(cr))
76 aid.write({'groups_id': [(4, gid)]})
79 def get_extended_interface_group(self, cr, uid, context=None):
80 data_obj = self.pool.get('ir.model.data')
81 extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
82 return data_obj.browse(cr, uid, extended_group_data_id, context=context).res_id
86 def _lang_get(self, cr, uid, context=None):
87 obj = self.pool.get('res.lang')
88 ids = obj.search(cr, uid, [('translatable','=',True)])
89 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
90 res = [(r['code'], r['name']) for r in res]
93 def _tz_get(self,cr,uid, context=None):
94 return [(x, x) for x in pytz.all_timezones]
102 WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
103 WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
104 "\"%(name)s\".\n\nYour login is %(login)s, "\
105 "you should ask your supervisor or system administrator if you "\
106 "haven't been given your password yet.\n\n"\
107 "If you aren't %(name)s, this email reached you errorneously, "\
110 def get_welcome_mail_subject(self, cr, uid, context=None):
111 """ Returns the subject of the mail new users receive (when
112 created via the res.config.users wizard), default implementation
113 is to return config_users.WELCOME_MAIL_SUBJECT
115 return self.WELCOME_MAIL_SUBJECT
116 def get_welcome_mail_body(self, cr, uid, context=None):
117 """ Returns the subject of the mail new users receive (when
118 created via the res.config.users wizard), default implementation
119 is to return config_users.WELCOME_MAIL_BODY
121 return self.WELCOME_MAIL_BODY
123 def get_current_company(self, cr, uid):
124 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)
127 def send_welcome_email(self, cr, uid, id, context=None):
128 logger= netsvc.Logger()
129 user = self.pool.get('res.users').read(cr, uid, id, context=context)
130 if not tools.config.get('smtp_server'):
131 logger.notifyChannel('mails', netsvc.LOG_WARNING,
132 _('"smtp_server" needs to be set to send mails to users'))
134 if not tools.config.get('email_from'):
135 logger.notifyChannel("mails", netsvc.LOG_WARNING,
136 _('"email_from" needs to be set to send welcome mails '
139 if not user.get('email'):
142 return tools.email_send(email_from=None, email_to=[user['email']],
143 subject=self.get_welcome_mail_subject(
144 cr, uid, context=context),
145 body=self.get_welcome_mail_body(
146 cr, uid, context=context) % user)
148 def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
149 """Implementation of 'view' function field setter, sets the type of interface of the users.
150 @param name: Name of the field
151 @param arg: User defined argument
152 @param value: new value returned
155 if not value or value not in ['simple','extended']:
157 group_obj = self.pool.get('res.groups')
158 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
159 # First always remove the users from the group (avoids duplication if called twice)
160 self.write(cr, uid, ids, {'groups_id': [(3, extended_group_id)]}, context=context)
161 # Then add them back if requested
162 if value == 'extended':
163 self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
167 def _get_interface_type(self, cr, uid, ids, name, args, context=None):
168 """Implementation of 'view' function field getter, returns the type of interface of the users.
169 @param field_name: Name of the field
170 @param arg: User defined argument
171 @return: Dictionary of values
173 group_obj = self.pool.get('res.groups')
174 extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
175 extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
176 return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
178 def _email_get(self, cr, uid, ids, name, arg, context=None):
179 # perform this as superuser because the current user is allowed to read users, and that includes
180 # the email, even without any direct read access on the res_partner_address object.
181 return dict([(user.id, user.address_id.email) for user in self.browse(cr, 1, ids)]) # no context to avoid potential security issues as superuser
183 def _email_set(self, cr, uid, ids, name, value, arg, context=None):
184 if not isinstance(ids,list):
186 address_obj = self.pool.get('res.partner.address')
187 for user in self.browse(cr, uid, ids, context=context):
188 # perform this as superuser because the current user is allowed to write to the user, and that includes
189 # the email even without any direct write access on the res_partner_address object.
191 address_obj.write(cr, 1, user.address_id.id, {'email': value or None}) # no context to avoid potential security issues as superuser
193 address_id = address_obj.create(cr, 1, {'name': user.name, 'email': value or None}) # no context to avoid potential security issues as superuser
194 self.write(cr, uid, ids, {'address_id': address_id}, context)
197 def _set_new_password(self, cr, uid, id, name, value, args, context=None):
199 # Do not update the password if no value is provided, ignore silently.
200 # For example web client submits False values for all empty fields.
203 # To change their own password users must use the client-specific change password wizard,
204 # so that the new password is immediately used for further RPC requests, otherwise the user
205 # will face unexpected 'Access Denied' exceptions.
206 raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
207 self.write(cr, uid, id, {'password': value})
209 def _get_password(self, cr, uid, ids, arg, karg, context=None):
210 return dict.fromkeys(ids, '')
213 'name': fields.char('User Name', size=64, required=True, select=True,
214 help="The new user's real name, used for searching"
215 " and most listings"),
216 'login': fields.char('Login', size=64, required=True,
217 help="Used to log into the system"),
218 '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."),
219 'new_password': fields.function(_get_password, method=True, type='char', size=64,
220 fnct_inv=_set_new_password,
221 string='Change password', help="Only specify a value if you want to change the user password. "
222 "This user will have to logout and login again!"),
223 'email': fields.char('E-mail', size=64,
224 help='If an email is provided, the user will be sent a message '
225 'welcoming him.\n\nWarning: if "email_from" and "smtp_server"'
226 " aren't configured, it won't be possible to email new "
228 'signature': fields.text('Signature', size=64),
229 'address_id': fields.many2one('res.partner.address', 'Address'),
230 'active': fields.boolean('Active'),
231 '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."),
232 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
233 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
235 # Special behavior for this field: res.company.search() will only return the companies
236 # available to the current user (should be the user's companies?), when the user_preference
238 'company_id': fields.many2one('res.company', 'Company', required=True,
239 help="The company this user is currently working for.", context={'user_preference': True}),
241 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
242 'context_lang': fields.selection(_lang_get, 'Language', required=True,
243 help="Sets the language for the user's user interface, when UI "
244 "translations are available"),
245 'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
246 help="The user's timezone, used to perform timezone conversions "
247 "between the server and the client."),
248 'view': fields.function(_get_interface_type, method=True, type='selection', fnct_inv=_set_interface_type,
249 selection=[('simple','Simplified'),('extended','Extended')],
250 string='Interface', help="Choose between the simplified interface and the extended one"),
251 'user_email': fields.function(_email_get, method=True, fnct_inv=_email_set, string='Email', type="char", size=240),
252 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
253 'date': fields.datetime('Last Connection', readonly=True),
256 def on_change_company_id(self, cr, uid, ids, company_id):
259 'title': _("Company Switch Warning"),
260 '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)"),
264 def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
265 def override_password(o):
266 if 'password' in o and ( 'id' not in o or o['id'] != uid ):
267 o['password'] = '********'
270 result = super(users, self).read(cr, uid, ids, fields, context, load)
271 canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', raise_exception=False)
273 if isinstance(ids, (int, float)):
274 result = override_password(result)
276 result = map(override_password, result)
280 def _check_company(self, cr, uid, ids, context=None):
281 return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
284 (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
288 ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
291 def _get_email_from(self, cr, uid, ids, context=None):
292 if not isinstance(ids, list):
294 res = dict.fromkeys(ids, False)
295 for user in self.browse(cr, uid, ids, context=context):
297 res[user.id] = "%s <%s>" % (user.name, user.user_email)
300 def _get_admin_id(self, cr):
301 if self.__admin_ids.get(cr.dbname) is None:
302 ir_model_data_obj = self.pool.get('ir.model.data')
303 mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
304 self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
305 return self.__admin_ids[cr.dbname]
307 def _get_company(self,cr, uid, context=None, uid2=False):
310 user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
311 company_id = user.get('company_id', False)
312 return company_id and company_id[0] or False
314 def _get_companies(self, cr, uid, context=None):
315 c = self._get_company(cr, uid, context)
320 def _get_menu(self,cr, uid, context=None):
321 dataobj = self.pool.get('ir.model.data')
323 model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
324 if model != 'ir.actions.act_window':
330 def _get_group(self,cr, uid, context=None):
331 dataobj = self.pool.get('ir.model.data')
334 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
335 result.append(group_id)
336 dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
337 result.append(group_id)
339 # If these groups does not exists anymore
345 'context_lang': 'en_US',
347 'menu_id': _get_menu,
348 'company_id': _get_company,
349 'company_ids': _get_companies,
350 'groups_id': _get_group,
356 def company_get(self, cr, uid, uid2, context=None):
357 return self._get_company(cr, uid, context=context, uid2=uid2)
359 # User can write to a few of her own fields (but not her groups for example)
360 SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
362 def write(self, cr, uid, ids, values, context=None):
363 if not hasattr(ids, '__iter__'):
366 for key in values.keys():
367 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
370 if 'company_id' in values:
371 if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
372 del values['company_id']
373 uid = 1 # safe fields only, so we write as super-user to bypass access rights
375 res = super(users, self).write(cr, uid, ids, values, context=context)
377 # clear caches linked to the users
378 self.company_get.clear_cache(cr.dbname)
379 self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
380 clear = partial(self.pool.get('ir.rule').clear_cache, cr)
383 if db in self._uid_cache:
385 if id in self._uid_cache[db]:
386 del self._uid_cache[db][id]
390 def unlink(self, cr, uid, ids, context=None):
392 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, ...)'))
394 if db in self._uid_cache:
396 if id in self._uid_cache[db]:
397 del self._uid_cache[db][id]
398 return super(users, self).unlink(cr, uid, ids, context=context)
400 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
407 ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
409 ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
410 return self.name_get(cr, user, ids)
412 def copy(self, cr, uid, id, default=None, context=None):
413 user2copy = self.read(cr, uid, [id], ['login','name'])[0]
416 copy_pattern = _("%s (copy)")
417 copydef = dict(login=(copy_pattern % user2copy['login']),
418 name=(copy_pattern % user2copy['name']),
419 address_id=False, # avoid sharing the address of the copied user!
421 copydef.update(default)
422 return super(users, self).copy(cr, uid, id, copydef, context)
424 def context_get(self, cr, uid, context=None):
425 user = self.browse(cr, uid, uid, context)
427 for k in self._columns.keys():
428 if k.startswith('context_'):
429 res = getattr(user,k) or False
430 if isinstance(res, browse_record):
432 result[k[8:]] = res or False
435 def action_get(self, cr, uid, context=None):
436 dataobj = self.pool.get('ir.model.data')
437 data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
438 return dataobj.browse(cr, uid, data_id, context=context).res_id
441 def login(self, db, login, password):
444 cr = pooler.get_db(db).cursor()
446 # autocommit: our single request will be performed atomically.
447 # (In this way, there is no opportunity to have two transactions
448 # interleaving their cr.execute()..cr.commit() calls and have one
449 # of them rolled back due to a concurrent access.)
450 # We effectively unconditionally write the res_users line.
452 # Even w/ autocommit there's a chance the user row will be locked,
453 # in which case we can't delay the login just for the purpose of
454 # update the last login date - hence we use FOR UPDATE NOWAIT to
455 # try to get the lock - fail-fast
456 cr.execute("""SELECT id from res_users
457 WHERE login=%s AND password=%s
458 AND active FOR UPDATE NOWAIT""",
459 (tools.ustr(login), tools.ustr(password)), log_exceptions=False)
460 cr.execute('UPDATE res_users SET date=now() WHERE login=%s AND password=%s AND active RETURNING id',
461 (tools.ustr(login), tools.ustr(password)))
463 # Failing to acquire the lock on the res_users row probably means
464 # another request is holding it - no big deal, we skip the update
465 # for this time, and let the user login anyway.
467 cr.execute("""SELECT id from res_users
468 WHERE login=%s AND password=%s
470 (tools.ustr(login), tools.ustr(password)))
478 def check_super(self, passwd):
479 if passwd == tools.config['admin_passwd']:
482 raise security.ExceptionNoTb('AccessDenied')
484 def check(self, db, uid, passwd):
485 """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
486 raise an exception if it is not."""
488 # empty passwords disallowed for obvious security reasons
489 raise security.ExceptionNoTb('AccessDenied')
490 if self._uid_cache.get(db, {}).get(uid) == passwd:
492 cr = pooler.get_db(db).cursor()
494 cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
495 (int(uid), passwd, True))
496 res = cr.fetchone()[0]
498 raise security.ExceptionNoTb('AccessDenied')
499 if self._uid_cache.has_key(db):
500 ulist = self._uid_cache[db]
503 self._uid_cache[db] = {uid:passwd}
507 def access(self, db, uid, passwd, sec_level, ids):
510 cr = pooler.get_db(db).cursor()
512 cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
515 raise security.ExceptionNoTb('Bad username or password')
520 def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
521 """Change current user password. Old password must be provided explicitly
522 to prevent hijacking an existing user session, or for cases where the cleartext
523 password is not used to authenticate requests.
526 :raise: security.ExceptionNoTb when old password is wrong
527 :raise: except_osv when new password is not set or empty
529 self.check(cr.dbname, uid, old_passwd)
531 return self.write(cr, uid, uid, {'password': new_passwd})
532 raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
536 class config_users(osv.osv_memory):
537 _name = 'res.config.users'
538 _inherit = ['res.users', 'res.config']
540 def _generate_signature(self, cr, name, email, context=None):
541 return _('--\n%(name)s %(email)s\n') % {
543 'email': email and ' <'+email+'>' or '',
546 def create_user(self, cr, uid, new_id, context=None):
547 """ create a new res.user instance from the data stored
548 in the current res.config.users.
550 If an email address was filled in for the user, sends a mail
551 composed of the return values of ``get_welcome_mail_subject``
552 and ``get_welcome_mail_body`` (which should be unicode values),
553 with the user's data %-formatted into the mail body
555 base_data = self.read(cr, uid, new_id, context=context)
556 partner_id = self.pool.get('res.partner').main_partner(cr, uid)
557 address = self.pool.get('res.partner.address').create(
558 cr, uid, {'name': base_data['name'],
559 'email': base_data['email'],
560 'partner_id': partner_id,},
564 signature=self._generate_signature(
565 cr, base_data['name'], base_data['email'], context=context),
568 new_user = self.pool.get('res.users').create(
569 cr, uid, user_data, context)
570 self.send_welcome_email(cr, uid, new_user, context=context)
571 def execute(self, cr, uid, ids, context=None):
572 'Do nothing on execution, just launch the next action/todo'
574 def action_add(self, cr, uid, ids, context=None):
575 'Create a user, and re-display the view'
576 self.create_user(cr, uid, ids[0], context=context)
580 'res_model': 'res.config.users',
581 'view_id':self.pool.get('ir.ui.view')\
582 .search(cr,uid,[('name','=','res.config.users.confirm.form')]),
583 'type': 'ir.actions.act_window',
588 class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
589 _inherit = 'res.groups'
591 'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
594 def unlink(self, cr, uid, ids, context=None):
596 for record in self.read(cr, uid, ids, ['users'], context=context):
598 group_users.extend(record['users'])
601 user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
602 if len(user_names) >= 5:
603 user_names = user_names[:5]
605 raise osv.except_osv(_('Warning !'),
606 _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
607 ', '.join(user_names))
608 return super(groups2, self).unlink(cr, uid, ids, context=context)
612 class res_config_view(osv.osv_memory):
613 _name = 'res.config.view'
614 _inherit = 'res.config'
616 'name':fields.char('Name', size=64),
617 'view': fields.selection([('simple','Simplified'),
618 ('extended','Extended')],
619 'Interface', required=True ),
622 'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
625 def execute(self, cr, uid, ids, context=None):
626 res = self.read(cr, uid, ids)[0]
627 self.pool.get('res.users').write(cr, uid, [uid],
628 {'view':res['view']}, context=context)
632 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: