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