[FIX] res_users: improve behavior when user record is locked during login
[odoo/odoo.git] / bin / addons / base / res / res_user.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
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>).
7 #
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.
12 #
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.
17 #
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/>.
20 #
21 ##############################################################################
22
23 from osv import fields,osv
24 from osv.orm import browse_record
25 import tools
26 from functools import partial
27 import pytz
28 import pooler
29 from tools.translate import _
30 from service import security
31 import netsvc
32
33 class groups(osv.osv):
34     _name = "res.groups"
35     _order = 'name'
36     _description = "Access Groups"
37     _columns = {
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),
44     }
45     _sql_constraints = [
46         ('name_uniq', 'unique (name)', 'The name of the group must be unique !')
47     ]
48
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)
53
54     def write(self, cr, uid, ids, vals, context=None):
55         if 'name' in vals:
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)
61         return res
62
63     def create(self, cr, uid, vals, context=None):
64         if 'name' in vals:
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):
70             pass
71         else:
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))
75             if aid:
76                 aid.write({'groups_id': [(4, gid)]})
77         return gid
78
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
83
84 groups()
85
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]
91     return res
92
93 def _tz_get(self,cr,uid, context=None):
94     return [(x, x) for x in pytz.all_timezones]
95
96 class users(osv.osv):
97     __admin_ids = {}
98     _uid_cache = {}
99     _name = "res.users"
100     _order = 'name'
101
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, "\
108         "please delete it."
109
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
114         """
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
120         """
121         return self.WELCOME_MAIL_BODY
122
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)
125         return cr.fetchall()
126
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'))
133             return False
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 '
137                   'to users'))
138             return False
139         if not user.get('email'):
140             return False
141
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)
147
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
153         @return:  True/False
154         """
155         if not value or value not in ['simple','extended']:
156             return False
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)
164         return True
165
166
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
172         """
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]))
177
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
182
183     def _email_set(self, cr, uid, ids, name, value, arg, context=None):
184         if not isinstance(ids,list):
185             ids = [ids]
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.
190             if user.address_id:
191                 address_obj.write(cr, 1, user.address_id.id, {'email': value or None}) # no context to avoid potential security issues as superuser
192             else:
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)
195         return True
196
197     def _set_new_password(self, cr, uid, id, name, value, args, context=None):
198         if value is False:
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.
201             return
202         if uid == id:
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})
208
209     _columns = {
210         'name': fields.char('User Name', size=64, required=True, select=True,
211                             help="The new user's real name, used for searching"
212                                  " and most listings"),
213         'login': fields.char('Login', size=64, required=True,
214                              help="Used to log into the system"),
215         '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."),
216         'new_password': fields.function(lambda *a:'', method=True, type='char', size=64,
217                                 fnct_inv=_set_new_password,
218                                 string='Change password', help="Only specify a value if you want to change the user password. "
219                                 "This user will have to logout and login again!"),
220         'email': fields.char('E-mail', size=64,
221             help='If an email is provided, the user will be sent a message '
222                  'welcoming him.\n\nWarning: if "email_from" and "smtp_server"'
223                  " aren't configured, it won't be possible to email new "
224                  "users."),
225         'signature': fields.text('Signature', size=64),
226         'address_id': fields.many2one('res.partner.address', 'Address'),
227         'active': fields.boolean('Active'),
228         '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."),
229         'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
230         'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
231
232         # Special behavior for this field: res.company.search() will only return the companies
233         # available to the current user (should be the user's companies?), when the user_preference
234         # context is set.
235         'company_id': fields.many2one('res.company', 'Company', required=True,
236             help="The company this user is currently working for.", context={'user_preference': True}),
237
238         'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
239         'context_lang': fields.selection(_lang_get, 'Language', required=True,
240             help="Sets the language for the user's user interface, when UI "
241                  "translations are available"),
242         'context_tz': fields.selection(_tz_get,  'Timezone', size=64,
243             help="The user's timezone, used to perform timezone conversions "
244                  "between the server and the client."),
245         'view': fields.function(_get_interface_type, method=True, type='selection', fnct_inv=_set_interface_type,
246                                 selection=[('simple','Simplified'),('extended','Extended')],
247                                 string='Interface', help="Choose between the simplified interface and the extended one"),
248         'user_email': fields.function(_email_get, method=True, fnct_inv=_email_set, string='Email', type="char", size=240),
249         'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
250         'date': fields.datetime('Last Connection', readonly=True),
251     }
252
253     def on_change_company_id(self, cr, uid, ids, company_id):
254         return {
255                 'warning' : {
256                     'title': _("Company Switch Warning"),
257                     '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)"),
258                 }
259         }
260
261     def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
262         def override_password(o):
263             if 'password' in o and ( 'id' not in o or o['id'] != uid ):
264                 o['password'] = '********'
265             return o
266
267         result = super(users, self).read(cr, uid, ids, fields, context, load)
268         canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', raise_exception=False)
269         if not canwrite:
270             if isinstance(ids, (int, float)):
271                 result = override_password(result)
272             else:
273                 result = map(override_password, result)
274         return result
275
276
277     def _check_company(self, cr, uid, ids, context=None):
278         return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
279
280     _constraints = [
281         (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
282     ]
283
284     _sql_constraints = [
285         ('login_key', 'UNIQUE (login)',  'You can not have two users with the same login !')
286     ]
287
288     def _get_email_from(self, cr, uid, ids, context=None):
289         if not isinstance(ids, list):
290             ids = [ids]
291         res = dict.fromkeys(ids, False)
292         for user in self.browse(cr, uid, ids, context=context):
293             if user.user_email:
294                 res[user.id] = "%s <%s>" % (user.name, user.user_email)
295         return res
296
297     def _get_admin_id(self, cr):
298         if self.__admin_ids.get(cr.dbname) is None:
299             ir_model_data_obj = self.pool.get('ir.model.data')
300             mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
301             self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
302         return self.__admin_ids[cr.dbname]
303
304     def _get_company(self,cr, uid, context=None, uid2=False):
305         if not uid2:
306             uid2 = uid
307         user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
308         company_id = user.get('company_id', False)
309         return company_id and company_id[0] or False
310
311     def _get_companies(self, cr, uid, context=None):
312         c = self._get_company(cr, uid, context)
313         if c:
314             return [c]
315         return False
316
317     def _get_menu(self,cr, uid, context=None):
318         dataobj = self.pool.get('ir.model.data')
319         try:
320             model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
321             if model != 'ir.actions.act_window':
322                 return False
323             return res_id
324         except ValueError:
325             return False
326
327     def _get_group(self,cr, uid, context=None):
328         dataobj = self.pool.get('ir.model.data')
329         result = []
330         try:
331             dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
332             result.append(group_id)
333             dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
334             result.append(group_id)
335         except ValueError:
336             # If these groups does not exists anymore
337             pass
338         return result
339
340     _defaults = {
341         'password' : '',
342         'context_lang': 'en_US',
343         'active' : True,
344         'menu_id': _get_menu,
345         'company_id': _get_company,
346         'company_ids': _get_companies,
347         'groups_id': _get_group,
348         'address_id': False,
349         'menu_tips':True
350     }
351
352     @tools.cache()
353     def company_get(self, cr, uid, uid2, context=None):
354         return self._get_company(cr, uid, context=context, uid2=uid2)
355
356     # User can write to a few of her own fields (but not her groups for example)
357     SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
358
359     def write(self, cr, uid, ids, values, context=None):
360         if not hasattr(ids, '__iter__'):
361             ids = [ids]
362         if ids == [uid]:
363             for key in values.keys():
364                 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
365                     break
366             else:
367                 if 'company_id' in values:
368                     if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
369                         del values['company_id']
370                 uid = 1 # safe fields only, so we write as super-user to bypass access rights
371
372         res = super(users, self).write(cr, uid, ids, values, context=context)
373
374         # clear caches linked to the users
375         self.company_get.clear_cache(cr.dbname)
376         self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
377         clear = partial(self.pool.get('ir.rule').clear_cache, cr)
378         map(clear, ids)
379         db = cr.dbname
380         if db in self._uid_cache:
381             for id in ids:
382                 if id in self._uid_cache[db]:
383                     del self._uid_cache[db][id]
384
385         return res
386
387     def unlink(self, cr, uid, ids, context=None):
388         if 1 in ids:
389             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, ...)'))
390         db = cr.dbname
391         if db in self._uid_cache:
392             for id in ids:
393                 if id in self._uid_cache[db]:
394                     del self._uid_cache[db][id]
395         return super(users, self).unlink(cr, uid, ids, context=context)
396
397     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
398         if not args:
399             args=[]
400         if not context:
401             context={}
402         ids = []
403         if name:
404             ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
405         if not ids:
406             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
407         return self.name_get(cr, user, ids)
408
409     def copy(self, cr, uid, id, default=None, context=None):
410         user2copy = self.read(cr, uid, [id], ['login','name'])[0]
411         if default is None:
412             default = {}
413         copy_pattern = _("%s (copy)")
414         copydef = dict(login=(copy_pattern % user2copy['login']),
415                        name=(copy_pattern % user2copy['name']),
416                        address_id=False, # avoid sharing the address of the copied user!
417                        )
418         copydef.update(default)
419         return super(users, self).copy(cr, uid, id, copydef, context)
420
421     def context_get(self, cr, uid, context=None):
422         user = self.browse(cr, uid, uid, context)
423         result = {}
424         for k in self._columns.keys():
425             if k.startswith('context_'):
426                 res = getattr(user,k) or False
427                 if isinstance(res, browse_record):
428                     res = res.id
429                 result[k[8:]] = res or False
430         return result
431
432     def action_get(self, cr, uid, context=None):
433         dataobj = self.pool.get('ir.model.data')
434         data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
435         return dataobj.browse(cr, uid, data_id, context=context).res_id
436
437
438     def login(self, db, login, password):
439         if not password:
440             return False
441         cr = pooler.get_db(db).cursor()
442         try:
443             # autocommit: our single request will be performed atomically.
444             # (In this way, there is no opportunity to have two transactions
445             # interleaving their cr.execute()..cr.commit() calls and have one
446             # of them rolled back due to a concurrent access.)
447             # We effectively unconditionally write the res_users line.
448             cr.autocommit(True)
449             # Even w/ autocommit there's a chance the user row will be locked,
450             # in which case we can't delay the login just for the purpose of
451             # update the last login date - hence we use FOR UPDATE NOWAIT to
452             # try to get the lock - fail-fast
453             cr.execute("""SELECT id from res_users
454                           WHERE login=%s AND password=%s
455                                 AND active FOR UPDATE NOWAIT""",
456                        (tools.ustr(login), tools.ustr(password)), log_exceptions=False)
457             cr.execute('UPDATE res_users SET date=now() WHERE login=%s AND password=%s AND active RETURNING id',
458                     (tools.ustr(login), tools.ustr(password)))
459         except Exception:
460             # Failing to acquire the lock on the res_users row probably means
461             # another request is holding it - no big deal, we skip the update
462             # for this time, and let the user login anyway.
463             cr.rollback()
464             cr.execute("""SELECT id from res_users
465                           WHERE login=%s AND password=%s
466                                 AND active""",
467                        (tools.ustr(login), tools.ustr(password)))
468         finally:
469             res = cr.fetchone()
470             cr.close()
471             if res:
472                 return res[0]
473         return False
474
475     def check_super(self, passwd):
476         if passwd == tools.config['admin_passwd']:
477             return True
478         else:
479             raise security.ExceptionNoTb('AccessDenied')
480
481     def check(self, db, uid, passwd):
482         """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
483            raise an exception if it is not."""
484         if not passwd:
485             # empty passwords disallowed for obvious security reasons
486             raise security.ExceptionNoTb('AccessDenied')
487         if self._uid_cache.get(db, {}).get(uid) == passwd:
488             return
489         cr = pooler.get_db(db).cursor()
490         try:
491             cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
492                         (int(uid), passwd, True))
493             res = cr.fetchone()[0]
494             if not res:
495                 raise security.ExceptionNoTb('AccessDenied')
496             if self._uid_cache.has_key(db):
497                 ulist = self._uid_cache[db]
498                 ulist[uid] = passwd
499             else:
500                 self._uid_cache[db] = {uid:passwd}
501         finally:
502             cr.close()
503
504     def access(self, db, uid, passwd, sec_level, ids):
505         if not passwd:
506             return False
507         cr = pooler.get_db(db).cursor()
508         try:
509             cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
510             res = cr.fetchone()
511             if not res:
512                 raise security.ExceptionNoTb('Bad username or password')
513             return res[0]
514         finally:
515             cr.close()
516
517     def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
518         """Change current user password. Old password must be provided explicitly
519         to prevent hijacking an existing user session, or for cases where the cleartext
520         password is not used to authenticate requests.
521
522         :return: True
523         :raise: security.ExceptionNoTb when old password is wrong
524         :raise: except_osv when new password is not set or empty
525         """
526         self.check(cr.dbname, uid, old_passwd)
527         if new_passwd:
528             return self.write(cr, uid, uid, {'password': new_passwd})
529         raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
530
531 users()
532
533 class config_users(osv.osv_memory):
534     _name = 'res.config.users'
535     _inherit = ['res.users', 'res.config']
536
537     def _generate_signature(self, cr, name, email, context=None):
538         return _('--\n%(name)s %(email)s\n') % {
539             'name': name or '',
540             'email': email and ' <'+email+'>' or '',
541             }
542
543     def create_user(self, cr, uid, new_id, context=None):
544         """ create a new res.user instance from the data stored
545         in the current res.config.users.
546
547         If an email address was filled in for the user, sends a mail
548         composed of the return values of ``get_welcome_mail_subject``
549         and ``get_welcome_mail_body`` (which should be unicode values),
550         with the user's data %-formatted into the mail body
551         """
552         base_data = self.read(cr, uid, new_id, context=context)
553         partner_id = self.pool.get('res.partner').main_partner(cr, uid)
554         address = self.pool.get('res.partner.address').create(
555             cr, uid, {'name': base_data['name'],
556                       'email': base_data['email'],
557                       'partner_id': partner_id,},
558             context)
559         user_data = dict(
560             base_data,
561             signature=self._generate_signature(
562                 cr, base_data['name'], base_data['email'], context=context),
563             address_id=address,
564             )
565         new_user = self.pool.get('res.users').create(
566             cr, uid, user_data, context)
567         self.send_welcome_email(cr, uid, new_user, context=context)
568     def execute(self, cr, uid, ids, context=None):
569         'Do nothing on execution, just launch the next action/todo'
570         pass
571     def action_add(self, cr, uid, ids, context=None):
572         'Create a user, and re-display the view'
573         self.create_user(cr, uid, ids[0], context=context)
574         return {
575             'view_type': 'form',
576             "view_mode": 'form',
577             'res_model': 'res.config.users',
578             'view_id':self.pool.get('ir.ui.view')\
579                 .search(cr,uid,[('name','=','res.config.users.confirm.form')]),
580             'type': 'ir.actions.act_window',
581             'target':'new',
582             }
583 config_users()
584
585 class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
586     _inherit = 'res.groups'
587     _columns = {
588         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
589     }
590
591     def unlink(self, cr, uid, ids, context=None):
592         group_users = []
593         for record in self.read(cr, uid, ids, ['users'], context=context):
594             if record['users']:
595                 group_users.extend(record['users'])
596
597         if group_users:
598             user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
599             if len(user_names) >= 5:
600                 user_names = user_names[:5]
601                 user_names += '...'
602             raise osv.except_osv(_('Warning !'),
603                         _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
604                             ', '.join(user_names))
605         return super(groups2, self).unlink(cr, uid, ids, context=context)
606
607 groups2()
608
609 class res_config_view(osv.osv_memory):
610     _name = 'res.config.view'
611     _inherit = 'res.config'
612     _columns = {
613         'name':fields.char('Name', size=64),
614         'view': fields.selection([('simple','Simplified'),
615                                   ('extended','Extended')],
616                                  'Interface', required=True ),
617     }
618     _defaults={
619         'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
620     }
621
622     def execute(self, cr, uid, ids, context=None):
623         res = self.read(cr, uid, ids)[0]
624         self.pool.get('res.users').write(cr, uid, [uid],
625                                  {'view':res['view']}, context=context)
626
627 res_config_view()
628
629 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: