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