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