This is a time-consuming, although straight-forward feature to
implement. By far the hardest thing is understanding the sequence of
events, particularly because the views are confusingly named. This is
how it works (with specifics based on the implementation in this
chapter):
Although I would choose these views to have clearer names more along the lines of
This is what the email looks like when printed to the console, with some empty lines removed (the
In order for the final “your password was successfully changed” view (
Change
to
To create a custom template, you can
The relative path of the file (as based off of one of the
Custom template:
The only custom template we’ll be creating is for the set-your-new-password form (
This is the default template, as installed by Django:
The entire “content” block must be replaced with our custom code. The
form as associated to the JavaScript by giving it the name “
Save the following as
Save the following as
(To repeat the warning from the top of part six: The server-side length checks are “succeeding” but crashing–that is, they only crash when the lengths are incorrect. While this is a critical problem, it only applies when the client-side JavaScript is disabled. Here is a Stack Overflow question documenting the problem. A solution would be greatly appreciated.)
with
Output:
Our tests passed.
- On the login page there is an “I forgot my password” link.
- Click on it and you are taken to a page on which you need to enter
your the email address from when you created the account. Pressing
submit sends a one-time-only reset-my-password link to the user’s email.
The view for this page is
django.contrib.auth.views.password_reset
. - After it’s submitted, another page appears whose only purpose is to
inform that the the email was sent, and they should go and view it for
further instructions. The view for this page is
django.contrib.auth.views.password_reset_done
. - The email itself is sent. The template for this email is specified by the
email_template_name
parameter of thepassword_reset
view. See the next section for an example email. - The link in the email takes you to the “set your new password” form,
which includes a redundant confirmation field. The view for this page
is
django.contrib.auth.views.password_reset_confirm
. - After the form is submitted, the final page is presented, which only
states “your password has been changed”, and likely provides a link
back to the login page. The view for this page is
django.contrib.auth.views.password_reset_complete
.
Although I would choose these views to have clearer names more along the lines of
password_reset_1of4_email_request
,password_reset_2of4_email_sent
,pwd_reset_3of4_new_pwd_form
, andpassword_reset_4of4_finished
Set up email: Print to console only
Normally an email is actually sent (how to do this). Instead, we’re going to print its contents to the console. This is trivially-implemented by adding a single variable to/home/myname/django_auth_lifecycle/djauth_root/django_auth_lifecycle/settings.py
1
2
| EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' |
127.0.0.1.:8001
in the link must be changed to the name of your webserver):MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit Subject: Password reset on 127.0.0.1:8001 From: webmaster@localhost To: myemailaddress@yahoo.com Date: Wed, 18 Feb 2015 16:38:02 -0000 Message-ID: You're receiving this email because you requested a password reset for your user account at 127.0.0.1:8001. Please go to the following page and choose a new password: http://127.0.0.1:8001/auth/pwd_reset_3of4_new_pwd_form/MQ/3zd-add9dfa05216b9ead4cc/ Your username, in case you've forgotten: admin Thanks for using our site! The 127.0.0.1:8001 team -------------------------------------------------------------------------------
Set the login view name
One more change in the settings file.In order for the final “your password was successfully changed” view (
password_reset_complete
) to link back to the login page, we must tell it where to link to, since ours is not using the default name. Add the LOGIN_URL
variable:
1
2
| LOGIN_URL = "login" #View name in auth_lifecycle.registration.urls |
Activate the forgot-my-password link
In/home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/templates/registration/login.html
Change
1
| ...I forgot my password... |
1
| < a href = "{% url 'password_reset' %}" >I forgot my password</ a > |
Custom templates: Overview
Aside from the email itself, each of the four views has its own template. Creating custom versions of these templates is optional. You could use all of the built-in defaults and skip straight to updatingurls.py
. The directory containing all default templates, as installed with Django, is:/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/django/contrib/admin/templates/registration/
To create a custom template, you can
- Duplicate the entire custom template and tweak what you like, or
- Extend the template and override only the needed sections
The relative path of the file (as based off of one of the
TEMPLATE_DIRS
) must be either
- Equal to the default value of the view’s
template_name
parameter, as specified in each view’s documentation. For example, thepassword_reset
view states “Defaults toregistration/password_change_done.html
if not supplied.” - Or set to an alternate value, by passing it through it’s url-entry:
123
url(r
"^pwd_reset_3of4_new_pwd_form/(?P<uidb64>\w+)/(?P<token>[\w-]+)/$"
,
"django.contrib.auth.views.password_reset_confirm"
,
{
"template_name"
:
"registration/pwd_reset_3of4_new_pwd_form.html"
}
email_template_name
parameter in password_reset
.)
Custom template: pwd_reset_3of4_new_pwd_form
The only custom template we’ll be creating is for the set-your-new-password form (django.contrib.auth.views.password_reset_done
), so we can also do a client-side check for the password lengths, and that they’re equal. This will be implemented with JQuery Validation.This is the default template, as installed by Django:
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/django/contrib/admin/templates/registration/password_reset_confirm.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| {% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %} < div class = "breadcrumbs" > < a href = "{% url 'admin:index' %}" >{% trans 'Home' %}</ a > › {% trans 'Password reset confirmation' %} </ div > {% endblock %} {% block title %}{{ title }}{% endblock %} {% block content_title %}< h1 >{{ title }}</ h1 >{% endblock %} {% block content %} {% if validlink %} < p >{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</ p > < form action = "" method = "post" >{% csrf_token %} {{ form.new_password1.errors }} < p class = "aligned wide" >< label for = "id_new_password1" >{% trans 'New password:' %}</ label >{{ form.new_password1 }}</ p > {{ form.new_password2.errors }} < 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 %} < p >{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</ p > {% endif %} {% endblock %} |
newPwdForm
“.Save the following as
/home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/templates/registration/pwd_reset_3of4_new_pwd_form.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
| {% extends "registration/password_reset_confirm.html" %} {% comment %} The extends Must be the first line in the template. This comment may not be before it. Example use: Documentation: {% endcomment %} {% load i18n %} {# For the "trans" tag #} {% block content %} {% if validlink %} < p >{% trans "Please enter your new password twice so we can verify." %}</ p > < p >{% trans "you typed it in correctly." %}</ p > < form action = "" method = "post" id = "newPwdForm" > <!-- Form id rqd by JS. --> {% csrf_token %} {{ form.new_password1.errors }} < p class = "aligned wide" > < label for = "id_new_password1" >{% trans 'New password:' %}</ label > {{ form.new_password1 }}</ p > {{ form.new_password2.errors }} < 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 %} < p >{% trans "The password reset link was invalid, possibly because it" %} {% trans "has already been used. Please request a new password reset." %}</ p > {% endif %} < script > //These values come from auth_lifecycle.models var minPassLen = {{ PASSWORD_MIN_LEN }}; //PASSWORD_MIN_LEN var maxPassLen = {{ PASSWORD_MAX_LEN }}; //PASSWORD_MAX_LEN var passwordMsg = "{% trans "Password must be between " %}" + minPassLen + "{% trans " and " %}" + maxPassLen + "{% trans " characters, inclusive." %}"; jQuery.validator.setDefaults({ success: "valid", //Avoids form submit. Comment when in production...START //debug: true //submitHandler: function() { // alert("Success! The form was pretend-submitted!"); //} //Avoids form submit. Comment when in production...END }); $( "#newPwdForm" ).validate({ rules: { new_password1: { required: true, minlength: minPassLen, maxlength: maxPassLen }, new_password2: { equalTo: "#id_new_password1" } }, messages: { new_password1: { required: "{% trans "Password required" %}", minlength: passwordMsg, maxlength: passwordMsg } } }); </ script > {% endblock %} |
Server-side check
As done with the login form, we’re going to update the set-a-new-password form to enforce length. The default form used by this view,django.contrib.auth.forms.SetPasswordForm
, does check for the passwords being equal, but does not have any min or max lengths:
1
2
| new_password1 = forms.CharField(label = _( "New password" ), widget = forms.PasswordInput) |
/home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/form_reset_set_new_pwd.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| from auth_lifecycle.models import PASSWORD_MIN_LEN, PASSWORD_MAX_LEN from auth_lifecycle.registration.view_login import get_min_max_incl_err_msg from django import forms #NOT django.contrib.auth.forms from django.contrib.auth.forms import SetPasswordForm from django.utils.translation import ugettext, ugettext_lazy as _ min_max_len_err_msg = get_min_max_incl_err_msg(PASSWORD_MIN_LEN, PASSWORD_MAX_LEN) class SetPasswordFormEnforceLength(SetPasswordForm): """ A `SetPasswordForm` that enforces min/max lengths. Pass this into the login form via the `set_password_form` parameter. Which is done in `registration/urls.py`. """ new_password1 = forms.CharField(label = _( "New password" ), widget = forms.PasswordInput, min_length = PASSWORD_MIN_LEN, max_length = PASSWORD_MAX_LEN, error_messages = { 'min_length' : min_max_len_err_msg, 'max_length' : min_max_len_err_msg }) |
(To repeat the warning from the top of part six: The server-side length checks are “succeeding” but crashing–that is, they only crash when the lengths are incorrect. While this is a critical problem, it only applies when the client-side JavaScript is disabled. Here is a Stack Overflow question documenting the problem. A solution would be greatly appreciated.)
Configure the urls
Replace the contents of/home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/urls.py
with
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| from auth_lifecycle.models import PASSWORD_MIN_LEN, PASSWORD_MAX_LEN from auth_lifecycle.registration.view_login import AuthenticationFormEnforceLength from auth_lifecycle.registration.form_reset_set_new_pwd import SetPasswordFormEnforceLength from django.conf.urls import patterns, url #Passing keyword arguments through url entries: urlpatterns = patterns('', url(r "^login/$" , "auth_lifecycle.registration.view_login.login_maybe_remember" , { "authentication_form" : AuthenticationFormEnforceLength }, name = "login" ), url(r "^logout_then_login/$" , "django.contrib.auth.views.logout_then_login" , { "login_url" : "login" }, name = "logout_then_login" ), url(r "^password_reset_1of4_email_request/$" , "django.contrib.auth.views.password_reset" , name = "password_reset" ), url(r "^password_reset_2of4_email_sent/$" , "django.contrib.auth.views.password_reset_done" , name = "password_reset_done" ), url(r "^pwd_reset_3of4_new_pwd_form/(?P<uidb64>\w+)/(?P<token>[\w-]+)/$" , "django.contrib.auth.views.password_reset_confirm" , { "template_name" : "registration/pwd_reset_3of4_new_pwd_form.html" , "extra_context" : { "PASSWORD_MIN_LEN" : PASSWORD_MIN_LEN, "PASSWORD_MAX_LEN" : PASSWORD_MAX_LEN }, "set_password_form" : SetPasswordFormEnforceLength }, name = "password_reset_confirm" ), #If NOT using a custom template: # url(r"^pwd_reset_3of4_new_pwd_form/(?P<uidb64>\w+)/(?P<token>[\w-]+)/$", # "django.contrib.auth.views.password_reset_confirm", # name="password_reset_confirm"), url(r "^password_reset_4of4_finished/$" , "django.contrib.auth.views.password_reset_complete" , name = "password_reset_complete" ), ) |
Tests
Save the following as/home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/....py
1
| ... |
Our tests passed.
No comments:
Post a Comment