[FIX] Forgot to update a field name.
[odoo/odoo.git] / openerp / addons / base / res / res_users.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 import logging
24 from functools import partial
25
26 import pytz
27
28 import netsvc
29 import pooler
30 import tools
31 from osv import fields,osv
32 from osv.orm import browse_record
33 from service import security
34 from tools.translate import _
35 import openerp
36 import openerp.exceptions
37
38 import io, StringIO
39 from PIL import Image
40 import random
41
42 _logger = logging.getLogger(__name__)
43
44 class groups(osv.osv):
45     _name = "res.groups"
46     _description = "Access Groups"
47     _rec_name = 'full_name'
48
49     def _get_full_name(self, cr, uid, ids, field, arg, context=None):
50         res = {}
51         for g in self.browse(cr, uid, ids, context):
52             if g.category_id:
53                 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
54             else:
55                 res[g.id] = g.name
56         return res
57
58     _columns = {
59         'name': fields.char('Name', size=64, required=True, translate=True),
60         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
61         'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
62         'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
63             'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
64         'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
65         'comment' : fields.text('Comment', size=250, translate=True),
66         'category_id': fields.many2one('ir.module.category', 'Application', select=True),
67         'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
68     }
69
70     _sql_constraints = [
71         ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
72     ]
73
74     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
75         # add explicit ordering if search is sorted on full_name
76         if order and order.startswith('full_name'):
77             ids = super(groups, self).search(cr, uid, args, context=context)
78             gs = self.browse(cr, uid, ids, context)
79             gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
80             gs = gs[offset:offset+limit] if limit else gs[offset:]
81             return map(int, gs)
82         return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
83
84     def copy(self, cr, uid, id, default=None, context=None):
85         group_name = self.read(cr, uid, [id], ['name'])[0]['name']
86         default.update({'name': _('%s (copy)')%group_name})
87         return super(groups, self).copy(cr, uid, id, default, context)
88
89     def write(self, cr, uid, ids, vals, context=None):
90         if 'name' in vals:
91             if vals['name'].startswith('-'):
92                 raise osv.except_osv(_('Error'),
93                         _('The name of the group can not start with "-"'))
94         res = super(groups, self).write(cr, uid, ids, vals, context=context)
95         self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
96         return res
97
98     def create(self, cr, uid, vals, context=None):
99         if 'name' in vals:
100             if vals['name'].startswith('-'):
101                 raise osv.except_osv(_('Error'),
102                         _('The name of the group can not start with "-"'))
103         gid = super(groups, self).create(cr, uid, vals, context=context)
104         if context and context.get('noadmin', False):
105             pass
106         else:
107             # assign this new group to user_root
108             user_obj = self.pool.get('res.users')
109             aid = user_obj.browse(cr, 1, user_obj._get_admin_id(cr))
110             if aid:
111                 aid.write({'groups_id': [(4, gid)]})
112         return gid
113
114     def unlink(self, cr, uid, ids, context=None):
115         group_users = []
116         for record in self.read(cr, uid, ids, ['users'], context=context):
117             if record['users']:
118                 group_users.extend(record['users'])
119         if group_users:
120             user_names = [user.name for user in self.pool.get('res.users').browse(cr, uid, group_users, context=context)]
121             user_names = list(set(user_names))
122             if len(user_names) >= 5:
123                 user_names = user_names[:5] + ['...']
124             raise osv.except_osv(_('Warning !'),
125                         _('Group(s) cannot be deleted, because some user(s) still belong to them: %s !') % \
126                             ', '.join(user_names))
127         return super(groups, self).unlink(cr, uid, ids, context=context)
128
129     def get_extended_interface_group(self, cr, uid, context=None):
130         data_obj = self.pool.get('ir.model.data')
131         extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
132         return data_obj.browse(cr, uid, extended_group_data_id, context=context).res_id
133
134 groups()
135
136 def _lang_get(self, cr, uid, context=None):
137     obj = self.pool.get('res.lang')
138     ids = obj.search(cr, uid, [('translatable','=',True)])
139     res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
140     res = [(r['code'], r['name']) for r in res]
141     return res
142
143 def _tz_get(self,cr,uid, context=None):
144     return [(x, x) for x in pytz.all_timezones]
145
146 class users(osv.osv):
147     __admin_ids = {}
148     _uid_cache = {}
149     _name = "res.users"
150     _order = 'name'
151
152     WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
153     WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
154         "\"%(name)s\".\n\nYour login is %(login)s, "\
155         "you should ask your supervisor or system administrator if you "\
156         "haven't been given your password yet.\n\n"\
157         "If you aren't %(name)s, this email reached you errorneously, "\
158         "please delete it."
159
160     def get_welcome_mail_subject(self, cr, uid, context=None):
161         """ Returns the subject of the mail new users receive (when
162         created via the res.config.users wizard), default implementation
163         is to return config_users.WELCOME_MAIL_SUBJECT
164         """
165         return self.WELCOME_MAIL_SUBJECT
166     def get_welcome_mail_body(self, cr, uid, context=None):
167         """ Returns the subject of the mail new users receive (when
168         created via the res.config.users wizard), default implementation
169         is to return config_users.WELCOME_MAIL_BODY
170         """
171         return self.WELCOME_MAIL_BODY
172
173     def get_current_company(self, cr, uid):
174         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)
175         return cr.fetchall()
176
177     def send_welcome_email(self, cr, uid, id, context=None):
178         if isinstance(id,list): id = id[0]
179         user = self.read(cr, uid, id, ['email','login','name', 'user_email'], context=context)
180         email = user['email'] or user['user_email']
181
182         ir_mail_server = self.pool.get('ir.mail_server')
183         msg = ir_mail_server.build_email(email_from=None, # take config default
184                                          email_to=[email],
185                                          subject=self.get_welcome_mail_subject(cr, uid, context=context),
186                                          body=(self.get_welcome_mail_body(cr, uid, context=context) % user))
187         return ir_mail_server.send_email(cr, uid, msg, context=context)
188
189     def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
190         """Implementation of 'view' function field setter, sets the type of interface of the users.
191         @param name: Name of the field
192         @param arg: User defined argument
193         @param value: new value returned
194         @return:  True/False
195         """
196         if not value or value not in ['simple','extended']:
197             return False
198         group_obj = self.pool.get('res.groups')
199         extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
200         # First always remove the users from the group (avoids duplication if called twice)
201         self.write(cr, uid, ids, {'groups_id': [(3, extended_group_id)]}, context=context)
202         # Then add them back if requested
203         if value == 'extended':
204             self.write(cr, uid, ids, {'groups_id': [(4, extended_group_id)]}, context=context)
205         return True
206
207     def _get_interface_type(self, cr, uid, ids, name, args, context=None):
208         """Implementation of 'view' function field getter, returns the type of interface of the users.
209         @param field_name: Name of the field
210         @param arg: User defined argument
211         @return:  Dictionary of values
212         """
213         group_obj = self.pool.get('res.groups')
214         extended_group_id = group_obj.get_extended_interface_group(cr, uid, context=context)
215         extended_users = group_obj.read(cr, uid, extended_group_id, ['users'], context=context)['users']
216         return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
217
218     def onchange_avatar(self, cr, uid, ids, value, context=None):
219         return {'value': {'avatar_stored': value, 'avatar': self._avatar_resize(cr, uid, value, context=context)  } }
220     
221     def _set_avatar(self, cr, uid, id, name, value, args, context=None):
222         return self.write(cr, uid, [id], {'avatar_stored': value}, context=context)
223     
224     def _avatar_resize(self, cr, uid, avatar, context=None):
225         image_stream = io.BytesIO(avatar_stored.decode('base64'))
226         img = Image.open(image_stream)
227         img.thumbnail((180, 150), Image.ANTIALIAS)
228         img_stream = StringIO.StringIO()
229         img.save(img_stream, "JPEG")
230         return img_stream.getvalue().encode('base64')
231     
232     def _get_avatar(self, cr, uid, ids, name, args, context=None):
233         result = {}
234         for user in self.browse(cr, uid, ids, context=context):
235             if not user.avatar:
236                 result[user.id] = False
237             else:
238                 result[user.id] = self._avatar_resize(cr, uid, user.avatar_stored)
239         return result
240
241     def _set_new_password(self, cr, uid, id, name, value, args, context=None):
242         if value is False:
243             # Do not update the password if no value is provided, ignore silently.
244             # For example web client submits False values for all empty fields.
245             return
246         if uid == id:
247             # To change their own password users must use the client-specific change password wizard,
248             # so that the new password is immediately used for further RPC requests, otherwise the user
249             # will face unexpected 'Access Denied' exceptions.
250             raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
251         self.write(cr, uid, id, {'password': value})
252
253     def _get_password(self, cr, uid, ids, arg, karg, context=None):
254         return dict.fromkeys(ids, '')
255
256     _columns = {
257         'id': fields.integer('ID'),
258         'name': fields.char('User Name', size=64, required=True, select=True,
259                             help="The new user's real name, used for searching"
260                                  " and most listings"),
261         'login': fields.char('Login', size=64, required=True,
262                              help="Used to log into the system"),
263         '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."),
264         'new_password': fields.function(_get_password, type='char', size=64,
265                                 fnct_inv=_set_new_password,
266                                 string='Set password', help="Specify a value only when creating a user or if you're changing the user's password, "
267                                                             "otherwise leave empty. After a change of password, the user has to login again."),
268         'user_email': fields.char('Email', size=64),
269         'signature': fields.text('Signature', size=64),
270         'avatar_stored': fields.binary('Stored avatar', help="This field holds the image used as avatar for the user. The avatar field is used as an interface to access this field. The image is base64 encoded, and PIL-supported."),
271         'avatar': fields.function(_get_avatar, fnct_inv=_set_avatar, string='Avatar', type="binary",
272             store = {
273                 'res.users': (lambda self, cr, uid, ids, c={}: ids, ['avatar_stored'], 10),
274             }, help="Image used as avatar for the user. It is automatically resized as a 180x150 px image."),
275         'active': fields.boolean('Active'),
276         '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."),
277         'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
278         'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
279
280         # Special behavior for this field: res.company.search() will only return the companies
281         # available to the current user (should be the user's companies?), when the user_preference
282         # context is set.
283         'company_id': fields.many2one('res.company', 'Company', required=True,
284             help="The company this user is currently working for.", context={'user_preference': True}),
285
286         'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
287         'context_lang': fields.selection(_lang_get, 'Language', required=True,
288             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."),
289         'context_tz': fields.selection(_tz_get,  'Timezone', size=64,
290             help="The user's timezone, used to output proper date and time values inside printed reports. "
291                  "It is important to set a value for this field. You should use the same timezone "
292                  "that is otherwise used to pick and render date and time values: your computer's timezone."),
293         'view': fields.function(_get_interface_type, type='selection', fnct_inv=_set_interface_type,
294                                 selection=[('simple','Simplified'),('extended','Extended')],
295                                 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."),
296         'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
297         'date': fields.datetime('Latest Connection', readonly=True),
298     }
299
300     def on_change_company_id(self, cr, uid, ids, company_id):
301         return {
302                 'warning' : {
303                     'title': _("Company Switch Warning"),
304                     '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)"),
305                 }
306         }
307
308     def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
309         def override_password(o):
310             if 'password' in o and ( 'id' not in o or o['id'] != uid ):
311                 o['password'] = '********'
312             return o
313         result = super(users, self).read(cr, uid, ids, fields, context, load)
314         canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', False)
315         if not canwrite:
316             if isinstance(ids, (int, float)):
317                 result = override_password(result)
318             else:
319                 result = map(override_password, result)
320         return result
321
322
323     def _check_company(self, cr, uid, ids, context=None):
324         return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
325
326     _constraints = [
327         (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
328     ]
329
330     _sql_constraints = [
331         ('login_key', 'UNIQUE (login)',  'You can not have two users with the same login !')
332     ]
333
334     def _get_email_from(self, cr, uid, ids, context=None):
335         if not isinstance(ids, list):
336             ids = [ids]
337         res = dict.fromkeys(ids, False)
338         for user in self.browse(cr, uid, ids, context=context):
339             if user.user_email:
340                 res[user.id] = "%s <%s>" % (user.name, user.user_email)
341         return res
342
343     def _get_admin_id(self, cr):
344         if self.__admin_ids.get(cr.dbname) is None:
345             ir_model_data_obj = self.pool.get('ir.model.data')
346             mdid = ir_model_data_obj._get_id(cr, 1, 'base', 'user_root')
347             self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
348         return self.__admin_ids[cr.dbname]
349
350     def _get_company(self,cr, uid, context=None, uid2=False):
351         if not uid2:
352             uid2 = uid
353         user = self.pool.get('res.users').read(cr, uid, uid2, ['company_id'], context)
354         company_id = user.get('company_id', False)
355         return company_id and company_id[0] or False
356
357     def _get_companies(self, cr, uid, context=None):
358         c = self._get_company(cr, uid, context)
359         if c:
360             return [c]
361         return False
362
363     def _get_menu(self,cr, uid, context=None):
364         dataobj = self.pool.get('ir.model.data')
365         try:
366             model, res_id = dataobj.get_object_reference(cr, uid, 'base', 'action_menu_admin')
367             if model != 'ir.actions.act_window':
368                 return False
369             return res_id
370         except ValueError:
371             return False
372
373     def _get_group(self,cr, uid, context=None):
374         dataobj = self.pool.get('ir.model.data')
375         result = []
376         try:
377             dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
378             result.append(group_id)
379             dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
380             result.append(group_id)
381         except ValueError:
382             # If these groups does not exists anymore
383             pass
384         return result
385
386     def _get_avatar(self, cr, uid, context=None):
387         # default avatar file name: avatar0 -> avatar6.jpg, choose randomly
388         avatar_path = openerp.modules.get_module_resource('base', 'images', 'avatar%d.jpg' % random.randint(0, 6))
389         return self._avatar_resize(cr, uid, open(avatar_path, 'rb').read().encode('base64'))
390     
391     _defaults = {
392         'password' : '',
393         'context_lang': 'en_US',
394         'avatar': _get_avatar,
395         'active' : True,
396         'menu_id': _get_menu,
397         'company_id': _get_company,
398         'company_ids': _get_companies,
399         'groups_id': _get_group,
400         'menu_tips': False
401     }
402
403     # User can write to a few of her own fields (but not her groups for example)
404     SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
405
406     def write(self, cr, uid, ids, values, context=None):
407         if not hasattr(ids, '__iter__'):
408             ids = [ids]
409         if ids == [uid]:
410             for key in values.keys():
411                 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
412                     break
413             else:
414                 if 'company_id' in values:
415                     if not (values['company_id'] in self.read(cr, 1, uid, ['company_ids'], context=context)['company_ids']):
416                         del values['company_id']
417                 uid = 1 # safe fields only, so we write as super-user to bypass access rights
418
419         res = super(users, self).write(cr, uid, ids, values, context=context)
420
421         # clear caches linked to the users
422         self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
423         clear = partial(self.pool.get('ir.rule').clear_cache, cr)
424         map(clear, ids)
425         db = cr.dbname
426         if db in self._uid_cache:
427             for id in ids:
428                 if id in self._uid_cache[db]:
429                     del self._uid_cache[db][id]
430
431         return res
432
433     def unlink(self, cr, uid, ids, context=None):
434         if 1 in ids:
435             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, ...)'))
436         db = cr.dbname
437         if db in self._uid_cache:
438             for id in ids:
439                 if id in self._uid_cache[db]:
440                     del self._uid_cache[db][id]
441         return super(users, self).unlink(cr, uid, ids, context=context)
442
443     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
444         if not args:
445             args=[]
446         if not context:
447             context={}
448         ids = []
449         if name:
450             ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit)
451         if not ids:
452             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit)
453         return self.name_get(cr, user, ids)
454
455     def copy(self, cr, uid, id, default=None, context=None):
456         user2copy = self.read(cr, uid, [id], ['login','name'])[0]
457         if default is None:
458             default = {}
459         copy_pattern = _("%s (copy)")
460         copydef = dict(login=(copy_pattern % user2copy['login']),
461                        name=(copy_pattern % user2copy['name']),
462                        )
463         copydef.update(default)
464         return super(users, self).copy(cr, uid, id, copydef, context)
465
466     def context_get(self, cr, uid, context=None):
467         user = self.browse(cr, uid, uid, context)
468         result = {}
469         for k in self._columns.keys():
470             if k.startswith('context_'):
471                 res = getattr(user,k) or False
472                 if isinstance(res, browse_record):
473                     res = res.id
474                 result[k[8:]] = res or False
475         return result
476
477     def action_get(self, cr, uid, context=None):
478         dataobj = self.pool.get('ir.model.data')
479         data_id = dataobj._get_id(cr, 1, 'base', 'action_res_users_my')
480         return dataobj.browse(cr, uid, data_id, context=context).res_id
481
482     def authenticate(self, db, login, password, user_agent_env):
483         """Verifies and returns the user ID corresponding to the given
484           ``login`` and ``password`` combination, or False if there was
485           no matching user.
486
487            :param str db: the database on which user is trying to authenticate
488            :param str login: username
489            :param str password: user password
490            :param dict user_agent_env: environment dictionary describing any
491                relevant environment attributes
492         """
493         uid = self.login(db, login, password)
494         if uid == openerp.SUPERUSER_ID:
495             # Successfully logged in as admin!
496             # Attempt to guess the web base url...
497             if user_agent_env and user_agent_env.get('base_location'):
498                 cr = pooler.get_db(db).cursor()
499                 try:
500                     self.pool.get('ir.config_parameter').set_param(cr, uid, 'web.base.url',
501                                                                    user_agent_env['base_location'])
502                     cr.commit()
503                 except Exception:
504                     _logger.exception("Failed to update web.base.url configuration parameter")
505                 finally:
506                     cr.close()
507         return uid
508
509     def login(self, db, login, password):
510         if not password:
511             return False
512         cr = pooler.get_db(db).cursor()
513         try:
514             # autocommit: our single request will be performed atomically.
515             # (In this way, there is no opportunity to have two transactions
516             # interleaving their cr.execute()..cr.commit() calls and have one
517             # of them rolled back due to a concurrent access.)
518             # We effectively unconditionally write the res_users line.
519             cr.autocommit(True)
520             # Even w/ autocommit there's a chance the user row will be locked,
521             # in which case we can't delay the login just for the purpose of
522             # update the last login date - hence we use FOR UPDATE NOWAIT to
523             # try to get the lock - fail-fast
524             cr.execute("""SELECT id from res_users
525                           WHERE login=%s AND password=%s
526                                 AND active FOR UPDATE NOWAIT""",
527                        (tools.ustr(login), tools.ustr(password)))
528             cr.execute("""UPDATE res_users
529                             SET date = now() AT TIME ZONE 'UTC'
530                             WHERE login=%s AND password=%s AND active
531                             RETURNING id""",
532                        (tools.ustr(login), tools.ustr(password)))
533         except Exception:
534             # Failing to acquire the lock on the res_users row probably means
535             # another request is holding it. No big deal, we don't want to
536             # prevent/delay login in that case. It will also have been logged
537             # as a SQL error, if anyone cares.
538             cr.execute("""SELECT id from res_users
539                           WHERE login=%s AND password=%s
540                                 AND active""",
541                        (tools.ustr(login), tools.ustr(password)))
542         finally:
543             res = cr.fetchone()
544             cr.close()
545             if res:
546                 return res[0]
547         return False
548
549     def check_super(self, passwd):
550         if passwd == tools.config['admin_passwd']:
551             return True
552         else:
553             raise openerp.exceptions.AccessDenied()
554
555     def check(self, db, uid, passwd):
556         """Verifies that the given (uid, password) pair is authorized for the database ``db`` and
557            raise an exception if it is not."""
558         if not passwd:
559             # empty passwords disallowed for obvious security reasons
560             raise openerp.exceptions.AccessDenied()
561         if self._uid_cache.get(db, {}).get(uid) == passwd:
562             return
563         cr = pooler.get_db(db).cursor()
564         try:
565             cr.execute('SELECT COUNT(1) FROM res_users WHERE id=%s AND password=%s AND active=%s',
566                         (int(uid), passwd, True))
567             res = cr.fetchone()[0]
568             if not res:
569                 raise openerp.exceptions.AccessDenied()
570             if self._uid_cache.has_key(db):
571                 ulist = self._uid_cache[db]
572                 ulist[uid] = passwd
573             else:
574                 self._uid_cache[db] = {uid:passwd}
575         finally:
576             cr.close()
577
578     def access(self, db, uid, passwd, sec_level, ids):
579         if not passwd:
580             return False
581         cr = pooler.get_db(db).cursor()
582         try:
583             cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
584             res = cr.fetchone()
585             if not res:
586                 raise openerp.exceptions.AccessDenied()
587             return res[0]
588         finally:
589             cr.close()
590
591     def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
592         """Change current user password. Old password must be provided explicitly
593         to prevent hijacking an existing user session, or for cases where the cleartext
594         password is not used to authenticate requests.
595
596         :return: True
597         :raise: openerp.exceptions.AccessDenied when old password is wrong
598         :raise: except_osv when new password is not set or empty
599         """
600         self.check(cr.dbname, uid, old_passwd)
601         if new_passwd:
602             return self.write(cr, uid, uid, {'password': new_passwd})
603         raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
604
605 users()
606
607
608
609 #
610 # Extension of res.groups and res.users with a relation for "implied" or 
611 # "inherited" groups.  Once a user belongs to a group, it automatically belongs
612 # to the implied groups (transitively).
613 #
614
615 class cset(object):
616     """ A cset (constrained set) is a set of elements that may be constrained to
617         be a subset of other csets.  Elements added to a cset are automatically
618         added to its supersets.  Cycles in the subset constraints are supported.
619     """
620     def __init__(self, xs):
621         self.supersets = set()
622         self.elements = set(xs)
623     def subsetof(self, other):
624         if other is not self:
625             self.supersets.add(other)
626             other.update(self.elements)
627     def update(self, xs):
628         xs = set(xs) - self.elements
629         if xs:      # xs will eventually be empty in case of a cycle
630             self.elements.update(xs)
631             for s in self.supersets:
632                 s.update(xs)
633     def __iter__(self):
634         return iter(self.elements)
635
636 def concat(ls):
637     """ return the concatenation of a list of iterables """
638     res = []
639     for l in ls: res.extend(l)
640     return res
641
642
643
644 class groups_implied(osv.osv):
645     _inherit = 'res.groups'
646
647     def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
648         "computes the transitive closure of relation implied_ids"
649         memo = {}           # use a memo for performance and cycle avoidance
650         def computed_set(g):
651             if g not in memo:
652                 memo[g] = cset(g.implied_ids)
653                 for h in g.implied_ids:
654                     computed_set(h).subsetof(memo[g])
655             return memo[g]
656
657         res = {}
658         for g in self.browse(cr, 1, ids, context):
659             res[g.id] = map(int, computed_set(g))
660         return res
661
662     _columns = {
663         'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
664             string='Inherits', help='Users of this group automatically inherit those groups'),
665         'trans_implied_ids': fields.function(_get_trans_implied,
666             type='many2many', relation='res.groups', string='Transitively inherits'),
667     }
668
669     def create(self, cr, uid, values, context=None):
670         users = values.pop('users', None)
671         gid = super(groups_implied, self).create(cr, uid, values, context)
672         if users:
673             # delegate addition of users to add implied groups
674             self.write(cr, uid, [gid], {'users': users}, context)
675         return gid
676
677     def write(self, cr, uid, ids, values, context=None):
678         res = super(groups_implied, self).write(cr, uid, ids, values, context)
679         if values.get('users') or values.get('implied_ids'):
680             # add all implied groups (to all users of each group)
681             for g in self.browse(cr, uid, ids):
682                 gids = map(int, g.trans_implied_ids)
683                 vals = {'users': [(4, u.id) for u in g.users]}
684                 super(groups_implied, self).write(cr, uid, gids, vals, context)
685         return res
686
687 groups_implied()
688
689 class users_implied(osv.osv):
690     _inherit = 'res.users'
691
692     def create(self, cr, uid, values, context=None):
693         groups = values.pop('groups_id', None)
694         user_id = super(users_implied, self).create(cr, uid, values, context)
695         if groups:
696             # delegate addition of groups to add implied groups
697             self.write(cr, uid, [user_id], {'groups_id': groups}, context)
698         return user_id
699
700     def write(self, cr, uid, ids, values, context=None):
701         if not isinstance(ids,list):
702             ids = [ids]
703         res = super(users_implied, self).write(cr, uid, ids, values, context)
704         if values.get('groups_id'):
705             # add implied groups for all users
706             for user in self.browse(cr, uid, ids):
707                 gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
708                 vals = {'groups_id': [(4, g.id) for g in gs]}
709                 super(users_implied, self).write(cr, uid, [user.id], vals, context)
710         return res
711
712 users_implied()
713
714
715
716 #
717 # Extension of res.groups and res.users for the special groups view in the users
718 # form.  This extension presents groups with selection and boolean widgets:
719 # - Groups are shown by application, with boolean and/or selection fields.
720 #   Selection fields typically defines a role "Name" for the given application.
721 # - Uncategorized groups are presented as boolean fields and grouped in a
722 #   section "Others".
723 #
724 # The user form view is modified by an inherited view (base.user_groups_view);
725 # the inherited view replaces the field 'groups_id' by a set of reified group
726 # fields (boolean or selection fields).  The arch of that view is regenerated
727 # each time groups are changed.
728 #
729 # Naming conventions for reified groups fields:
730 # - boolean field 'in_group_ID' is True iff
731 #       ID is in 'groups_id'
732 # - boolean field 'in_groups_ID1_..._IDk' is True iff
733 #       any of ID1, ..., IDk is in 'groups_id'
734 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
735 #       ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
736
737 def name_boolean_group(id): return 'in_group_' + str(id)
738 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
739 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
740
741 def is_boolean_group(name): return name.startswith('in_group_')
742 def is_boolean_groups(name): return name.startswith('in_groups_')
743 def is_selection_groups(name): return name.startswith('sel_groups_')
744 def is_reified_group(name):
745     return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
746
747 def get_boolean_group(name): return int(name[9:])
748 def get_boolean_groups(name): return map(int, name[10:].split('_'))
749 def get_selection_groups(name): return map(int, name[11:].split('_'))
750
751 def partition(f, xs):
752     "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
753     yes, nos = [], []
754     for x in xs:
755         (yes if f(x) else nos).append(x)
756     return yes, nos
757
758
759
760 class groups_view(osv.osv):
761     _inherit = 'res.groups'
762
763     def create(self, cr, uid, values, context=None):
764         res = super(groups_view, self).create(cr, uid, values, context)
765         self.update_user_groups_view(cr, uid, context)
766         return res
767
768     def write(self, cr, uid, ids, values, context=None):
769         res = super(groups_view, self).write(cr, uid, ids, values, context)
770         self.update_user_groups_view(cr, uid, context)
771         return res
772
773     def unlink(self, cr, uid, ids, context=None):
774         res = super(groups_view, self).unlink(cr, uid, ids, context)
775         self.update_user_groups_view(cr, uid, context)
776         return res
777
778     def update_user_groups_view(self, cr, uid, context=None):
779         # the view with id 'base.user_groups_view' inherits the user form view,
780         # and introduces the reified group fields
781         view = self.get_user_groups_view(cr, uid, context)
782         if view:
783             xml = u"""<?xml version="1.0" encoding="utf-8"?>
784 <!-- GENERATED AUTOMATICALLY BY GROUPS -->
785 <field name="groups_id" position="replace">
786 %s
787 %s
788 </field>
789 """
790             xml1, xml2 = [], []
791             xml1.append('<separator string="%s" colspan="4"/>' % _('Applications'))
792             for app, kind, gs in self.get_groups_by_application(cr, uid, context):
793                 if kind == 'selection':
794                     # application name with a selection field
795                     field_name = name_selection_groups(map(int, gs))
796                     xml1.append('<field name="%s"/>' % field_name)
797                     xml1.append('<newline/>')
798                 else:
799                     # application separator with boolean fields
800                     app_name = app and app.name or _('Other')
801                     xml2.append('<separator string="%s" colspan="4"/>' % app_name)
802                     for g in gs:
803                         field_name = name_boolean_group(g.id)
804                         xml2.append('<field name="%s"/>' % field_name)
805             view.write({'arch': xml % ('\n'.join(xml1), '\n'.join(xml2))})
806         return True
807
808     def get_user_groups_view(self, cr, uid, context=None):
809         try:
810             view = self.pool.get('ir.model.data').get_object(cr, 1, 'base', 'user_groups_view', context)
811             assert view and view._table_name == 'ir.ui.view'
812         except Exception:
813             view = False
814         return view
815
816     def get_application_groups(self, cr, uid, domain=None, context=None):
817         return self.search(cr, uid, domain or [])
818
819     def get_groups_by_application(self, cr, uid, context=None):
820         """ return all groups classified by application (module category), as a list of pairs:
821                 [(app, kind, [group, ...]), ...],
822             where app and group are browse records, and kind is either 'boolean' or 'selection'.
823             Applications are given in sequence order.  If kind is 'selection', the groups are
824             given in reverse implication order.
825         """
826         def linearized(gs):
827             gs = set(gs)
828             # determine sequence order: a group should appear after its implied groups
829             order = dict.fromkeys(gs, 0)
830             for g in gs:
831                 for h in gs.intersection(g.trans_implied_ids):
832                     order[h] -= 1
833             # check whether order is total, i.e., sequence orders are distinct
834             if len(set(order.itervalues())) == len(gs):
835                 return sorted(gs, key=lambda g: order[g])
836             return None
837
838         # classify all groups by application
839         gids = self.get_application_groups(cr, uid, context=context)
840         by_app, others = {}, []
841         for g in self.browse(cr, uid, gids, context):
842             if g.category_id:
843                 by_app.setdefault(g.category_id, []).append(g)
844             else:
845                 others.append(g)
846         # build the result
847         res = []
848         apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
849         for app in apps:
850             gs = linearized(by_app[app])
851             if gs:
852                 res.append((app, 'selection', gs))
853             else:
854                 res.append((app, 'boolean', by_app[app]))
855         if others:
856             res.append((False, 'boolean', others))
857         return res
858
859 groups_view()
860
861 class users_view(osv.osv):
862     _inherit = 'res.users'
863
864     def create(self, cr, uid, values, context=None):
865         self._set_reified_groups(values)
866         return super(users_view, self).create(cr, uid, values, context)
867
868     def write(self, cr, uid, ids, values, context=None):
869         self._set_reified_groups(values)
870         return super(users_view, self).write(cr, uid, ids, values, context)
871
872     def _set_reified_groups(self, values):
873         """ reflect reified group fields in values['groups_id'] """
874         if 'groups_id' in values:
875             # groups are already given, ignore group fields
876             for f in filter(is_reified_group, values.iterkeys()):
877                 del values[f]
878             return
879
880         add, remove = [], []
881         for f in values.keys():
882             if is_boolean_group(f):
883                 target = add if values.pop(f) else remove
884                 target.append(get_boolean_group(f))
885             elif is_boolean_groups(f):
886                 if not values.pop(f):
887                     remove.extend(get_boolean_groups(f))
888             elif is_selection_groups(f):
889                 remove.extend(get_selection_groups(f))
890                 selected = values.pop(f)
891                 if selected:
892                     add.append(selected)
893         # update values *only* if groups are being modified, otherwise
894         # we introduce spurious changes that might break the super.write() call.
895         if add or remove:
896             # remove groups in 'remove' and add groups in 'add'
897             values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
898
899     def default_get(self, cr, uid, fields, context=None):
900         group_fields, fields = partition(is_reified_group, fields)
901         fields1 = (fields + ['groups_id']) if group_fields else fields
902         values = super(users_view, self).default_get(cr, uid, fields1, context)
903         self._get_reified_groups(group_fields, values)
904         return values
905
906     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
907         if not fields:
908             fields = self.fields_get(cr, uid, context=context).keys()
909         group_fields, fields = partition(is_reified_group, fields)
910         if not 'groups_id' in fields:
911             fields.append('groups_id')
912         res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
913         for values in (res if isinstance(res, list) else [res]):
914             self._get_reified_groups(group_fields, values)
915         return res
916
917     def _get_reified_groups(self, fields, values):
918         """ compute the given reified group fields from values['groups_id'] """
919         gids = set(values.get('groups_id') or [])
920         for f in fields:
921             if is_boolean_group(f):
922                 values[f] = get_boolean_group(f) in gids
923             elif is_boolean_groups(f):
924                 values[f] = not gids.isdisjoint(get_boolean_groups(f))
925             elif is_selection_groups(f):
926                 selected = [gid for gid in get_selection_groups(f) if gid in gids]
927                 values[f] = selected and selected[-1] or False
928
929     def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
930         res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
931         # add reified groups fields
932         for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
933             if kind == 'selection':
934                 # selection group field
935                 tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
936                 res[name_selection_groups(map(int, gs))] = {
937                     'type': 'selection',
938                     'string': app and app.name or _('Other'),
939                     'selection': [(False, '')] + [(g.id, g.name) for g in gs],
940                     'help': '\n'.join(tips),
941                 }
942             else:
943                 # boolean group fields
944                 for g in gs:
945                     res[name_boolean_group(g.id)] = {
946                         'type': 'boolean',
947                         'string': g.name,
948                         'help': g.comment,
949                     }
950         return res
951
952 users_view()
953
954 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: