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