[FIX] res.users: fixed argument of field new_password for res.users, function field...
[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     def _get_password(self, cr, uid, ids, arg, karg, context=None):
210         return dict.fromkeys(ids, '')
211
212     _columns = {
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 "
227                  "users."),
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'),
234
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
237         # context is set.
238         'company_id': fields.many2one('res.company', 'Company', required=True,
239             help="The company this user is currently working for.", context={'user_preference': True}),
240
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),
254     }
255
256     def on_change_company_id(self, cr, uid, ids, company_id):
257         return {
258                 'warning' : {
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)"),
261                 }
262         }
263
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'] = '********'
268             return o
269
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)
272         if not canwrite:
273             if isinstance(ids, (int, float)):
274                 result = override_password(result)
275             else:
276                 result = map(override_password, result)
277         return result
278
279
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))
282
283     _constraints = [
284         (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
285     ]
286
287     _sql_constraints = [
288         ('login_key', 'UNIQUE (login)',  'You can not have two users with the same login !')
289     ]
290
291     def _get_email_from(self, cr, uid, ids, context=None):
292         if not isinstance(ids, list):
293             ids = [ids]
294         res = dict.fromkeys(ids, False)
295         for user in self.browse(cr, uid, ids, context=context):
296             if user.user_email:
297                 res[user.id] = "%s <%s>" % (user.name, user.user_email)
298         return res
299
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]
306
307     def _get_company(self,cr, uid, context=None, uid2=False):
308         if not uid2:
309             uid2 = uid
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
313
314     def _get_companies(self, cr, uid, context=None):
315         c = self._get_company(cr, uid, context)
316         if c:
317             return [c]
318         return False
319
320     def _get_menu(self,cr, uid, context=None):
321         dataobj = self.pool.get('ir.model.data')
322         try:
323             model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
324             if model != 'ir.actions.act_window':
325                 return False
326             return res_id
327         except ValueError:
328             return False
329
330     def _get_group(self,cr, uid, context=None):
331         dataobj = self.pool.get('ir.model.data')
332         result = []
333         try:
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)
338         except ValueError:
339             # If these groups does not exists anymore
340             pass
341         return result
342
343     _defaults = {
344         'password' : '',
345         'context_lang': 'en_US',
346         'active' : True,
347         'menu_id': _get_menu,
348         'company_id': _get_company,
349         'company_ids': _get_companies,
350         'groups_id': _get_group,
351         'address_id': False,
352         'menu_tips':True
353     }
354
355     @tools.cache()
356     def company_get(self, cr, uid, uid2, context=None):
357         return self._get_company(cr, uid, context=context, uid2=uid2)
358
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']
361
362     def write(self, cr, uid, ids, values, context=None):
363         if not hasattr(ids, '__iter__'):
364             ids = [ids]
365         if ids == [uid]:
366             for key in values.keys():
367                 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
368                     break
369             else:
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
374
375         res = super(users, self).write(cr, uid, ids, values, context=context)
376
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)
381         map(clear, ids)
382         db = cr.dbname
383         if db in self._uid_cache:
384             for id in ids:
385                 if id in self._uid_cache[db]:
386                     del self._uid_cache[db][id]
387
388         return res
389
390     def unlink(self, cr, uid, ids, context=None):
391         if 1 in ids:
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, ...)'))
393         db = cr.dbname
394         if db in self._uid_cache:
395             for id in ids:
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)
399
400     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
401         if not args:
402             args=[]
403         if not context:
404             context={}
405         ids = []
406         if name:
407             ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
408         if not ids:
409             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
410         return self.name_get(cr, user, ids)
411
412     def copy(self, cr, uid, id, default=None, context=None):
413         user2copy = self.read(cr, uid, [id], ['login','name'])[0]
414         if default is None:
415             default = {}
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!
420                        )
421         copydef.update(default)
422         return super(users, self).copy(cr, uid, id, copydef, context)
423
424     def context_get(self, cr, uid, context=None):
425         user = self.browse(cr, uid, uid, context)
426         result = {}
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):
431                     res = res.id
432                 result[k[8:]] = res or False
433         return result
434
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
439
440
441     def login(self, db, login, password):
442         if not password:
443             return False
444         cr = pooler.get_db(db).cursor()
445         try:
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.
451             cr.autocommit(True)
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)))
462         except Exception:
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.
466             cr.rollback()
467             cr.execute("""SELECT id from res_users
468                           WHERE login=%s AND password=%s
469                                 AND active""",
470                        (tools.ustr(login), tools.ustr(password)))
471         finally:
472             res = cr.fetchone()
473             cr.close()
474             if res:
475                 return res[0]
476         return False
477
478     def check_super(self, passwd):
479         if passwd == tools.config['admin_passwd']:
480             return True
481         else:
482             raise security.ExceptionNoTb('AccessDenied')
483
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."""
487         if not passwd:
488             # empty passwords disallowed for obvious security reasons
489             raise security.ExceptionNoTb('AccessDenied')
490         if self._uid_cache.get(db, {}).get(uid) == passwd:
491             return
492         cr = pooler.get_db(db).cursor()
493         try:
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]
497             if not res:
498                 raise security.ExceptionNoTb('AccessDenied')
499             if self._uid_cache.has_key(db):
500                 ulist = self._uid_cache[db]
501                 ulist[uid] = passwd
502             else:
503                 self._uid_cache[db] = {uid:passwd}
504         finally:
505             cr.close()
506
507     def access(self, db, uid, passwd, sec_level, ids):
508         if not passwd:
509             return False
510         cr = pooler.get_db(db).cursor()
511         try:
512             cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
513             res = cr.fetchone()
514             if not res:
515                 raise security.ExceptionNoTb('Bad username or password')
516             return res[0]
517         finally:
518             cr.close()
519
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.
524
525         :return: True
526         :raise: security.ExceptionNoTb when old password is wrong
527         :raise: except_osv when new password is not set or empty
528         """
529         self.check(cr.dbname, uid, old_passwd)
530         if new_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!"))
533
534 users()
535
536 class config_users(osv.osv_memory):
537     _name = 'res.config.users'
538     _inherit = ['res.users', 'res.config']
539
540     def _generate_signature(self, cr, name, email, context=None):
541         return _('--\n%(name)s %(email)s\n') % {
542             'name': name or '',
543             'email': email and ' <'+email+'>' or '',
544             }
545
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.
549
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
554         """
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,},
561             context)
562         user_data = dict(
563             base_data,
564             signature=self._generate_signature(
565                 cr, base_data['name'], base_data['email'], context=context),
566             address_id=address,
567             )
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'
573         pass
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)
577         return {
578             'view_type': 'form',
579             "view_mode": 'form',
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',
584             'target':'new',
585             }
586 config_users()
587
588 class groups2(osv.osv): ##FIXME: Is there a reason to inherit this object ?
589     _inherit = 'res.groups'
590     _columns = {
591         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
592     }
593
594     def unlink(self, cr, uid, ids, context=None):
595         group_users = []
596         for record in self.read(cr, uid, ids, ['users'], context=context):
597             if record['users']:
598                 group_users.extend(record['users'])
599
600         if group_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]
604                 user_names += '...'
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)
609
610 groups2()
611
612 class res_config_view(osv.osv_memory):
613     _name = 'res.config.view'
614     _inherit = 'res.config'
615     _columns = {
616         'name':fields.char('Name', size=64),
617         'view': fields.selection([('simple','Simplified'),
618                                   ('extended','Extended')],
619                                  'Interface', required=True ),
620     }
621     _defaults={
622         'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
623     }
624
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)
629
630 res_config_view()
631
632 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: