[FIX] auth_signup: refresh is no longer needed with the new api
[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 urlparse import urljoin
24 import werkzeug
25
26 from openerp.addons.base.ir.ir_mail_server import MailDeliveryException
27 from openerp.osv import osv, fields
28 from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT, ustr
29 from ast import literal_eval
30 from openerp.tools.translate import _
31
32 class SignupError(Exception):
33     pass
34
35 def random_token():
36     # the token has an entropy of about 120 bits (6 bits/char * 20 chars)
37     chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
38     return ''.join(random.choice(chars) for i in xrange(20))
39
40 def now(**kwargs):
41     dt = datetime.now() + timedelta(**kwargs)
42     return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
43
44
45 class res_partner(osv.Model):
46     _inherit = 'res.partner'
47
48     def _get_signup_valid(self, cr, uid, ids, name, arg, context=None):
49         dt = now()
50         res = {}
51         for partner in self.browse(cr, uid, ids, context):
52             res[partner.id] = bool(partner.signup_token) and \
53                                 (not partner.signup_expiration or dt <= partner.signup_expiration)
54         return res
55
56     def _get_signup_url_for_action(self, cr, uid, ids, action=None, view_type=None, menu_id=None, res_id=None, model=None, context=None):
57         """ generate a signup url for the given partner ids and action, possibly overriding
58             the url state components (menu_id, id, view_type) """
59         if context is None:
60             context= {}
61         res = dict.fromkeys(ids, False)
62         base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
63         for partner in self.browse(cr, uid, ids, context):
64             # when required, make sure the partner has a valid signup token
65             if context.get('signup_valid') and not partner.user_ids:
66                 self.signup_prepare(cr, uid, [partner.id], context=context)
67
68             route = 'login'
69             # the parameters to encode for the query
70             query = dict(db=cr.dbname)
71             signup_type = context.get('signup_force_type_in_url', partner.signup_type or '')
72             if signup_type:
73                 route = 'reset_password' if signup_type == 'reset' else signup_type
74
75             if partner.signup_token and signup_type:
76                 query['token'] = partner.signup_token
77             elif partner.user_ids:
78                 query['login'] = partner.user_ids[0].login
79             else:
80                 continue        # no signup token, no user, thus no signup url!
81
82             fragment = dict()
83             if action:
84                 fragment['action'] = action
85             if view_type:
86                 fragment['view_type'] = view_type
87             if menu_id:
88                 fragment['menu_id'] = menu_id
89             if model:
90                 fragment['model'] = model
91             if res_id:
92                 fragment['id'] = res_id
93
94             if fragment:
95                 query['redirect'] = '/web#' + werkzeug.url_encode(fragment)
96
97             res[partner.id] = urljoin(base_url, "/web/%s?%s" % (route, werkzeug.url_encode(query)))
98
99         return res
100
101     def _get_signup_url(self, cr, uid, ids, name, arg, context=None):
102         """ proxy for function field towards actual implementation """
103         return self._get_signup_url_for_action(cr, uid, ids, context=context)
104
105     _columns = {
106         'signup_token': fields.char('Signup Token', copy=False),
107         'signup_type': fields.char('Signup Token Type', copy=False),
108         'signup_expiration': fields.datetime('Signup Expiration', copy=False),
109         'signup_valid': fields.function(_get_signup_valid, type='boolean', string='Signup Token is Valid'),
110         'signup_url': fields.function(_get_signup_url, type='char', string='Signup URL'),
111     }
112
113     def action_signup_prepare(self, cr, uid, ids, context=None):
114         return self.signup_prepare(cr, uid, ids, context=context)
115
116     def signup_cancel(self, cr, uid, ids, context=None):
117         return self.write(cr, uid, ids, {'signup_token': False, 'signup_type': False, 'signup_expiration': False}, context=context)
118
119     def signup_prepare(self, cr, uid, ids, signup_type="signup", expiration=False, context=None):
120         """ generate a new token for the partners with the given validity, if necessary
121             :param expiration: the expiration datetime of the token (string, optional)
122         """
123         for partner in self.browse(cr, uid, ids, context):
124             if expiration or not partner.signup_valid:
125                 token = random_token()
126                 while self._signup_retrieve_partner(cr, uid, token, context=context):
127                     token = random_token()
128                 partner.write({'signup_token': token, 'signup_type': signup_type, 'signup_expiration': expiration})
129         return True
130
131     def _signup_retrieve_partner(self, cr, uid, token,
132             check_validity=False, raise_exception=False, context=None):
133         """ find the partner corresponding to a token, and possibly check its validity
134             :param token: the token to resolve
135             :param check_validity: if True, also check validity
136             :param raise_exception: if True, raise exception instead of returning False
137             :return: partner (browse record) or False (if raise_exception is False)
138         """
139         partner_ids = self.search(cr, uid, [('signup_token', '=', token)], context=context)
140         if not partner_ids:
141             if raise_exception:
142                 raise SignupError("Signup token '%s' is not valid" % token)
143             return False
144         partner = self.browse(cr, uid, partner_ids[0], context)
145         if check_validity and not partner.signup_valid:
146             if raise_exception:
147                 raise SignupError("Signup token '%s' is no longer valid" % token)
148             return False
149         return partner
150
151     def signup_retrieve_info(self, cr, uid, token, context=None):
152         """ retrieve the user info about the token
153             :return: a dictionary with the user information:
154                 - 'db': the name of the database
155                 - 'token': the token, if token is valid
156                 - 'name': the name of the partner, if token is valid
157                 - 'login': the user login, if the user already exists
158                 - 'email': the partner email, if the user does not exist
159         """
160         partner = self._signup_retrieve_partner(cr, uid, token, raise_exception=True, context=None)
161         res = {'db': cr.dbname}
162         if partner.signup_valid:
163             res['token'] = token
164             res['name'] = partner.name
165         if partner.user_ids:
166             res['login'] = partner.user_ids[0].login
167         else:
168             res['email'] = partner.email or ''
169         return res
170
171 class res_users(osv.Model):
172     _inherit = 'res.users'
173
174     def _get_state(self, cr, uid, ids, name, arg, context=None):
175         res = {}
176         for user in self.browse(cr, uid, ids, context):
177             res[user.id] = ('active' if user.login_date else 'new')
178         return res
179
180     _columns = {
181         'state': fields.function(_get_state, string='Status', type='selection',
182                     selection=[('new', 'Never Connected'), ('active', 'Activated')]),
183     }
184
185     def signup(self, cr, uid, values, token=None, context=None):
186         """ signup a user, to either:
187             - create a new user (no token), or
188             - create a user for a partner (with token, but no user for partner), or
189             - change the password of a user (with token, and existing user).
190             :param values: a dictionary with field values that are written on user
191             :param token: signup token (optional)
192             :return: (dbname, login, password) for the signed up user
193         """
194         if token:
195             # signup with a token: find the corresponding partner id
196             res_partner = self.pool.get('res.partner')
197             partner = res_partner._signup_retrieve_partner(
198                             cr, uid, token, check_validity=True, raise_exception=True, context=None)
199             # invalidate signup token
200             partner.write({'signup_token': False, 'signup_type': False, 'signup_expiration': False})
201
202             partner_user = partner.user_ids and partner.user_ids[0] or False
203
204             # avoid overwriting existing (presumably correct) values with geolocation data
205             if partner.country_id or partner.zip or partner.city:
206                 values.pop('city', None)
207                 values.pop('country_id', None)
208             if partner.lang:
209                 values.pop('lang', None)
210
211             if partner_user:
212                 # user exists, modify it according to values
213                 values.pop('login', None)
214                 values.pop('name', None)
215                 partner_user.write(values)
216                 return (cr.dbname, partner_user.login, values.get('password'))
217             else:
218                 # user does not exist: sign up invited user
219                 values.update({
220                     'name': partner.name,
221                     'partner_id': partner.id,
222                     'email': values.get('email') or values.get('login'),
223                 })
224                 if partner.company_id:
225                     values['company_id'] = partner.company_id.id
226                     values['company_ids'] = [(6, 0, [partner.company_id.id])]
227                 self._signup_create_user(cr, uid, values, context=context)
228         else:
229             # no token, sign up an external user
230             values['email'] = values.get('email') or values.get('login')
231             self._signup_create_user(cr, uid, values, context=context)
232
233         return (cr.dbname, values.get('login'), values.get('password'))
234
235     def _signup_create_user(self, cr, uid, values, context=None):
236         """ create a new user from the template user """
237         ir_config_parameter = self.pool.get('ir.config_parameter')
238         template_user_id = literal_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.template_user_id', 'False'))
239         assert template_user_id and self.exists(cr, uid, template_user_id, context=context), 'Signup: invalid template user'
240
241         # check that uninvited users may sign up
242         if 'partner_id' not in values:
243             if not literal_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.allow_uninvited', 'False')):
244                 raise SignupError('Signup is not allowed for uninvited users')
245
246         assert values.get('login'), "Signup: no login given for new user"
247         assert values.get('partner_id') or values.get('name'), "Signup: no name or partner given for new user"
248
249         # create a copy of the template user (attached to a specific partner_id if given)
250         values['active'] = True
251         context = dict(context or {}, no_reset_password=True)
252         try:
253             with cr.savepoint():
254                 return self.copy(cr, uid, template_user_id, values, context=context)
255         except Exception, e:
256             # copy may failed if asked login is not available.
257             raise SignupError(ustr(e))
258
259     def reset_password(self, cr, uid, login, context=None):
260         """ retrieve the user corresponding to login (login or email),
261             and reset their password
262         """
263         user_ids = self.search(cr, uid, [('login', '=', login)], context=context)
264         if not user_ids:
265             user_ids = self.search(cr, uid, [('email', '=', login)], context=context)
266         if len(user_ids) != 1:
267             raise Exception('Reset password: invalid username or email')
268         return self.action_reset_password(cr, uid, user_ids, context=context)
269
270     def action_reset_password(self, cr, uid, ids, context=None):
271         """ create signup token for each user, and send their signup url by email """
272         # prepare reset password signup
273         res_partner = self.pool.get('res.partner')
274         partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context)]
275         res_partner.signup_prepare(cr, uid, partner_ids, signup_type="reset", expiration=now(days=+1), context=context)
276
277         if not context:
278             context = {}
279
280         # send email to users with their signup url
281         template = False
282         if context.get('create_user'):
283             try:
284                 # get_object() raises ValueError if record does not exist
285                 template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'set_password_email')
286             except ValueError:
287                 pass
288         if not bool(template):
289             template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'reset_password_email')
290         assert template._name == 'email.template'
291
292         for user in self.browse(cr, uid, ids, context):
293             if not user.email:
294                 raise osv.except_osv(_("Cannot send email: user has no email address."), user.name)
295             self.pool.get('email.template').send_mail(cr, uid, template.id, user.id, force_send=True, raise_exception=True, context=context)
296
297     def create(self, cr, uid, values, context=None):
298         if context is None:
299             context = {}
300         # overridden to automatically invite user to sign up
301         user_id = super(res_users, self).create(cr, uid, values, context=context)
302         user = self.browse(cr, uid, user_id, context=context)
303         if user.email and not context.get('no_reset_password'):
304             context = dict(context, create_user=True)
305             try:
306                 self.action_reset_password(cr, uid, [user.id], context=context)
307             except MailDeliveryException:
308                 self.pool.get('res.partner').signup_cancel(cr, uid, [user.partner_id.id], context=context)
309         return user_id