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