Fixed #7723 - implemented a secure password reset form that uses a token and prompts user for new password.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@8162 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
9a56fe765e
commit
fcd837cd0f
|
@ -366,6 +366,9 @@ LOGOUT_URL = '/accounts/logout/'
|
|||
|
||||
LOGIN_REDIRECT_URL = '/accounts/profile/'
|
||||
|
||||
# The number of days a password reset link is valid for
|
||||
PASSWORD_RESET_TIMEOUT_DAYS = 3
|
||||
|
||||
###########
|
||||
# TESTING #
|
||||
###########
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> › {% trans 'Password reset' %}</div>{% endblock %}
|
||||
|
||||
{% block title %}{% trans 'Password reset complete' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{% trans 'Password reset complete' %}</h1>
|
||||
|
||||
<p>{% trans "Your password has been set. You may go ahead and log in now." %}</p>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,32 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> › {% trans 'Password reset confirmation' %}</div>{% endblock %}
|
||||
|
||||
{% block title %}{% trans 'Password reset' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if validlink %}
|
||||
|
||||
<h1>{% trans 'Enter new password' %}</h1>
|
||||
|
||||
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
|
||||
|
||||
<form action="" method="post">
|
||||
{% if form.new_password1.errors %}{{ form.new_password1.errors }}{% endif %}
|
||||
<p class="aligned wide"><label for="id_new_password1">{% trans 'New password:' %}</label>{{ form.new_password1 }}</p>
|
||||
{% if form.new_password2.errors %}{{ form.new_password2.errors }}{% endif %}
|
||||
<p class="aligned wide"><label for="id_new_password2">{% trans 'Confirm password:' %}</label>{{ form.new_password2 }}</p>
|
||||
<p><input type="submit" value="{% trans 'Change my password' %}" /></p>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
<h1>{% trans 'Password reset unsuccessful' %}</h1>
|
||||
|
||||
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -9,6 +9,6 @@
|
|||
|
||||
<h1>{% trans 'Password reset successful' %}</h1>
|
||||
|
||||
<p>{% trans "We've e-mailed a new password to the e-mail address you submitted. You should be receiving it shortly." %}</p>
|
||||
<p>{% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}</p>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
{% load i18n %}
|
||||
{% load i18n %}{% autoescape off %}
|
||||
{% trans "You're receiving this e-mail because you requested a password reset" %}
|
||||
{% blocktrans %}for your user account at {{ site_name }}{% endblocktrans %}.
|
||||
|
||||
{% blocktrans %}Your new password is: {{ new_password }}{% endblocktrans %}
|
||||
|
||||
{% trans "Feel free to change this password by going to this page:" %}
|
||||
|
||||
http://{{ domain }}/password_change/
|
||||
|
||||
{% trans "Please go to the following page and choose a new password:" %}
|
||||
{% block reset_link %}
|
||||
{{ protocol }}://{{ domain }}/reset/{{ uid }}-{{ token }}/
|
||||
{% endblock %}
|
||||
{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
|
||||
|
||||
{% trans "Thanks for using our site!" %}
|
||||
|
||||
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||
|
||||
{% endautoescape %}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<h1>{% trans "Password reset" %}</h1>
|
||||
|
||||
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll reset your password and e-mail the new one to you." %}</p>
|
||||
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}</p>
|
||||
|
||||
<form action="" method="post">
|
||||
{% if form.email.errors %}{{ form.email.errors }}{% endif %}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.sites.models import Site
|
||||
from django.template import Context, loader
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.http import int_to_base36
|
||||
|
||||
class UserCreationForm(forms.ModelForm):
|
||||
"""
|
||||
|
@ -97,16 +99,14 @@ class PasswordResetForm(forms.Form):
|
|||
self.users_cache = User.objects.filter(email__iexact=email)
|
||||
if len(self.users_cache) == 0:
|
||||
raise forms.ValidationError(_("That e-mail address doesn't have an associated user account. Are you sure you've registered?"))
|
||||
|
||||
def save(self, domain_override=None, email_template_name='registration/password_reset_email.html'):
|
||||
|
||||
def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
|
||||
use_https=False, token_generator=default_token_generator):
|
||||
"""
|
||||
Calculates a new password randomly and sends it to the user.
|
||||
Generates a one-use only link for restting password and sends to the user
|
||||
"""
|
||||
from django.core.mail import send_mail
|
||||
for user in self.users_cache:
|
||||
new_pass = User.objects.make_random_password()
|
||||
user.set_password(new_pass)
|
||||
user.save()
|
||||
if not domain_override:
|
||||
current_site = Site.objects.get_current()
|
||||
site_name = current_site.name
|
||||
|
@ -115,26 +115,49 @@ class PasswordResetForm(forms.Form):
|
|||
site_name = domain = domain_override
|
||||
t = loader.get_template(email_template_name)
|
||||
c = {
|
||||
'new_password': new_pass,
|
||||
'email': user.email,
|
||||
'domain': domain,
|
||||
'site_name': site_name,
|
||||
'uid': int_to_base36(user.id),
|
||||
'user': user,
|
||||
'token': token_generator.make_token(user),
|
||||
'protocol': use_https and 'https' or 'http',
|
||||
}
|
||||
send_mail(_("Password reset on %s") % site_name,
|
||||
t.render(Context(c)), None, [user.email])
|
||||
|
||||
class PasswordChangeForm(forms.Form):
|
||||
class SetPasswordForm(forms.Form):
|
||||
"""
|
||||
A form that lets a user change his/her password.
|
||||
A form that lets a user change set his/her password without
|
||||
entering the old password
|
||||
"""
|
||||
old_password = forms.CharField(label=_("Old password"), max_length=30, widget=forms.PasswordInput)
|
||||
new_password1 = forms.CharField(label=_("New password"), max_length=30, widget=forms.PasswordInput)
|
||||
new_password2 = forms.CharField(label=_("New password confirmation"), max_length=30, widget=forms.PasswordInput)
|
||||
|
||||
new_password1 = forms.CharField(label=_("New password"), max_length=60, widget=forms.PasswordInput)
|
||||
new_password2 = forms.CharField(label=_("New password confirmation"), max_length=60, widget=forms.PasswordInput)
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
super(PasswordChangeForm, self).__init__(*args, **kwargs)
|
||||
super(SetPasswordForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_new_password2(self):
|
||||
password1 = self.cleaned_data.get('new_password1')
|
||||
password2 = self.cleaned_data.get('new_password2')
|
||||
if password1 and password2:
|
||||
if password1 != password2:
|
||||
raise forms.ValidationError(_("The two password fields didn't match."))
|
||||
return password2
|
||||
|
||||
def save(self, commit=True):
|
||||
self.user.set_password(self.cleaned_data['new_password1'])
|
||||
if commit:
|
||||
self.user.save()
|
||||
return self.user
|
||||
|
||||
class PasswordChangeForm(SetPasswordForm):
|
||||
"""
|
||||
A form that lets a user change his/her password by entering
|
||||
their old password.
|
||||
"""
|
||||
old_password = forms.CharField(label=_("Old password"), max_length=60, widget=forms.PasswordInput)
|
||||
|
||||
def clean_old_password(self):
|
||||
"""
|
||||
|
@ -144,21 +167,8 @@ class PasswordChangeForm(forms.Form):
|
|||
if not self.user.check_password(old_password):
|
||||
raise forms.ValidationError(_("Your old password was entered incorrectly. Please enter it again."))
|
||||
return old_password
|
||||
PasswordChangeForm.base_fields.keyOrder = ['old_password', 'new_password1', 'new_password2']
|
||||
|
||||
def clean_new_password2(self):
|
||||
password1 = self.cleaned_data.get('new_password1')
|
||||
password2 = self.cleaned_data.get('new_password2')
|
||||
if password1 and password2:
|
||||
if password1 != password2:
|
||||
raise forms.ValidationError(_("The two password fields didn't match."))
|
||||
return password2
|
||||
|
||||
def save(self, commit=True):
|
||||
self.user.set_password(self.cleaned_data['new_password1'])
|
||||
if commit:
|
||||
self.user.save()
|
||||
return self.user
|
||||
|
||||
class AdminPasswordChangeForm(forms.Form):
|
||||
"""
|
||||
A form used to change the password of a user in the admin interface.
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from django.contrib.auth.tests.basic import BASIC_TESTS, PasswordResetTest
|
||||
from django.contrib.auth.tests.basic import BASIC_TESTS
|
||||
from django.contrib.auth.tests.views import PasswordResetTest
|
||||
from django.contrib.auth.tests.forms import FORM_TESTS
|
||||
from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
|
||||
|
||||
__test__ = {
|
||||
'BASIC_TESTS': BASIC_TESTS,
|
||||
'PASSWORDRESET_TESTS': PasswordResetTest,
|
||||
'FORM_TESTS': FORM_TESTS,
|
||||
'TOKEN_GENERATOR_TESTS': TOKEN_GENERATOR_TESTS
|
||||
}
|
||||
|
|
|
@ -54,24 +54,3 @@ u'joe@somewhere.org'
|
|||
>>> u.password
|
||||
u'!'
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core import mail
|
||||
|
||||
class PasswordResetTest(TestCase):
|
||||
fixtures = ['authtestdata.json']
|
||||
urls = 'django.contrib.auth.urls'
|
||||
|
||||
def test_email_not_found(self):
|
||||
"Error is raised if the provided email address isn't currently registered"
|
||||
response = self.client.get('/password_reset/')
|
||||
self.assertEquals(response.status_code, 200)
|
||||
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
|
||||
self.assertContains(response, "That e-mail address doesn't have an associated user account")
|
||||
self.assertEquals(len(mail.outbox), 0)
|
||||
|
||||
def test_email_found(self):
|
||||
"Email is sent if a valid email address is provided for password reset"
|
||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEquals(response.status_code, 302)
|
||||
self.assertEquals(len(mail.outbox), 1)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
FORM_TESTS = """
|
||||
>>> from django.contrib.auth.models import User
|
||||
>>> from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||
>>> from django.contrib.auth.forms import PasswordChangeForm
|
||||
>>> from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm
|
||||
|
||||
The user already exists.
|
||||
|
||||
|
@ -95,6 +95,32 @@ True
|
|||
>>> form.non_field_errors()
|
||||
[]
|
||||
|
||||
SetPasswordForm:
|
||||
|
||||
The two new passwords do not match.
|
||||
|
||||
>>> data = {
|
||||
... 'new_password1': 'abc123',
|
||||
... 'new_password2': 'abc',
|
||||
... }
|
||||
>>> form = SetPasswordForm(user, data)
|
||||
>>> form.is_valid()
|
||||
False
|
||||
>>> form["new_password2"].errors
|
||||
[u"The two password fields didn't match."]
|
||||
|
||||
The success case.
|
||||
|
||||
>>> data = {
|
||||
... 'new_password1': 'abc123',
|
||||
... 'new_password2': 'abc123',
|
||||
... }
|
||||
>>> form = SetPasswordForm(user, data)
|
||||
>>> form.is_valid()
|
||||
True
|
||||
|
||||
PasswordChangeForm:
|
||||
|
||||
The old password is incorrect.
|
||||
|
||||
>>> data = {
|
||||
|
@ -132,4 +158,9 @@ The success case.
|
|||
>>> form.is_valid()
|
||||
True
|
||||
|
||||
Regression test - check the order of fields:
|
||||
|
||||
>>> PasswordChangeForm(user, {}).fields.keys()
|
||||
['old_password', 'new_password1', 'new_password2']
|
||||
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
TOKEN_GENERATOR_TESTS = """
|
||||
>>> from django.contrib.auth.models import User, AnonymousUser
|
||||
>>> from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
>>> from django.conf import settings
|
||||
>>> u = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw')
|
||||
>>> p0 = PasswordResetTokenGenerator()
|
||||
>>> tk1 = p0.make_token(u)
|
||||
>>> p0.check_token(u, tk1)
|
||||
True
|
||||
|
||||
Tests to ensure we can use the token after n days, but no greater.
|
||||
Use a mocked version of PasswordResetTokenGenerator so we can change
|
||||
the value of 'today'
|
||||
|
||||
>>> class Mocked(PasswordResetTokenGenerator):
|
||||
... def __init__(self, today):
|
||||
... self._today_val = today
|
||||
... def _today(self):
|
||||
... return self._today_val
|
||||
|
||||
>>> from datetime import date, timedelta
|
||||
>>> p1 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS))
|
||||
>>> p1.check_token(u, tk1)
|
||||
True
|
||||
>>> p2 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1))
|
||||
>>> p2.check_token(u, tk1)
|
||||
False
|
||||
|
||||
"""
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
import re
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.core import mail
|
||||
|
||||
class PasswordResetTest(TestCase):
|
||||
fixtures = ['authtestdata.json']
|
||||
urls = 'django.contrib.auth.urls'
|
||||
|
||||
def test_email_not_found(self):
|
||||
"Error is raised if the provided email address isn't currently registered"
|
||||
response = self.client.get('/password_reset/')
|
||||
self.assertEquals(response.status_code, 200)
|
||||
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
|
||||
self.assertContains(response, "That e-mail address doesn't have an associated user account")
|
||||
self.assertEquals(len(mail.outbox), 0)
|
||||
|
||||
def test_email_found(self):
|
||||
"Email is sent if a valid email address is provided for password reset"
|
||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEquals(response.status_code, 302)
|
||||
self.assertEquals(len(mail.outbox), 1)
|
||||
self.assert_("http://" in mail.outbox[0].body)
|
||||
|
||||
def _test_confirm_start(self):
|
||||
# Start by creating the email
|
||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEquals(response.status_code, 302)
|
||||
self.assertEquals(len(mail.outbox), 1)
|
||||
return self._read_signup_email(mail.outbox[0])
|
||||
|
||||
def _read_signup_email(self, email):
|
||||
urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
|
||||
self.assert_(urlmatch is not None, "No URL found in sent email")
|
||||
return urlmatch.group(), urlmatch.groups()[0]
|
||||
|
||||
def test_confirm_valid(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.get(path)
|
||||
# redirect to a 'complete' page:
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assert_("Please enter your new password" in response.content)
|
||||
|
||||
def test_confirm_invalid(self):
|
||||
url, path = self._test_confirm_start()
|
||||
# Lets munge the token in the path, but keep the same length,
|
||||
# in case the URL conf will reject a different length
|
||||
path = path[:-5] + ("0"*4) + path[-1]
|
||||
|
||||
response = self.client.get(path)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assert_("The password reset link was invalid" in response.content)
|
||||
|
||||
def test_confirm_invalid_post(self):
|
||||
# Same as test_confirm_invalid, but trying
|
||||
# to do a POST instead.
|
||||
url, path = self._test_confirm_start()
|
||||
path = path[:-5] + ("0"*4) + path[-1]
|
||||
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword',
|
||||
'new_password2':' anewpassword'})
|
||||
# Check the password has not been changed
|
||||
u = User.objects.get(email='staffmember@example.com')
|
||||
self.assert_(not u.check_password("anewpassword"))
|
||||
|
||||
def test_confirm_complete(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword',
|
||||
'new_password2': 'anewpassword'})
|
||||
# It redirects us to a 'complete' page:
|
||||
self.assertEquals(response.status_code, 302)
|
||||
# Check the password has been changed
|
||||
u = User.objects.get(email='staffmember@example.com')
|
||||
self.assert_(u.check_password("anewpassword"))
|
||||
|
||||
# Check we can't use the link again
|
||||
response = self.client.get(path)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assert_("The password reset link was invalid" in response.content)
|
||||
|
||||
def test_confirm_different_passwords(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword',
|
||||
'new_password2':' x'})
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assert_("The two password fields didn't match" in response.content)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
from datetime import date
|
||||
from django.conf import settings
|
||||
from django.utils.http import int_to_base36, base36_to_int
|
||||
|
||||
class PasswordResetTokenGenerator(object):
|
||||
"""
|
||||
Stratgy object used to generate and check tokens for the password
|
||||
reset mechanism.
|
||||
"""
|
||||
def make_token(self, user):
|
||||
"""
|
||||
Returns a token that can be used once to do a password reset
|
||||
for the given user.
|
||||
"""
|
||||
return self._make_token_with_timestamp(user, self._num_days(self._today()))
|
||||
|
||||
def check_token(self, user, token):
|
||||
"""
|
||||
Check that a password reset token is correct for a given user.
|
||||
"""
|
||||
# Parse the tokem
|
||||
try:
|
||||
ts_b36, hash = token.split("-")
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
try:
|
||||
ts = base36_to_int(ts_b36)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# Check that the timestamp/uid has not been tampered with
|
||||
if self._make_token_with_timestamp(user, ts) != token:
|
||||
return False
|
||||
|
||||
# Check the timestamp is within limit
|
||||
if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _make_token_with_timestamp(self, user, timestamp):
|
||||
# timestamp is number of days since 2001-1-1. Converted to
|
||||
# base 36, this gives us a 3 digit string until about 2121
|
||||
ts_b36 = int_to_base36(timestamp)
|
||||
|
||||
# By hashing on the internal state of the user and using state
|
||||
# that is sure to change (the password salt will change as soon as
|
||||
# the password is set, at least for current Django auth, and
|
||||
# last_login will also change), we produce a hash that will be
|
||||
# invalid as soon as it is used.
|
||||
# We limit the hash to 20 chars to keep URL short
|
||||
import sha
|
||||
hash = sha.new(settings.SECRET_KEY + unicode(user.id) +
|
||||
user.password + unicode(user.last_login) +
|
||||
unicode(timestamp)).hexdigest()[::2]
|
||||
return "%s-%s" % (ts_b36, hash)
|
||||
|
||||
def _num_days(self, dt):
|
||||
return (dt - date(2001,1,1)).days
|
||||
|
||||
def _today(self):
|
||||
# Used for mocking in tests
|
||||
return date.today()
|
||||
|
||||
default_token_generator = PasswordResetTokenGenerator()
|
|
@ -8,6 +8,9 @@ urlpatterns = patterns('',
|
|||
('^logout/$', 'django.contrib.auth.views.logout'),
|
||||
('^password_change/$', 'django.contrib.auth.views.password_change'),
|
||||
('^password_change/done/$', 'django.contrib.auth.views.password_change_done'),
|
||||
('^password_reset/$', 'django.contrib.auth.views.password_reset')
|
||||
('^password_reset/$', 'django.contrib.auth.views.password_reset'),
|
||||
('^password_reset/done/$', 'django.contrib.auth.views.password_reset_done'),
|
||||
('^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm'),
|
||||
('^reset/done/$', 'django.contrib.auth.views.password_reset_complete'),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm, AdminPasswordChangeForm
|
||||
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm, AdminPasswordChangeForm
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import render_to_response, get_object_or_404
|
||||
from django.contrib.sites.models import Site, RequestSite
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.template import RequestContext
|
||||
from django.utils.http import urlquote
|
||||
from django.utils.http import urlquote, base36_to_int
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth.models import User
|
||||
|
@ -65,19 +66,29 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
|
|||
login_url = settings.LOGIN_URL
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next)))
|
||||
|
||||
# 4 views for password reset:
|
||||
# - password_reset sends the mail
|
||||
# - password_reset_done shows a success message for the above
|
||||
# - password_reset_confirm checks the link the user clicked and
|
||||
# prompts for a new password
|
||||
# - password_reset_complete shows a success message for the above
|
||||
|
||||
def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html',
|
||||
email_template_name='registration/password_reset_email.html',
|
||||
password_reset_form=PasswordResetForm):
|
||||
password_reset_form=PasswordResetForm, token_generator=default_token_generator):
|
||||
if request.method == "POST":
|
||||
form = password_reset_form(request.POST)
|
||||
if form.is_valid():
|
||||
opts = {}
|
||||
opts['use_https'] = request.is_secure()
|
||||
opts['token_generator'] = token_generator
|
||||
if is_admin_site:
|
||||
form.save(domain_override=request.META['HTTP_HOST'])
|
||||
opts['domain_override'] = request.META['HTTP_HOST']
|
||||
else:
|
||||
if Site._meta.installed:
|
||||
form.save(email_template_name=email_template_name)
|
||||
else:
|
||||
form.save(domain_override=RequestSite(request).domain, email_template_name=email_template_name)
|
||||
opts['email_template_name'] = email_template_name
|
||||
if not Site._meta.installed:
|
||||
opts['domain_override'] = RequestSite(request).domain
|
||||
form.save(**opts)
|
||||
return HttpResponseRedirect('%sdone/' % request.path)
|
||||
else:
|
||||
form = password_reset_form()
|
||||
|
@ -88,6 +99,39 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas
|
|||
def password_reset_done(request, template_name='registration/password_reset_done.html'):
|
||||
return render_to_response(template_name, context_instance=RequestContext(request))
|
||||
|
||||
def password_reset_confirm(request, uidb36=None, token=None, template_name='registration/password_reset_confirm.html',
|
||||
token_generator=default_token_generator, set_password_form=SetPasswordForm):
|
||||
"""
|
||||
View that checks the hash in a password reset link and presents a
|
||||
form for entering a new password.
|
||||
"""
|
||||
assert uidb36 is not None and token is not None # checked by URLconf
|
||||
try:
|
||||
uid_int = base36_to_int(uidb36)
|
||||
except ValueError:
|
||||
raise Http404
|
||||
|
||||
user = get_object_or_404(User, id=uid_int)
|
||||
context_instance = RequestContext(request)
|
||||
|
||||
if token_generator.check_token(user, token):
|
||||
context_instance['validlink'] = True
|
||||
if request.method == 'POST':
|
||||
form = set_password_form(user, request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return HttpResponseRedirect("../done/")
|
||||
else:
|
||||
form = set_password_form(None)
|
||||
else:
|
||||
context_instance['validlink'] = False
|
||||
form = None
|
||||
context_instance['form'] = form
|
||||
return render_to_response(template_name, context_instance=context_instance)
|
||||
|
||||
def password_reset_complete(request, template_name='registration/password_reset_complete.html'):
|
||||
return render_to_response(template_name, context_instance=RequestContext(request))
|
||||
|
||||
def password_change(request, template_name='registration/password_change_form.html'):
|
||||
if request.method == "POST":
|
||||
form = PasswordChangeForm(request.user, request.POST)
|
||||
|
|
|
@ -65,3 +65,32 @@ def http_date(epoch_seconds=None):
|
|||
"""
|
||||
rfcdate = formatdate(epoch_seconds)
|
||||
return '%s GMT' % rfcdate[:25]
|
||||
|
||||
# Base 36 functions: useful for generating compact URLs
|
||||
|
||||
def base36_to_int(s):
|
||||
"""
|
||||
Convertd a base 36 string to an integer
|
||||
"""
|
||||
return int(s, 36)
|
||||
|
||||
def int_to_base36(i):
|
||||
"""
|
||||
Converts an integer to a base36 string
|
||||
"""
|
||||
digits = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
factor = 0
|
||||
# Find starting factor
|
||||
while True:
|
||||
factor += 1
|
||||
if i < 36 ** factor:
|
||||
factor -= 1
|
||||
break
|
||||
base36 = []
|
||||
# Construct base36 representation
|
||||
while factor >= 0:
|
||||
j = 36 ** factor
|
||||
base36.append(digits[i / j])
|
||||
i = i % j
|
||||
factor -= 1
|
||||
return ''.join(base36)
|
||||
|
|
Loading…
Reference in New Issue