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