[MERGE] auth password wizard
[odoo/odoo.git] / addons / auth_signup / res_users.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>
19 #
20 ##############################################################################
21 from datetime import datetime, timedelta
22 import random
23 from urllib import urlencode
24 from urlparse import urljoin
25
26 from openerp.osv import osv, fields
27 from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
28 from openerp.tools.safe_eval import safe_eval
29 from openerp.tools.translate import _
30
31 class SignupError(Exception):
32     pass
33
34 def random_token():
35     # the token has an entropy of about 120 bits (6 bits/char * 20 chars)
36     chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
37     return ''.join(random.choice(chars) for i in xrange(20))
38
39 def now(**kwargs):
40     dt = datetime.now() + timedelta(**kwargs)
41     return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
42
43
44 class res_partner(osv.Model):
45     _inherit = 'res.partner'
46
47     def _get_signup_valid(self, cr, uid, ids, name, arg, context=None):
48         dt = now()
49         res = {}
50         for partner in self.browse(cr, uid, ids, context):
51             res[partner.id] = bool(partner.signup_token) and \
52                                 (not partner.signup_expiration or dt <= partner.signup_expiration)
53         return res
54
55     def _get_signup_url_for_action(self, cr, uid, ids, action='login', view_type=None, menu_id=None, res_id=None, context=None):
56         """ generate a signup url for the given partner ids and action, possibly overriding
57             the url state components (menu_id, id, view_type) """
58         res = dict.fromkeys(ids, False)
59         base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
60         for partner in self.browse(cr, uid, ids, context):
61             # when required, make sure the partner has a valid signup token
62             if context and context.get('signup_valid') and not partner.user_ids:
63                 self.signup_prepare(cr, uid, [partner.id], context=context)
64
65             # the parameters to encode for the query and fragment part of url
66             query = {'db': cr.dbname}
67             fragment = {'action': action, 'type': partner.signup_type}
68
69             if partner.signup_token:
70                 fragment['token'] = partner.signup_token
71             elif partner.user_ids:
72                 fragment['db'] = cr.dbname
73                 fragment['login'] = partner.user_ids[0].login
74             else:
75                 continue        # no signup token, no user, thus no signup url!
76
77             if view_type:
78                 fragment['view_type'] = view_type
79             if menu_id:
80                 fragment['menu_id'] = menu_id
81             if res_id:
82                 fragment['id'] = res_id
83
84             res[partner.id] = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
85
86         return res
87
88     def _get_signup_url(self, cr, uid, ids, name, arg, context=None):
89         """ proxy for function field towards actual implementation """
90         return self._get_signup_url_for_action(cr, uid, ids, context=context)
91
92     _columns = {
93         'signup_token': fields.char('Signup Token'),
94         'signup_type': fields.char('Signup Token Type'),
95         'signup_expiration': fields.datetime('Signup Expiration'),
96         'signup_valid': fields.function(_get_signup_valid, type='boolean', string='Signup Token is Valid'),
97         'signup_url': fields.function(_get_signup_url, type='char', string='Signup URL'),
98     }
99
100     def action_signup_prepare(self, cr, uid, ids, context=None):
101         return self.signup_prepare(cr, uid, ids, context=context)
102
103     def signup_prepare(self, cr, uid, ids, signup_type="signup", expiration=False, context=None):
104         """ generate a new token for the partners with the given validity, if necessary
105             :param expiration: the expiration datetime of the token (string, optional)
106         """
107         for partner in self.browse(cr, uid, ids, context):
108             if expiration or not partner.signup_valid:
109                 token = random_token()
110                 while self._signup_retrieve_partner(cr, uid, token, context=context):
111                     token = random_token()
112                 partner.write({'signup_token': token, 'signup_type': signup_type, 'signup_expiration': expiration})
113         return True
114
115     def _signup_retrieve_partner(self, cr, uid, token,
116             check_validity=False, raise_exception=False, context=None):
117         """ find the partner corresponding to a token, and possibly check its validity
118             :param token: the token to resolve
119             :param check_validity: if True, also check validity
120             :param raise_exception: if True, raise exception instead of returning False
121             :return: partner (browse record) or False (if raise_exception is False)
122         """
123         partner_ids = self.search(cr, uid, [('signup_token', '=', token)], context=context)
124         if not partner_ids:
125             if raise_exception:
126                 raise SignupError("Signup token '%s' is not valid" % token)
127             return False
128         partner = self.browse(cr, uid, partner_ids[0], context)
129         if check_validity and not partner.signup_valid:
130             if raise_exception:
131                 raise SignupError("Signup token '%s' is no longer valid" % token)
132             return False
133         return partner
134
135     def signup_retrieve_info(self, cr, uid, token, context=None):
136         """ retrieve the user info about the token
137             :return: a dictionary with the user information:
138                 - 'db': the name of the database
139                 - 'token': the token, if token is valid
140                 - 'name': the name of the partner, if token is valid
141                 - 'login': the user login, if the user already exists
142                 - 'email': the partner email, if the user does not exist
143         """
144         partner = self._signup_retrieve_partner(cr, uid, token, raise_exception=True, context=None)
145         res = {'db': cr.dbname}
146         if partner.signup_valid:
147             res['token'] = token
148             res['name'] = partner.name
149         if partner.user_ids:
150             res['login'] = partner.user_ids[0].login
151         else:
152             res['email'] = partner.email or ''
153         return res
154
155 class res_users(osv.Model):
156     _inherit = 'res.users'
157
158     def _get_state(self, cr, uid, ids, name, arg, context=None):
159         res = {}
160         for user in self.browse(cr, uid, ids, context):
161             res[user.id] = ('reset' if user.signup_valid else
162                             'active' if user.login_date else
163                             'new')
164         return res
165
166     _columns = {
167         'state': fields.function(_get_state, string='Status', type='selection',
168                     selection=[('new', 'New'), ('active', 'Active'), ('reset', 'Resetting Password')]),
169     }
170
171     def signup(self, cr, uid, values, token=None, context=None):
172         """ signup a user, to either:
173             - create a new user (no token), or
174             - create a user for a partner (with token, but no user for partner), or
175             - change the password of a user (with token, and existing user).
176             :param values: a dictionary with field values that are written on user
177             :param token: signup token (optional)
178             :return: (dbname, login, password) for the signed up user
179         """
180         if token:
181             # signup with a token: find the corresponding partner id
182             res_partner = self.pool.get('res.partner')
183             partner = res_partner._signup_retrieve_partner(
184                             cr, uid, token, check_validity=True, raise_exception=True, context=None)
185             # invalidate signup token
186             partner.write({'signup_token': False, 'signup_type': False, 'signup_expiration': False})
187
188             partner_user = partner.user_ids and partner.user_ids[0] or False
189             if partner_user:
190                 # user exists, modify it according to values
191                 values.pop('login', None)
192                 values.pop('name', None)
193                 partner_user.write(values)
194                 return (cr.dbname, partner_user.login, values.get('password'))
195             else:
196                 # user does not exist: sign up invited user
197                 values.update({
198                     'name': partner.name,
199                     'partner_id': partner.id,
200                     'email': values.get('email') or values.get('login'),
201                 })
202                 self._signup_create_user(cr, uid, values, context=context)
203         else:
204             # no token, sign up an external user
205             values['email'] = values.get('email') or values.get('login')
206             self._signup_create_user(cr, uid, values, context=context)
207
208         return (cr.dbname, values.get('login'), values.get('password'))
209
210     def _signup_create_user(self, cr, uid, values, context=None):
211         """ create a new user from the template user """
212         ir_config_parameter = self.pool.get('ir.config_parameter')
213         template_user_id = safe_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.template_user_id', 'False'))
214         assert template_user_id and self.exists(cr, uid, template_user_id, context=context), 'Signup: invalid template user'
215
216         # check that uninvited users may sign up
217         if 'partner_id' not in values:
218             if not safe_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.allow_uninvited', 'False')):
219                 raise SignupError('Signup is not allowed for uninvited users')
220
221         assert values.get('login'), "Signup: no login given for new user"
222         assert values.get('partner_id') or values.get('name'), "Signup: no name or partner given for new user"
223
224         # create a copy of the template user (attached to a specific partner_id if given)
225         values['active'] = True
226         return self.copy(cr, uid, template_user_id, values, context=context)
227
228     def reset_password(self, cr, uid, login, context=None):
229         """ retrieve the user corresponding to login (login or email),
230             and reset their password
231         """
232         user_ids = self.search(cr, uid, [('login', '=', login)], context=context)
233         if not user_ids:
234             user_ids = self.search(cr, uid, [('email', '=', login)], context=context)
235         if len(user_ids) != 1:
236             raise Exception('Reset password: invalid username or email')
237         return self.action_reset_password(cr, uid, user_ids, context=context)
238
239     def action_reset_password(self, cr, uid, ids, context=None):
240         """ create signup token for each user, and send their signup url by email """
241         # prepare reset password signup
242         res_partner = self.pool.get('res.partner')
243         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context)]
244         res_partner.signup_prepare(cr, uid, partner_ids, signup_type="reset", expiration=now(days=+1), context=context)
245
246         # send email to users with their signup url
247         template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'reset_password_email')
248         mail_obj = self.pool.get('mail.mail')
249         assert template._name == 'email.template'
250         for user in self.browse(cr, uid, ids, context):
251             if not user.email:
252                 raise osv.except_osv(_("Cannot send email: user has no email address."), user.name)
253             mail_id = self.pool.get('email.template').send_mail(cr, uid, template.id, user.id, True, context=context)
254             mail_state = mail_obj.read(cr, uid, mail_id, ['state'], context=context)
255             if mail_state and mail_state == 'exception':
256                 raise osv.except_osv(_("Cannot send email: no outgoing email server configured.\nYou can configure it under Settings/General Settings."), user.name)
257             else:
258                 raise osv.except_osv(_("Mail sent to:"), user.email)
259
260         return True
261
262     def create(self, cr, uid, values, context=None):
263         # overridden to automatically invite user to sign up
264         user_id = super(res_users, self).create(cr, uid, values, context=context)
265         user = self.browse(cr, uid, user_id, context=context)
266         if context and context.get('reset_password') and user.email:
267             user.action_reset_password()
268         return user_id