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