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