[IMP] models: inherited fields are related fields read as the current user
[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     @tools.ormcache(skiparg=2)
377     def context_get(self, cr, uid, context=None):
378         user = self.browse(cr, SUPERUSER_ID, uid, context)
379         result = {}
380         for k in self._all_columns.keys():
381             if k.startswith('context_'):
382                 context_key = k[8:]
383             elif k in ['lang', 'tz']:
384                 context_key = k
385             else:
386                 context_key = False
387             if context_key:
388                 res = getattr(user, k) or False
389                 if isinstance(res, models.BaseModel):
390                     res = res.id
391                 result[context_key] = res or False
392         return result
393
394     def action_get(self, cr, uid, context=None):
395         dataobj = self.pool['ir.model.data']
396         data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
397         return dataobj.browse(cr, uid, data_id, context=context).res_id
398
399     def check_super(self, passwd):
400         if passwd == tools.config['admin_passwd']:
401             return True
402         else:
403             raise openerp.exceptions.AccessDenied()
404
405     def check_credentials(self, cr, uid, password):
406         """ Override this method to plug additional authentication methods"""
407         res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)])
408         if not res:
409             raise openerp.exceptions.AccessDenied()
410
411     def _login(self, db, login, password):
412         if not password:
413             return False
414         user_id = False
415         cr = self.pool.cursor()
416         try:
417             # autocommit: our single update request will be performed atomically.
418             # (In this way, there is no opportunity to have two transactions
419             # interleaving their cr.execute()..cr.commit() calls and have one
420             # of them rolled back due to a concurrent access.)
421             cr.autocommit(True)
422             # check if user exists
423             res = self.search(cr, SUPERUSER_ID, [('login','=',login)])
424             if res:
425                 user_id = res[0]
426                 # check credentials
427                 self.check_credentials(cr, user_id, password)
428                 # We effectively unconditionally write the res_users line.
429                 # Even w/ autocommit there's a chance the user row will be locked,
430                 # in which case we can't delay the login just for the purpose of
431                 # update the last login date - hence we use FOR UPDATE NOWAIT to
432                 # try to get the lock - fail-fast
433                 # Failing to acquire the lock on the res_users row probably means
434                 # another request is holding it. No big deal, we don't want to
435                 # prevent/delay login in that case. It will also have been logged
436                 # as a SQL error, if anyone cares.
437                 try:
438                     cr.execute("SELECT id FROM res_users WHERE id=%s FOR UPDATE NOWAIT", (user_id,), log_exceptions=False)
439                     cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,))
440                     self.invalidate_cache(cr, user_id, ['login_date'], [user_id])
441                 except Exception:
442                     _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True)
443         except openerp.exceptions.AccessDenied:
444             _logger.info("Login failed for db:%s login:%s", db, login)
445             user_id = False
446         finally:
447             cr.close()
448
449         return user_id
450
451     def authenticate(self, db, login, password, user_agent_env):
452         """Verifies and returns the user ID corresponding to the given
453           ``login`` and ``password`` combination, or False if there was
454           no matching user.
455
456            :param str db: the database on which user is trying to authenticate
457            :param str login: username
458            :param str password: user password
459            :param dict user_agent_env: environment dictionary describing any
460                relevant environment attributes
461         """
462         uid = self._login(db, login, password)
463         if uid == openerp.SUPERUSER_ID:
464             # Successfully logged in as admin!
465             # Attempt to guess the web base url...
466             if user_agent_env and user_agent_env.get('base_location'):
467                 cr = self.pool.cursor()
468                 try:
469                     base = user_agent_env['base_location']
470                     ICP = self.pool['ir.config_parameter']
471                     if not ICP.get_param(cr, uid, 'web.base.url.freeze'):
472                         ICP.set_param(cr, uid, 'web.base.url', base)
473                     cr.commit()
474                 except Exception:
475                     _logger.exception("Failed to update web.base.url configuration parameter")
476                 finally:
477                     cr.close()
478         return uid
479
480     def check(self, db, uid, passwd):
481         """Verifies that the given (uid, password) is authorized for the database ``db`` and
482            raise an exception if it is not."""
483         if not passwd:
484             # empty passwords disallowed for obvious security reasons
485             raise openerp.exceptions.AccessDenied()
486         if self._uid_cache.get(db, {}).get(uid) == passwd:
487             return
488         cr = self.pool.cursor()
489         try:
490             self.check_credentials(cr, uid, passwd)
491             if self._uid_cache.has_key(db):
492                 self._uid_cache[db][uid] = passwd
493             else:
494                 self._uid_cache[db] = {uid:passwd}
495         finally:
496             cr.close()
497
498     def change_password(self, cr, uid, old_passwd, new_passwd, context=None):
499         """Change current user password. Old password must be provided explicitly
500         to prevent hijacking an existing user session, or for cases where the cleartext
501         password is not used to authenticate requests.
502
503         :return: True
504         :raise: openerp.exceptions.AccessDenied when old password is wrong
505         :raise: except_osv when new password is not set or empty
506         """
507         self.check(cr.dbname, uid, old_passwd)
508         if new_passwd:
509             return self.write(cr, uid, uid, {'password': new_passwd})
510         raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
511
512     def preference_save(self, cr, uid, ids, context=None):
513         return {
514             'type': 'ir.actions.client',
515             'tag': 'reload_context',
516         }
517
518     def preference_change_password(self, cr, uid, ids, context=None):
519         return {
520             'type': 'ir.actions.client',
521             'tag': 'change_password',
522             'target': 'new',
523         }
524
525     @tools.ormcache(skiparg=2)
526     def has_group(self, cr, uid, group_ext_id):
527         """Checks whether user belongs to given group.
528
529         :param str group_ext_id: external ID (XML ID) of the group.
530            Must be provided in fully-qualified form (``module.ext_id``), as there
531            is no implicit module to use..
532         :return: True if the current user is a member of the group with the
533            given external ID (XML ID), else False.
534         """
535         assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
536         module, ext_id = group_ext_id.split('.')
537         cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
538                         (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
539                    (uid, module, ext_id))
540         return bool(cr.fetchone())
541
542 #----------------------------------------------------------
543 # Implied groups
544 #
545 # Extension of res.groups and res.users with a relation for "implied"
546 # or "inherited" groups.  Once a user belongs to a group, it
547 # automatically belongs to the implied groups (transitively).
548 #----------------------------------------------------------
549
550 class cset(object):
551     """ A cset (constrained set) is a set of elements that may be constrained to
552         be a subset of other csets.  Elements added to a cset are automatically
553         added to its supersets.  Cycles in the subset constraints are supported.
554     """
555     def __init__(self, xs):
556         self.supersets = set()
557         self.elements = set(xs)
558     def subsetof(self, other):
559         if other is not self:
560             self.supersets.add(other)
561             other.update(self.elements)
562     def update(self, xs):
563         xs = set(xs) - self.elements
564         if xs:      # xs will eventually be empty in case of a cycle
565             self.elements.update(xs)
566             for s in self.supersets:
567                 s.update(xs)
568     def __iter__(self):
569         return iter(self.elements)
570
571 concat = itertools.chain.from_iterable
572
573 class groups_implied(osv.osv):
574     _inherit = 'res.groups'
575
576     def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
577         "computes the transitive closure of relation implied_ids"
578         memo = {}           # use a memo for performance and cycle avoidance
579         def computed_set(g):
580             if g not in memo:
581                 memo[g] = cset(g.implied_ids)
582                 for h in g.implied_ids:
583                     computed_set(h).subsetof(memo[g])
584             return memo[g]
585
586         res = {}
587         for g in self.browse(cr, SUPERUSER_ID, ids, context):
588             res[g.id] = map(int, computed_set(g))
589         return res
590
591     _columns = {
592         'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
593             string='Inherits', help='Users of this group automatically inherit those groups'),
594         'trans_implied_ids': fields.function(_get_trans_implied,
595             type='many2many', relation='res.groups', string='Transitively inherits'),
596     }
597
598     def create(self, cr, uid, values, context=None):
599         users = values.pop('users', None)
600         gid = super(groups_implied, self).create(cr, uid, values, context)
601         if users:
602             # delegate addition of users to add implied groups
603             self.write(cr, uid, [gid], {'users': users}, context)
604         return gid
605
606     def write(self, cr, uid, ids, values, context=None):
607         res = super(groups_implied, self).write(cr, uid, ids, values, context)
608         if values.get('users') or values.get('implied_ids'):
609             # add all implied groups (to all users of each group)
610             for g in self.browse(cr, uid, ids, context=context):
611                 gids = map(int, g.trans_implied_ids)
612                 vals = {'users': [(4, u.id) for u in g.users]}
613                 super(groups_implied, self).write(cr, uid, gids, vals, context)
614         return res
615
616 class users_implied(osv.osv):
617     _inherit = 'res.users'
618
619     def create(self, cr, uid, values, context=None):
620         groups = values.pop('groups_id', None)
621         user_id = super(users_implied, self).create(cr, uid, values, context)
622         if groups:
623             # delegate addition of groups to add implied groups
624             self.write(cr, uid, [user_id], {'groups_id': groups}, context)
625             self.pool['ir.ui.view'].clear_cache()
626         return user_id
627
628     def write(self, cr, uid, ids, values, context=None):
629         if not isinstance(ids,list):
630             ids = [ids]
631         res = super(users_implied, self).write(cr, uid, ids, values, context)
632         if values.get('groups_id'):
633             # add implied groups for all users
634             for user in self.browse(cr, uid, ids):
635                 gs = set(concat(g.trans_implied_ids for g in user.groups_id))
636                 vals = {'groups_id': [(4, g.id) for g in gs]}
637                 super(users_implied, self).write(cr, uid, [user.id], vals, context)
638             self.pool['ir.ui.view'].clear_cache()
639         return res
640
641 #----------------------------------------------------------
642 # Vitrual checkbox and selection for res.user form view
643 #
644 # Extension of res.groups and res.users for the special groups view in the users
645 # form.  This extension presents groups with selection and boolean widgets:
646 # - Groups are shown by application, with boolean and/or selection fields.
647 #   Selection fields typically defines a role "Name" for the given application.
648 # - Uncategorized groups are presented as boolean fields and grouped in a
649 #   section "Others".
650 #
651 # The user form view is modified by an inherited view (base.user_groups_view);
652 # the inherited view replaces the field 'groups_id' by a set of reified group
653 # fields (boolean or selection fields).  The arch of that view is regenerated
654 # each time groups are changed.
655 #
656 # Naming conventions for reified groups fields:
657 # - boolean field 'in_group_ID' is True iff
658 #       ID is in 'groups_id'
659 # - selection field 'sel_groups_ID1_..._IDk' is ID iff
660 #       ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk}
661 #----------------------------------------------------------
662
663 def name_boolean_group(id):
664     return 'in_group_' + str(id)
665
666 def name_selection_groups(ids):
667     return 'sel_groups_' + '_'.join(map(str, ids))
668
669 def is_boolean_group(name):
670     return name.startswith('in_group_')
671
672 def is_selection_groups(name):
673     return name.startswith('sel_groups_')
674
675 def is_reified_group(name):
676     return is_boolean_group(name) or is_selection_groups(name)
677
678 def get_boolean_group(name):
679     return int(name[9:])
680
681 def get_selection_groups(name):
682     return map(int, name[11:].split('_'))
683
684 def partition(f, xs):
685     "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
686     yes, nos = [], []
687     for x in xs:
688         (yes if f(x) else nos).append(x)
689     return yes, nos
690
691 def parse_m2m(commands):
692     "return a list of ids corresponding to a many2many value"
693     ids = []
694     for command in commands:
695         if isinstance(command, (tuple, list)):
696             if command[0] in (1, 4):
697                 ids.append(command[2])
698             elif command[0] == 5:
699                 ids = []
700             elif command[0] == 6:
701                 ids = list(command[2])
702         else:
703             ids.append(command)
704     return ids
705
706
707 class groups_view(osv.osv):
708     _inherit = 'res.groups'
709
710     def create(self, cr, uid, values, context=None):
711         res = super(groups_view, self).create(cr, uid, values, context)
712         self.update_user_groups_view(cr, uid, context)
713         return res
714
715     def write(self, cr, uid, ids, values, context=None):
716         res = super(groups_view, self).write(cr, uid, ids, values, context)
717         self.update_user_groups_view(cr, uid, context)
718         return res
719
720     def unlink(self, cr, uid, ids, context=None):
721         res = super(groups_view, self).unlink(cr, uid, ids, context)
722         self.update_user_groups_view(cr, uid, context)
723         return res
724
725     def update_user_groups_view(self, cr, uid, context=None):
726         # the view with id 'base.user_groups_view' inherits the user form view,
727         # and introduces the reified group fields
728         # we have to try-catch this, because at first init the view does not exist
729         # but we are already creating some basic groups
730         view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context)
731         if view and view.exists() and view._name == 'ir.ui.view':
732             xml1, xml2 = [], []
733             xml1.append(E.separator(string=_('Application'), colspan="4"))
734             for app, kind, gs in self.get_groups_by_application(cr, uid, context):
735                 # hide groups in category 'Hidden' (except to group_no_one)
736                 attrs = {'groups': 'base.group_no_one'} if app and app.xml_id == 'base.module_category_hidden' else {}
737                 if kind == 'selection':
738                     # application name with a selection field
739                     field_name = name_selection_groups(map(int, gs))
740                     xml1.append(E.field(name=field_name, **attrs))
741                     xml1.append(E.newline())
742                 else:
743                     # application separator with boolean fields
744                     app_name = app and app.name or _('Other')
745                     xml2.append(E.separator(string=app_name, colspan="4", **attrs))
746                     for g in gs:
747                         field_name = name_boolean_group(g.id)
748                         xml2.append(E.field(name=field_name, **attrs))
749
750             xml = E.field(*(xml1 + xml2), name="groups_id", position="replace")
751             xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
752             xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
753             view.write({'arch': xml_content})
754         return True
755
756     def get_application_groups(self, cr, uid, domain=None, context=None):
757         return self.search(cr, uid, domain or [])
758
759     def get_groups_by_application(self, cr, uid, context=None):
760         """ return all groups classified by application (module category), as a list of pairs:
761                 [(app, kind, [group, ...]), ...],
762             where app and group are browse records, and kind is either 'boolean' or 'selection'.
763             Applications are given in sequence order.  If kind is 'selection', the groups are
764             given in reverse implication order.
765         """
766         def linearized(gs):
767             gs = set(gs)
768             # determine sequence order: a group should appear after its implied groups
769             order = dict.fromkeys(gs, 0)
770             for g in gs:
771                 for h in gs.intersection(g.trans_implied_ids):
772                     order[h] -= 1
773             # check whether order is total, i.e., sequence orders are distinct
774             if len(set(order.itervalues())) == len(gs):
775                 return sorted(gs, key=lambda g: order[g])
776             return None
777
778         # classify all groups by application
779         gids = self.get_application_groups(cr, uid, context=context)
780         by_app, others = {}, []
781         for g in self.browse(cr, uid, gids, context):
782             if g.category_id:
783                 by_app.setdefault(g.category_id, []).append(g)
784             else:
785                 others.append(g)
786         # build the result
787         res = []
788         apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
789         for app in apps:
790             gs = linearized(by_app[app])
791             if gs:
792                 res.append((app, 'selection', gs))
793             else:
794                 res.append((app, 'boolean', by_app[app]))
795         if others:
796             res.append((False, 'boolean', others))
797         return res
798
799 class users_view(osv.osv):
800     _inherit = 'res.users'
801
802     def create(self, cr, uid, values, context=None):
803         values = self._remove_reified_groups(values)
804         return super(users_view, self).create(cr, uid, values, context)
805
806     def write(self, cr, uid, ids, values, context=None):
807         values = self._remove_reified_groups(values)
808         return super(users_view, self).write(cr, uid, ids, values, context)
809
810     def _remove_reified_groups(self, values):
811         """ return `values` without reified group fields """
812         add, rem = [], []
813         values1 = {}
814
815         for key, val in values.iteritems():
816             if is_boolean_group(key):
817                 (add if val else rem).append(get_boolean_group(key))
818             elif is_selection_groups(key):
819                 rem += get_selection_groups(key)
820                 if val:
821                     add.append(val)
822             else:
823                 values1[key] = val
824
825         if 'groups_id' not in values and (add or rem):
826             # remove group ids in `rem` and add group ids in `add`
827             values1['groups_id'] = zip(repeat(3), rem) + zip(repeat(4), add)
828
829         return values1
830
831     def default_get(self, cr, uid, fields, context=None):
832         group_fields, fields = partition(is_reified_group, fields)
833         fields1 = (fields + ['groups_id']) if group_fields else fields
834         values = super(users_view, self).default_get(cr, uid, fields1, context)
835         self._add_reified_groups(group_fields, values)
836
837         # add "default_groups_ref" inside the context to set default value for group_id with xml values
838         if 'groups_id' in fields and isinstance(context.get("default_groups_ref"), list):
839             groups = []
840             ir_model_data = self.pool.get('ir.model.data')
841             for group_xml_id in context["default_groups_ref"]:
842                 group_split = group_xml_id.split('.')
843                 if len(group_split) != 2:
844                     raise osv.except_osv(_('Invalid context value'), _('Invalid context default_groups_ref value (model.name_id) : "%s"') % group_xml_id)
845                 try:
846                     temp, group_id = ir_model_data.get_object_reference(cr, uid, group_split[0], group_split[1])
847                 except ValueError:
848                     group_id = False
849                 groups += [group_id]
850             values['groups_id'] = groups
851         return values
852
853     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
854         # determine whether reified groups fields are required, and which ones
855         fields1 = fields or self.fields_get(cr, uid, context=context).keys()
856         group_fields, other_fields = partition(is_reified_group, fields1)
857
858         # read regular fields (other_fields); add 'groups_id' if necessary
859         drop_groups_id = False
860         if group_fields and fields:
861             if 'groups_id' not in other_fields:
862                 other_fields.append('groups_id')
863                 drop_groups_id = True
864         else:
865             other_fields = fields
866
867         res = super(users_view, self).read(cr, uid, ids, other_fields, context=context, load=load)
868
869         # post-process result to add reified group fields
870         if group_fields:
871             for values in (res if isinstance(res, list) else [res]):
872                 self._add_reified_groups(group_fields, values)
873                 if drop_groups_id:
874                     values.pop('groups_id', None)
875         return res
876
877     def _add_reified_groups(self, fields, values):
878         """ add the given reified group fields into `values` """
879         gids = set(parse_m2m(values.get('groups_id') or []))
880         for f in fields:
881             if is_boolean_group(f):
882                 values[f] = get_boolean_group(f) in gids
883             elif is_selection_groups(f):
884                 selected = [gid for gid in get_selection_groups(f) if gid in gids]
885                 values[f] = selected and selected[-1] or False
886
887     def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
888         res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
889         # add reified groups fields
890         for app, kind, gs in self.pool['res.groups'].get_groups_by_application(cr, uid, context):
891             if kind == 'selection':
892                 # selection group field
893                 tips = ['%s: %s' % (g.name, g.comment) for g in gs if g.comment]
894                 res[name_selection_groups(map(int, gs))] = {
895                     'type': 'selection',
896                     'string': app and app.name or _('Other'),
897                     'selection': [(False, '')] + [(g.id, g.name) for g in gs],
898                     'help': '\n'.join(tips),
899                     'exportable': False,
900                     'selectable': False,
901                 }
902             else:
903                 # boolean group fields
904                 for g in gs:
905                     res[name_boolean_group(g.id)] = {
906                         'type': 'boolean',
907                         'string': g.name,
908                         'help': g.comment,
909                         'exportable': False,
910                         'selectable': False,
911                     }
912         return res
913
914 #----------------------------------------------------------
915 # change password wizard
916 #----------------------------------------------------------
917
918 class change_password_wizard(osv.TransientModel):
919     """
920         A wizard to manage the change of users' passwords
921     """
922
923     _name = "change.password.wizard"
924     _description = "Change Password Wizard"
925     _columns = {
926         'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'),
927     }
928
929     def _default_user_ids(self, cr, uid, context=None):
930         if context is None:
931             context = {}
932         user_model = self.pool['res.users']
933         user_ids = context.get('active_model') == 'res.users' and context.get('active_ids') or []
934         return [
935             (0, 0, {'user_id': user.id, 'user_login': user.login})
936             for user in user_model.browse(cr, uid, user_ids, context=context)
937         ]
938
939     _defaults = {
940         'user_ids': _default_user_ids,
941     }
942
943     def change_password_button(self, cr, uid, ids, context=None):
944         wizard = self.browse(cr, uid, ids, context=context)[0]
945         need_reload = any(uid == user.user_id.id for user in wizard.user_ids)
946
947         line_ids = [user.id for user in wizard.user_ids]
948         self.pool.get('change.password.user').change_password_button(cr, uid, line_ids, context=context)
949
950         if need_reload:
951             return {
952                 'type': 'ir.actions.client',
953                 'tag': 'reload'
954             }
955
956         return {'type': 'ir.actions.act_window_close'}
957
958 class change_password_user(osv.TransientModel):
959     """
960         A model to configure users in the change password wizard
961     """
962
963     _name = 'change.password.user'
964     _description = 'Change Password Wizard User'
965     _columns = {
966         'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True),
967         'user_id': fields.many2one('res.users', string='User', required=True),
968         'user_login': fields.char('User Login', readonly=True),
969         'new_passwd': fields.char('New Password'),
970     }
971     _defaults = {
972         'new_passwd': '',
973     }
974
975     def change_password_button(self, cr, uid, ids, context=None):
976         for line in self.browse(cr, uid, ids, context=context):
977             line.user_id.write({'password': line.new_passwd})
978         # don't keep temporary passwords in the database longer than necessary
979         self.write(cr, uid, ids, {'new_passwd': False}, context=context)
980
981
982 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: