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