[FIX] res.users: safer read() when computing default company value
[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-2014 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 import itertools
23 import logging
24 from functools import partial
25
26 from lxml import etree
27 from lxml.builder import E
28
29 import openerp
30 from openerp import SUPERUSER_ID, models
31 from openerp import tools
32 import openerp.exceptions
33 from openerp.osv import fields, osv, expression
34 from openerp.tools.translate import _
35 from openerp.http import request
36
37 _logger = logging.getLogger(__name__)
38
39 #----------------------------------------------------------
40 # Basic res.groups and res.users
41 #----------------------------------------------------------
42
43 class res_groups(osv.osv):
44     _name = "res.groups"
45     _description = "Access Groups"
46     _rec_name = 'full_name'
47     _order = 'name'
48
49     def _get_full_name(self, cr, uid, ids, field, arg, context=None):
50         res = {}
51         for g in self.browse(cr, uid, ids, context):
52             if g.category_id:
53                 res[g.id] = '%s / %s' % (g.category_id.name, g.name)
54             else:
55                 res[g.id] = g.name
56         return res
57
58     def _search_group(self, cr, uid, obj, name, args, context=None):
59         operand = args[0][2]
60         operator = args[0][1]
61         lst = True
62         if isinstance(operand, bool):
63             domains = [[('name', operator, operand)], [('category_id.name', operator, operand)]]
64             if operator in expression.NEGATIVE_TERM_OPERATORS == (not operand):
65                 return expression.AND(domains)
66             else:
67                 return expression.OR(domains)
68         if isinstance(operand, basestring):
69             lst = False
70             operand = [operand]
71         where = []
72         for group in operand:
73             values = filter(bool, group.split('/'))
74             group_name = values.pop().strip()
75             category_name = values and '/'.join(values).strip() or group_name
76             group_domain = [('name', operator, lst and [group_name] or group_name)]
77             category_domain = [('category_id.name', operator, lst and [category_name] or category_name)]
78             if operator in expression.NEGATIVE_TERM_OPERATORS and not values:
79                 category_domain = expression.OR([category_domain, [('category_id', '=', False)]])
80             if (operator in expression.NEGATIVE_TERM_OPERATORS) == (not values):
81                 sub_where = expression.AND([group_domain, category_domain])
82             else:
83                 sub_where = expression.OR([group_domain, category_domain])
84             if operator in expression.NEGATIVE_TERM_OPERATORS:
85                 where = expression.AND([where, sub_where])
86             else:
87                 where = expression.OR([where, sub_where])
88         return where
89
90     _columns = {
91         'name': fields.char('Name', required=True, translate=True),
92         'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
93         'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
94         'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
95             'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
96         'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
97         'view_access': fields.many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', 'Views'),
98         'comment' : fields.text('Comment', size=250, translate=True),
99         'category_id': fields.many2one('ir.module.category', 'Application', select=True),
100         'full_name': fields.function(_get_full_name, type='char', string='Group Name', fnct_search=_search_group),
101     }
102
103     _sql_constraints = [
104         ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique within an application!')
105     ]
106
107     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
108         # add explicit ordering if search is sorted on full_name
109         if order and order.startswith('full_name'):
110             ids = super(res_groups, self).search(cr, uid, args, context=context)
111             gs = self.browse(cr, uid, ids, context)
112             gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
113             gs = gs[offset:offset+limit] if limit else gs[offset:]
114             return map(int, gs)
115         return super(res_groups, self).search(cr, uid, args, offset, limit, order, context, count)
116
117     def copy(self, cr, uid, id, default=None, context=None):
118         group_name = self.read(cr, uid, [id], ['name'])[0]['name']
119         default.update({'name': _('%s (copy)')%group_name})
120         return super(res_groups, self).copy(cr, uid, id, default, context)
121
122     def write(self, cr, uid, ids, vals, context=None):
123         if 'name' in vals:
124             if vals['name'].startswith('-'):
125                 raise osv.except_osv(_('Error'),
126                         _('The name of the group can not start with "-"'))
127         res = super(res_groups, self).write(cr, uid, ids, vals, context=context)
128         self.pool['ir.model.access'].call_cache_clearing_methods(cr)
129         self.pool['res.users'].has_group.clear_cache(self.pool['res.users'])
130         return res
131
132 class res_users(osv.osv):
133     """ User class. A res.users record models an OpenERP user and is different
134         from an employee.
135
136         res.users class now inherits from res.partner. The partner model is
137         used to store the data related to the partner: lang, name, address,
138         avatar, ... The user model is now dedicated to technical data.
139     """
140     __admin_ids = {}
141     _uid_cache = {}
142     _inherits = {
143         'res.partner': 'partner_id',
144     }
145     _name = "res.users"
146     _description = 'Users'
147
148     def _set_new_password(self, cr, uid, id, name, value, args, context=None):
149         if value is False:
150             # Do not update the password if no value is provided, ignore silently.
151             # For example web client submits False values for all empty fields.
152             return
153         if uid == id:
154             # To change their own password users must use the client-specific change password wizard,
155             # so that the new password is immediately used for further RPC requests, otherwise the user
156             # will face unexpected 'Access Denied' exceptions.
157             raise osv.except_osv(_('Operation Canceled'), _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
158         self.write(cr, uid, id, {'password': value})
159
160     def _get_password(self, cr, uid, ids, arg, karg, context=None):
161         return dict.fromkeys(ids, '')
162
163     _columns = {
164         'id': fields.integer('ID'),
165         'login_date': fields.date('Latest connection', select=1),
166         'partner_id': fields.many2one('res.partner', required=True,
167             string='Related Partner', ondelete='restrict',
168             help='Partner-related data of the user'),
169         'login': fields.char('Login', size=64, required=True,
170             help="Used to log into the system"),
171         'password': fields.char('Password', size=64, invisible=True, copy=False,
172             help="Keep empty if you don't want the user to be able to connect on the system."),
173         'new_password': fields.function(_get_password, type='char', size=64,
174             fnct_inv=_set_new_password, string='Set Password',
175             help="Specify a value only when creating a user or if you're "\
176                  "changing the user's password, otherwise leave empty. After "\
177                  "a change of password, the user has to login again."),
178         'signature': fields.html('Signature'),
179         'active': fields.boolean('Active'),
180         'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at log on for this user, in addition to the standard menu."),
181         'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
182         # Special behavior for this field: res.company.search() will only return the companies
183         # available to the current user (should be the user's companies?), when the user_preference
184         # context is set.
185         'company_id': fields.many2one('res.company', 'Company', required=True,
186             help='The company this user is currently working for.', context={'user_preference': True}),
187         'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
188     }
189
190     def on_change_login(self, cr, uid, ids, login, context=None):
191         if login and tools.single_email_re.match(login):
192             return {'value': {'email': login}}
193         return {}
194
195     def onchange_state(self, cr, uid, ids, state_id, context=None):
196         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
197         return self.pool.get('res.partner').onchange_state(cr, uid, partner_ids, state_id, context=context)
198
199     def onchange_type(self, cr, uid, ids, is_company, context=None):
200         """ Wrapper on the user.partner onchange_type, because some calls to the
201             partner form view applied to the user may trigger the
202             partner.onchange_type method, but applied to the user object.
203         """
204         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
205         return self.pool['res.partner'].onchange_type(cr, uid, partner_ids, is_company, context=context)
206
207     def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None):
208         """ Wrapper on the user.partner onchange_address, because some calls to the
209             partner form view applied to the user may trigger the
210             partner.onchange_type method, but applied to the user object.
211         """
212         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)]
213         return self.pool['res.partner'].onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context)
214
215     def _check_company(self, cr, uid, ids, context=None):
216         return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
217
218     _constraints = [
219         (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
220     ]
221
222     _sql_constraints = [
223         ('login_key', 'UNIQUE (login)',  'You can not have two users with the same login !')
224     ]
225
226     def _get_company(self,cr, uid, context=None, uid2=False):
227         if not uid2:
228             uid2 = uid
229         # Use read() to compute default company, and pass load=_classic_write to
230         # avoid useless name_get() calls. This will avoid prefetching fields
231         # while computing default values for new db columns, as the
232         # db backend may not be fully initialized yet.
233         user_data = self.pool['res.users'].read(cr, uid, uid2, ['company_id'],
234                                                 context=context, load='_classic_write')
235         comp_id = user_data['company_id']
236         return comp_id or False
237
238     def _get_companies(self, cr, uid, context=None):
239         c = self._get_company(cr, uid, context)
240         if c:
241             return [c]
242         return False
243
244     def _get_group(self,cr, uid, context=None):
245         dataobj = self.pool.get('ir.model.data')
246         result = []
247         try:
248             dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_user')
249             result.append(group_id)
250             dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_partner_manager')
251             result.append(group_id)
252         except ValueError:
253             # If these groups does not exists anymore
254             pass
255         return result
256
257     def _get_default_image(self, cr, uid, context=None):
258         return self.pool['res.partner']._get_default_image(cr, uid, False, colorize=True, context=context)
259
260     _defaults = {
261         'password': '',
262         'active': True,
263         'customer': False,
264         'company_id': _get_company,
265         'company_ids': _get_companies,
266         'groups_id': _get_group,
267         'image': _get_default_image,
268     }
269
270     # User can write on a few of his own fields (but not his groups for example)
271     SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz']
272     # User can read a few of his own fields
273     SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update']
274
275     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
276         def override_password(o):
277             if 'password' in o and ('id' not in o or o['id'] != uid):
278                 o['password'] = '********'
279             return o
280
281         if fields and (ids == [uid] or ids == uid):
282             for key in fields:
283                 if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')):
284                     break
285             else:
286                 # safe fields only, so we read as super-user to bypass access rights
287                 uid = SUPERUSER_ID
288
289         result = super(res_users, self).read(cr, uid, ids, fields=fields, context=context, load=load)
290         canwrite = self.pool['ir.model.access'].check(cr, uid, 'res.users', 'write', False)
291         if not canwrite:
292             if isinstance(ids, (int, long)):
293                 result = override_password(result)
294             else:
295                 result = map(override_password, result)
296
297         return result
298
299     def create(self, cr, uid, vals, context=None):
300         user_id = super(res_users, self).create(cr, uid, vals, context=context)
301         user = self.browse(cr, uid, user_id, context=context)
302         if user.partner_id.company_id: 
303             user.partner_id.write({'company_id': user.company_id.id})
304         return user_id
305
306     def write(self, cr, uid, ids, values, context=None):
307         if not hasattr(ids, '__iter__'):
308             ids = [ids]
309         if ids == [uid]:
310             for key in values.keys():
311                 if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
312                     break
313             else:
314                 if 'company_id' in values:
315                     user = self.browse(cr, SUPERUSER_ID, uid, context=context)
316                     if not (values['company_id'] in user.company_ids.ids):
317                         del values['company_id']
318                 uid = 1 # safe fields only, so we write as super-user to bypass access rights
319
320         res = super(res_users, self).write(cr, uid, ids, values, context=context)
321         if 'company_id' in values:
322             for user in self.browse(cr, uid, ids, context=context):
323                 # if partner is global we keep it that way
324                 if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']: 
325                     user.partner_id.write({'company_id': user.company_id.id})
326         # clear caches linked to the users
327         self.pool['ir.model.access'].call_cache_clearing_methods(cr)
328         clear = partial(self.pool['ir.rule'].clear_cache, cr)
329         map(clear, ids)
330         db = cr.dbname
331         if db in self._uid_cache:
332             for id in ids:
333                 if id in self._uid_cache[db]:
334                     del self._uid_cache[db][id]
335         self.context_get.clear_cache(self)
336         self.has_group.clear_cache(self)
337         return res
338
339     def unlink(self, cr, uid, ids, context=None):
340         if 1 in ids:
341             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 Odoo (updates, module installation, ...)'))
342         db = cr.dbname
343         if db in self._uid_cache:
344             for id in ids:
345                 if id in self._uid_cache[db]:
346                     del self._uid_cache[db][id]
347         return super(res_users, self).unlink(cr, uid, ids, context=context)
348
349     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
350         if not args:
351             args=[]
352         if not context:
353             context={}
354         ids = []
355         if name:
356             ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit, context=context)
357         if not ids:
358             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
359         return self.name_get(cr, user, ids, context=context)
360
361     def copy(self, cr, uid, id, default=None, context=None):
362         user2copy = self.read(cr, uid, [id], ['login','name'])[0]
363         default = dict(default or {})
364         if ('name' not in default) and ('partner_id' not in default):
365             default['name'] = _("%s (copy)") % user2copy['name']
366         if 'login' not in default:
367             default['login'] = _("%s (copy)") % user2copy['login']
368         return super(res_users, self).copy(cr, uid, id, default, context)
369
370     @tools.ormcache(skiparg=2)
371     def context_get(self, cr, uid, context=None):
372         user = self.browse(cr, SUPERUSER_ID, uid, context)
373         result = {}
374         for k in self._all_columns.keys():
375             if k.startswith('context_'):
376                 context_key = k[8:]
377             elif k in ['lang', 'tz']:
378                 context_key = k
379             else:
380                 context_key = False
381             if context_key:
382                 res = getattr(user, k) or False
383                 if isinstance(res, models.BaseModel):
384                     res = res.id
385                 result[context_key] = res or False
386         return result
387
388     def action_get(self, cr, uid, context=None):
389         dataobj = self.pool['ir.model.data']
390         data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
391         return dataobj.browse(cr, uid, data_id, context=context).res_id
392
393     def check_super(self, passwd):
394         if passwd == tools.config['admin_passwd']:
395             return True
396         else:
397             raise openerp.exceptions.AccessDenied()
398
399     def check_credentials(self, cr, uid, password):
400         """ Override this method to plug additional authentication methods"""
401         res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
402         if not res:
403             raise openerp.exceptions.AccessDenied()
404
405     def _login(self, db, login, password):
406         if not password:
407             return False
408         user_id = False
409         cr = self.pool.cursor()
410         try:
411             # autocommit: our single update request will be performed atomically.
412             # (In this way, there is no opportunity to have two transactions
413             # interleaving their cr.execute()..cr.commit() calls and have one
414             # of them rolled back due to a concurrent access.)
415             cr.autocommit(True)
416             # check if user exists
417             res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
418             if res:
419                 user_id = res[0]
420                 # check credentials
421                 self.check_credentials(cr, user_id, password)
422                 # We effectively unconditionally write the res_users line.
423                 # Even w/ autocommit there's a chance the user row will be locked,
424                 # in which case we can't delay the login just for the purpose of
425                 # update the last login date - hence we use FOR UPDATE NOWAIT to
426                 # try to get the lock - fail-fast
427                 # Failing to acquire the lock on the res_users row probably means
428                 # another request is holding it. No big deal, we don't want to
429                 # prevent/delay login in that case. It will also have been logged
430                 # as a SQL error, if anyone cares.
431                 try:
432                     cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
433                     cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
434                     self.invalidate_cache(cr, user_id, ['login_date'], [user_id])
435                 except Exception:
436                     _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
437         except openerp.exceptions.AccessDenied:
438             _logger.info("Login failed for db:%s login:%s", db, login)
439             user_id = False
440         finally:
441             cr.close()
442
443         return user_id
444
445     def authenticate(self, db, login, password, user_agent_env):
446         """Verifies and returns the user ID corresponding to the given
447           ``login`` and ``password`` combination, or False if there was
448           no matching user.
449
450            :param str db: the database on which user is trying to authenticate
451            :param str login: username
452            :param str password: user password
453            :param dict user_agent_env: environment dictionary describing any
454                relevant environment attributes
455         """
456         uid = self._login(db, login, password)
457         if uid == openerp.SUPERUSER_ID:
458             # Successfully logged in as admin!
459             # Attempt to guess the web base url...
460             if user_agent_env and user_agent_env.get('base_location'):
461                 cr = self.pool.cursor()
462                 try:
463                     base = user_agent_env['base_location']
464                     ICP = self.pool['ir.config_parameter']
465                     if not ICP.get_param(cr, uid, 'web.base.url.freeze'):
466                         ICP.set_param(cr, uid, 'web.base.url', base)
467                     cr.commit()
468                 except Exception:
469                     _logger.exception("Failed to update web.base.url configuration parameter")
470                 finally:
471                     cr.close()
472         return uid
473
474     def check(self, db, uid, passwd):
475         """Verifies that the given (uid, password) is authorized for the database ``db`` and
476            raise an exception if it is not."""
477         if not passwd:
478             # empty passwords disallowed for obvious security reasons
479             raise openerp.exceptions.AccessDenied()
480         if self._uid_cache.get(db, {}).get(uid) == passwd:
481             return
482         cr = self.pool.cursor()
483         try:
484             self.check_credentials(cr, uid, passwd)
485             if self._uid_cache.has_key(db):
486                 self._uid_cache[db][uid] = passwd
487             else:
488                 self._uid_cache[db] = {uid:passwd}
489         finally:
490             cr.close()
491
492     def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
493         """Change current user password. Old password must be provided explicitly
494         to prevent hijacking an existing user session, or for cases where the cleartext
495         password is not used to authenticate requests.
496
497         :return: True
498         :raise: openerp.exceptions.AccessDenied when old password is wrong
499         :raise: except_osv when new password is not set or empty
500         """
501         self.check(cr.dbname, uid, old_passwd)
502         if new_passwd:
503             return self.write(cr, uid, uid, {'password': new_passwd})
504         raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
505
506     def preference_save(self, cr, uid, ids, context=None):
507         return {
508             'type': 'ir.actions.client',
509             'tag': 'reload_context',
510         }
511
512     def preference_change_password(self, cr, uid, ids, context=None):
513         return {
514             'type': 'ir.actions.client',
515             'tag': 'change_password',
516             'target': 'new',
517         }
518
519     @tools.ormcache(skiparg=2)
520     def has_group(self, cr, uid, group_ext_id):
521         """Checks whether user belongs to given group.
522
523         :param str group_ext_id: external ID (XML ID) of the group.
524            Must be provided in fully-qualified form (``module.ext_id``), as there
525            is no implicit module to use..
526         :return: True if the current user is a member of the group with the
527            given external ID (XML ID), else False.
528         """
529         assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
530         module, ext_id = group_ext_id.split('.')
531         cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
532                         (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
533                    (uid, module, ext_id))
534         return bool(cr.fetchone())
535
536 #----------------------------------------------------------
537 # Implied groups
538 #
539 # Extension of res.groups and res.users with a relation for "implied"
540 # or "inherited" groups.  Once a user belongs to a group, it
541 # automatically belongs to the implied groups (transitively).
542 #----------------------------------------------------------
543
544 class cset(object):
545     """ A cset (constrained set) is a set of elements that may be constrained to
546         be a subset of other csets.  Elements added to a cset are automatically
547         added to its supersets.  Cycles in the subset constraints are supported.
548     """
549     def __init__(self, xs):
550         self.supersets = set()
551         self.elements = set(xs)
552     def subsetof(self, other):
553         if other is not self:
554             self.supersets.add(other)
555             other.update(self.elements)
556     def update(self, xs):
557         xs = set(xs) - self.elements
558         if xs:      # xs will eventually be empty in case of a cycle
559             self.elements.update(xs)
560             for s in self.supersets:
561                 s.update(xs)
562     def __iter__(self):
563         return iter(self.elements)
564
565 concat = itertools.chain.from_iterable
566
567 class groups_implied(osv.osv):
568     _inherit = 'res.groups'
569
570     def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
571         "computes the transitive closure of relation implied_ids"
572         memo = {}           # use a memo for performance and cycle avoidance
573         def computed_set(g):
574             if g not in memo:
575                 memo[g] = cset(g.implied_ids)
576                 for h in g.implied_ids:
577                     computed_set(h).subsetof(memo[g])
578             return memo[g]
579
580         res = {}
581         for g in self.browse(cr, SUPERUSER_ID, ids, context):
582             res[g.id] = map(int, computed_set(g))
583         return res
584
585     _columns = {
586         'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
587             string='Inherits', help='Users of this group automatically inherit those groups'),
588         'trans_implied_ids': fields.function(_get_trans_implied,
589             type='many2many', relation='res.groups', string='Transitively inherits'),
590     }
591
592     def create(self, cr, uid, values, context=None):
593         users = values.pop('users', None)
594         gid = super(groups_implied, self).create(cr, uid, values, context)
595         if users:
596             # delegate addition of users to add implied groups
597             self.write(cr, uid, [gid], {'users': users}, context)
598         return gid
599
600     def write(self, cr, uid, ids, values, context=None):
601         res = super(groups_implied, self).write(cr, uid, ids, values, context)
602         if values.get('users') or values.get('implied_ids'):
603             # add all implied groups (to all users of each group)
604             for g in self.browse(cr, uid, ids, context=context):
605                 gids = map(int, g.trans_implied_ids)
606                 vals = {'users': [(4, u.id) for u in g.users]}
607                 super(groups_implied, self).write(cr, uid, gids, vals, context)
608         return res
609
610 class users_implied(osv.osv):
611     _inherit = 'res.users'
612
613     def create(self, cr, uid, values, context=None):
614         groups = values.pop('groups_id', None)
615         user_id = super(users_implied, self).create(cr, uid, values, context)
616         if groups:
617             # delegate addition of groups to add implied groups
618             self.write(cr, uid, [user_id], {'groups_id': groups}, context)
619             self.pool['ir.ui.view'].clear_cache()
620         return user_id
621
622     def write(self, cr, uid, ids, values, context=None):
623         if not isinstance(ids,list):
624             ids = [ids]
625         res = super(users_implied, self).write(cr, uid, ids, values, context)
626         if values.get('groups_id'):
627             # add implied groups for all users
628             for user in self.browse(cr, uid, ids):
629                 gs = set(concat(g.trans_implied_ids for g in user.groups_id))
630                 vals = {'groups_id': [(4, g.id) for g in gs]}
631                 super(users_implied, self).write(cr, uid, [user.id], vals, context)
632             self.pool['ir.ui.view'].clear_cache()
633         return res
634
635 #----------------------------------------------------------
636 # Vitrual checkbox and selection for res.user form view
637 #
638 # Extension of res.groups and res.users for the special groups view in the users
639 # form.  This extension presents groups with selection and boolean widgets:
640 # - Groups are shown by application, with boolean and/or selection fields.
641 #   Selection fields typically defines a role "Name" for the given application.
642 # - Uncategorized groups are presented as boolean fields and grouped in a
643 #   section "Others".
644 #
645 # The user form view is modified by an inherited view (base.user_groups_view);
646 # the inherited view replaces the field 'groups_id' by a set of reified group
647 # fields (boolean or selection fields).  The arch of that view is regenerated
648 # each time groups are changed.
649 #
650 # Naming conventions for reified groups fields:
651 # - boolean field 'in_group_ID' is True iff
652 #       ID is in 'groups_id'
653 # - boolean field 'in_groups_ID1_..._IDk' is True iff
654 #       any of ID1, ..., IDk is in 'groups_id'
655 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
656 #       ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
657 #----------------------------------------------------------
658
659 def name_boolean_group(id): return 'in_group_' + str(id)
660 def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids))
661 def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
662
663 def is_boolean_group(name): return name.startswith('in_group_')
664 def is_boolean_groups(name): return name.startswith('in_groups_')
665 def is_selection_groups(name): return name.startswith('sel_groups_')
666 def is_reified_group(name):
667     return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
668
669 def get_boolean_group(name): return int(name[9:])
670 def get_boolean_groups(name): return map(int, name[10:].split('_'))
671 def get_selection_groups(name): return map(int, name[11:].split('_'))
672
673 def partition(f, xs):
674     "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
675     yes, nos = [], []
676     for x in xs:
677         (yes if f(x) else nos).append(x)
678     return yes, nos
679
680 def parse_m2m(commands):
681     "return a list of ids corresponding to a many2many value"
682     ids = []
683     for command in commands:
684         if isinstance(command, (tuple, list)):
685             if command[0] in (1, 4):
686                 ids.append(command[2])
687             elif command[0] == 5:
688                 ids = []
689             elif command[0] == 6:
690                 ids = list(command[2])
691         else:
692             ids.append(command)
693     return ids
694
695
696 class groups_view(osv.osv):
697     _inherit = 'res.groups'
698
699     def create(self, cr, uid, values, context=None):
700         res = super(groups_view, self).create(cr, uid, values, context)
701         self.update_user_groups_view(cr, uid, context)
702         return res
703
704     def write(self, cr, uid, ids, values, context=None):
705         res = super(groups_view, self).write(cr, uid, ids, values, context)
706         self.update_user_groups_view(cr, uid, context)
707         return res
708
709     def unlink(self, cr, uid, ids, context=None):
710         res = super(groups_view, self).unlink(cr, uid, ids, context)
711         self.update_user_groups_view(cr, uid, context)
712         return res
713
714     def update_user_groups_view(self, cr, uid, context=None):
715         # the view with id 'base.user_groups_view' inherits the user form view,
716         # and introduces the reified group fields
717         # we have to try-catch this, because at first init the view does not exist
718         # but we are already creating some basic groups
719         view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context)
720         if view and view.exists() and view._name == 'ir.ui.view':
721             xml1, xml2 = [], []
722             xml1.append(E.separator(string=_('Application'), colspan="4"))
723             for app, kind, gs in self.get_groups_by_application(cr, uid, context):
724                 # hide groups in category 'Hidden' (except to group_no_one)
725                 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
726                 if kind == 'selection':
727                     # application name with a selection field
728                     field_name = name_selection_groups(map(int, gs))
729                     xml1.append(E.field(name=field_name, **attrs))
730                     xml1.append(E.newline())
731                 else:
732                     # application separator with boolean fields
733                     app_name = app and app.name or _('Other')
734                     xml2.append(E.separator(string=app_name, colspan="4", **attrs))
735                     for g in gs:
736                         field_name = name_boolean_group(g.id)
737                         xml2.append(E.field(name=field_name, **attrs))
738
739             xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
740             xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
741             xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
742             view.write({'arch': xml_content})
743         return True
744
745     def get_application_groups(self, cr, uid, domain=None, context=None):
746         return self.search(cr, uid, domain or [])
747
748     def get_groups_by_application(self, cr, uid, context=None):
749         """ return all groups classified by application (module category), as a list of pairs:
750                 [(app, kind, [group, ...]), ...],
751             where app and group are browse records, and kind is either 'boolean' or 'selection'.
752             Applications are given in sequence order.  If kind is 'selection', the groups are
753             given in reverse implication order.
754         """
755         def linearized(gs):
756             gs = set(gs)
757             # determine sequence order: a group should appear after its implied groups
758             order = dict.fromkeys(gs, 0)
759             for g in gs:
760                 for h in gs.intersection(g.trans_implied_ids):
761                     order[h] -= 1
762             # check whether order is total, i.e., sequence orders are distinct
763             if len(set(order.itervalues())) == len(gs):
764                 return sorted(gs, key=lambda g: order[g])
765             return None
766
767         # classify all groups by application
768         gids = self.get_application_groups(cr, uid, context=context)
769         by_app, others = {}, []
770         for g in self.browse(cr, uid, gids, context):
771             if g.category_id:
772                 by_app.setdefault(g.category_id, []).append(g)
773             else:
774                 others.append(g)
775         # build the result
776         res = []
777         apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
778         for app in apps:
779             gs = linearized(by_app[app])
780             if gs:
781                 res.append((app, 'selection', gs))
782             else:
783                 res.append((app, 'boolean', by_app[app]))
784         if others:
785             res.append((False, 'boolean', others))
786         return res
787
788 class users_view(osv.osv):
789     _inherit = 'res.users'
790
791     def create(self, cr, uid, values, context=None):
792         self._set_reified_groups(values)
793         return super(users_view, self).create(cr, uid, values, context)
794
795     def write(self, cr, uid, ids, values, context=None):
796         self._set_reified_groups(values)
797         return super(users_view, self).write(cr, uid, ids, values, context)
798
799     def _set_reified_groups(self, values):
800         """ reflect reified group fields in values['groups_id'] """
801         if 'groups_id' in values:
802             # groups are already given, ignore group fields
803             for f in filter(is_reified_group, values.iterkeys()):
804                 del values[f]
805             return
806
807         add, remove = [], []
808         for f in values.keys():
809             if is_boolean_group(f):
810                 target = add if values.pop(f) else remove
811                 target.append(get_boolean_group(f))
812             elif is_boolean_groups(f):
813                 if not values.pop(f):
814                     remove.extend(get_boolean_groups(f))
815             elif is_selection_groups(f):
816                 remove.extend(get_selection_groups(f))
817                 selected = values.pop(f)
818                 if selected:
819                     add.append(selected)
820         # update values *only* if groups are being modified, otherwise
821         # we introduce spurious changes that might break the super.write() call.
822         if add or remove:
823             # remove groups in 'remove' and add groups in 'add'
824             values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
825
826     def default_get(self, cr, uid, fields, context=None):
827         group_fields, fields = partition(is_reified_group, fields)
828         fields1 = (fields + ['groups_id']) if group_fields else fields
829         values = super(users_view, self).default_get(cr, uid, fields1, context)
830         self._get_reified_groups(group_fields, values)
831
832         # add "default_groups_ref" inside the context to set default value for group_id with xml values
833         if 'groups_id' in fields and isinstance(context.get("default_groups_ref"), list):
834             groups = []
835             ir_model_data = self.pool.get('ir.model.data')
836             for group_xml_id in context["default_groups_ref"]:
837                 group_split = group_xml_id.split('.')
838                 if len(group_split) != 2:
839                     raise osv.except_osv(_('Invalid context value'), _('Invalid context default_groups_ref value (model.name_id) : "%s"') % group_xml_id)
840                 try:
841                     temp, group_id = ir_model_data.get_object_reference(cr, uid, group_split[0], group_split[1])
842                 except ValueError:
843                     group_id = False
844                 groups += [group_id]
845             values['groups_id'] = groups
846         return values
847
848     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
849         fields_get = fields if fields is not None else self.fields_get(cr, uid, context=context).keys()
850         group_fields, _ = partition(is_reified_group, fields_get)
851
852         inject_groups_id = group_fields and fields and 'groups_id' not in fields
853         if inject_groups_id:
854             fields.append('groups_id')
855         res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
856
857         if res and group_fields:
858             for values in (res if isinstance(res, list) else [res]):
859                 self._get_reified_groups(group_fields, values)
860                 if inject_groups_id:
861                     values.pop('groups_id', None)
862         return res
863
864     def _get_reified_groups(self, fields, values):
865         """ compute the given reified group fields from values['groups_id'] """
866         gids = set(parse_m2m(values.get('groups_id') or []))
867         for f in fields:
868             if is_boolean_group(f):
869                 values[f] = get_boolean_group(f) in gids
870             elif is_boolean_groups(f):
871                 values[f] = not gids.isdisjoint(get_boolean_groups(f))
872             elif is_selection_groups(f):
873                 selected = [gid for gid in get_selection_groups(f) if gid in gids]
874                 values[f] = selected and selected[-1] or False
875
876     def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
877         res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
878         # add reified groups fields
879         for app, kind, gs in self.pool['res.groups'].get_groups_by_application(cr, uid, context):
880             if kind == 'selection':
881                 # selection group field
882                 tips = ['%s: %s' % (g.name, g.comment) for g in gs if g.comment]
883                 res[name_selection_groups(map(int, gs))] = {
884                     'type': 'selection',
885                     'string': app and app.name or _('Other'),
886                     'selection': [(False, '')] + [(g.id, g.name) for g in gs],
887                     'help': '\n'.join(tips),
888                     'exportable': False,
889                     'selectable': False,
890                 }
891             else:
892                 # boolean group fields
893                 for g in gs:
894                     res[name_boolean_group(g.id)] = {
895                         'type': 'boolean',
896                         'string': g.name,
897                         'help': g.comment,
898                         'exportable': False,
899                         'selectable': False,
900                     }
901         return res
902
903 #----------------------------------------------------------
904 # change password wizard
905 #----------------------------------------------------------
906
907 class change_password_wizard(osv.TransientModel):
908     """
909         A wizard to manage the change of users' passwords
910     """
911
912     _name = "change.password.wizard"
913     _description = "Change Password Wizard"
914     _columns = {
915         'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'),
916     }
917
918     def default_get(self, cr, uid, fields, context=None):
919         if context == None:
920             context = {}
921         user_ids = context.get('active_ids', [])
922         wiz_id = context.get('active_id', None)
923         res = []
924         users = self.pool.get('res.users').browse(cr, uid, user_ids, context=context)
925         for user in users:
926             res.append((0, 0, {
927                 'wizard_id': wiz_id,
928                 'user_id': user.id,
929                 'user_login': user.login,
930             }))
931         return {'user_ids': res}
932
933     def change_password_button(self, cr, uid, id, context=None):
934         wizard = self.browse(cr, uid, id, context=context)[0]
935         need_reload = any(uid == user.user_id.id for user in wizard.user_ids)
936         line_ids = [user.id for user in wizard.user_ids]
937
938         self.pool.get('change.password.user').change_password_button(cr, uid, line_ids, context=context)
939         # don't keep temporary password copies in the database longer than necessary
940         self.pool.get('change.password.user').write(cr, uid, line_ids, {'new_passwd': False}, context=context)
941
942         if need_reload:
943             return {
944                 'type': 'ir.actions.client',
945                 'tag': 'reload'
946             }
947
948         return {'type': 'ir.actions.act_window_close'}
949
950 class change_password_user(osv.TransientModel):
951     """
952         A model to configure users in the change password wizard
953     """
954
955     _name = 'change.password.user'
956     _description = 'Change Password Wizard User'
957     _columns = {
958         'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True),
959         'user_id': fields.many2one('res.users', string='User', required=True),
960         'user_login': fields.char('User Login', readonly=True),
961         'new_passwd': fields.char('New Password'),
962     }
963     _defaults = {
964         'new_passwd': '',
965     }
966
967     def change_password_button(self, cr, uid, ids, context=None):
968         for user in self.browse(cr, uid, ids, context=context):
969             self.pool.get('res.users').write(cr, uid, user.user_id.id, {'password': user.new_passwd})
970
971
972 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: