1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>)
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
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
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/>
20 ##############################################################################
21 from datetime import datetime, timedelta
23 from urllib import urlencode
24 from urlparse import urljoin
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 _
31 class SignupError(Exception):
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))
40 dt = datetime.now() + timedelta(**kwargs)
41 return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
44 class res_partner(osv.Model):
45 _inherit = 'res.partner'
47 def _get_signup_valid(self, cr, uid, ids, name, arg, context=None):
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)
55 def _get_signup_url_for_action(self, cr, uid, ids, action='login', view_type=None, menu_id=None, res_id=None, model=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)
66 # the parameters to encode for the query and fragment part of url
67 query = {'db': cr.dbname}
68 fragment = {'action': action, 'type': partner.signup_type}
70 if partner.signup_token:
71 fragment['token'] = partner.signup_token
72 elif partner.user_ids:
73 fragment['db'] = cr.dbname
74 fragment['login'] = partner.user_ids[0].login
76 continue # no signup token, no user, thus no signup url!
79 fragment['view_type'] = view_type
81 fragment['menu_id'] = menu_id
83 fragment['model'] = model
85 fragment['id'] = res_id
87 res[partner.id] = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
91 def _get_signup_url(self, cr, uid, ids, name, arg, context=None):
92 """ proxy for function field towards actual implementation """
93 return self._get_signup_url_for_action(cr, uid, ids, context=context)
96 'signup_token': fields.char('Signup Token'),
97 'signup_type': fields.char('Signup Token Type'),
98 'signup_expiration': fields.datetime('Signup Expiration'),
99 'signup_valid': fields.function(_get_signup_valid, type='boolean', string='Signup Token is Valid'),
100 'signup_url': fields.function(_get_signup_url, type='char', string='Signup URL'),
103 def action_signup_prepare(self, cr, uid, ids, context=None):
104 return self.signup_prepare(cr, uid, ids, context=context)
106 def signup_prepare(self, cr, uid, ids, signup_type="signup", expiration=False, context=None):
107 """ generate a new token for the partners with the given validity, if necessary
108 :param expiration: the expiration datetime of the token (string, optional)
110 for partner in self.browse(cr, uid, ids, context):
111 if expiration or not partner.signup_valid:
112 token = random_token()
113 while self._signup_retrieve_partner(cr, uid, token, context=context):
114 token = random_token()
115 partner.write({'signup_token': token, 'signup_type': signup_type, 'signup_expiration': expiration})
118 def _signup_retrieve_partner(self, cr, uid, token,
119 check_validity=False, raise_exception=False, context=None):
120 """ find the partner corresponding to a token, and possibly check its validity
121 :param token: the token to resolve
122 :param check_validity: if True, also check validity
123 :param raise_exception: if True, raise exception instead of returning False
124 :return: partner (browse record) or False (if raise_exception is False)
126 partner_ids = self.search(cr, uid, [('signup_token', '=', token)], context=context)
129 raise SignupError("Signup token '%s' is not valid" % token)
131 partner = self.browse(cr, uid, partner_ids[0], context)
132 if check_validity and not partner.signup_valid:
134 raise SignupError("Signup token '%s' is no longer valid" % token)
138 def signup_retrieve_info(self, cr, uid, token, context=None):
139 """ retrieve the user info about the token
140 :return: a dictionary with the user information:
141 - 'db': the name of the database
142 - 'token': the token, if token is valid
143 - 'name': the name of the partner, if token is valid
144 - 'login': the user login, if the user already exists
145 - 'email': the partner email, if the user does not exist
147 partner = self._signup_retrieve_partner(cr, uid, token, raise_exception=True, context=None)
148 res = {'db': cr.dbname}
149 if partner.signup_valid:
151 res['name'] = partner.name
153 res['login'] = partner.user_ids[0].login
155 res['email'] = partner.email or ''
158 class res_users(osv.Model):
159 _inherit = 'res.users'
161 def _get_state(self, cr, uid, ids, name, arg, context=None):
163 for user in self.browse(cr, uid, ids, context):
164 res[user.id] = ('reset' if user.signup_valid else
165 'active' if user.login_date else
170 'state': fields.function(_get_state, string='Status', type='selection',
171 selection=[('new', 'New'), ('active', 'Active'), ('reset', 'Resetting Password')]),
174 def signup(self, cr, uid, values, token=None, context=None):
175 """ signup a user, to either:
176 - create a new user (no token), or
177 - create a user for a partner (with token, but no user for partner), or
178 - change the password of a user (with token, and existing user).
179 :param values: a dictionary with field values that are written on user
180 :param token: signup token (optional)
181 :return: (dbname, login, password) for the signed up user
184 # signup with a token: find the corresponding partner id
185 res_partner = self.pool.get('res.partner')
186 partner = res_partner._signup_retrieve_partner(
187 cr, uid, token, check_validity=True, raise_exception=True, context=None)
188 # invalidate signup token
189 partner.write({'signup_token': False, 'signup_type': False, 'signup_expiration': False})
191 partner_user = partner.user_ids and partner.user_ids[0] or False
193 # user exists, modify it according to values
194 values.pop('login', None)
195 values.pop('name', None)
196 partner_user.write(values)
197 return (cr.dbname, partner_user.login, values.get('password'))
199 # user does not exist: sign up invited user
201 'name': partner.name,
202 'partner_id': partner.id,
203 'email': values.get('email') or values.get('login'),
205 self._signup_create_user(cr, uid, values, context=context)
207 # no token, sign up an external user
208 values['email'] = values.get('email') or values.get('login')
209 self._signup_create_user(cr, uid, values, context=context)
211 return (cr.dbname, values.get('login'), values.get('password'))
213 def _signup_create_user(self, cr, uid, values, context=None):
214 """ create a new user from the template user """
215 ir_config_parameter = self.pool.get('ir.config_parameter')
216 template_user_id = safe_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.template_user_id', 'False'))
217 assert template_user_id and self.exists(cr, uid, template_user_id, context=context), 'Signup: invalid template user'
219 # check that uninvited users may sign up
220 if 'partner_id' not in values:
221 if not safe_eval(ir_config_parameter.get_param(cr, uid, 'auth_signup.allow_uninvited', 'False')):
222 raise SignupError('Signup is not allowed for uninvited users')
224 assert values.get('login'), "Signup: no login given for new user"
225 assert values.get('partner_id') or values.get('name'), "Signup: no name or partner given for new user"
227 # create a copy of the template user (attached to a specific partner_id if given)
228 values['active'] = True
229 return self.copy(cr, uid, template_user_id, values, context=context)
231 def reset_password(self, cr, uid, login, context=None):
232 """ retrieve the user corresponding to login (login or email),
233 and reset their password
235 user_ids = self.search(cr, uid, [('login', '=', login)], context=context)
237 user_ids = self.search(cr, uid, [('email', '=', login)], context=context)
238 if len(user_ids) != 1:
239 raise Exception('Reset password: invalid username or email')
240 return self.action_reset_password(cr, uid, user_ids, context=context)
242 def action_reset_password(self, cr, uid, ids, context=None):
243 """ create signup token for each user, and send their signup url by email """
244 # prepare reset password signup
245 res_partner = self.pool.get('res.partner')
246 partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context)]
247 res_partner.signup_prepare(cr, uid, partner_ids, signup_type="reset", expiration=now(days=+1), context=context)
252 # send email to users with their signup url
254 if context.get('create_user'):
256 template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'set_password_email')
259 if not bool(template):
260 template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'reset_password_email')
261 mail_obj = self.pool.get('mail.mail')
262 assert template._name == 'email.template'
264 for user in self.browse(cr, uid, ids, context):
266 raise osv.except_osv(_("Cannot send email: user has no email address."), user.name)
267 mail_id = self.pool.get('email.template').send_mail(cr, uid, template.id, user.id, True, context=context)
268 mail_state = mail_obj.read(cr, uid, mail_id, ['state'], context=context)
270 if mail_state and mail_state['state'] == 'exception':
271 raise self.pool.get('res.config.settings').get_config_warning(cr, _("Cannot send email: no outgoing email server configured.\nYou can configure it under %(menu:base_setup.menu_general_configuration)s."), context)
274 'type': 'ir.actions.client',
275 'name': '_(Server Notification)',
276 'tag': 'action_notify',
278 'title': 'Mail Sent to: %s' % user.name,
279 'text': 'You can reset the password by yourself using this <a href=%s>link</a>' % user.partner_id.signup_url,
284 def create(self, cr, uid, values, context=None):
285 # overridden to automatically invite user to sign up
286 user_id = super(res_users, self).create(cr, uid, values, context=context)
287 user = self.browse(cr, uid, user_id, context=context)
288 if context and context.get('reset_password') and user.email:
289 ctx = dict(context, create_user=True)
290 self.action_reset_password(cr, uid, [user.id], context=ctx)