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