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