重设密码
密码重置过程涉及一些讨厌的URL模式。但是,正如我们在上一教程中讨论的那样,我们不需要成为正则表达式的专家。只是了解常见的问题。
开始之前的另一重要事项是,对于密码重置过程,我们需要发送电子邮件。一开始它有点复杂,因为我们需要外部服务。目前,我们将不会配置生产质量的电子邮件服务。实际上,在开发阶段,我们可以使用Django的调试工具来检查电子邮件是否正确发送。
控制台电子邮件后端
这个想法是在项目的开发过程中进行的,我们只记录它们而不是发送真实的电子邮件。有两种选择:将所有电子邮件写入文本文件,或仅在控制台中显示它们。我发现后一个选项更方便,因为我们已经在使用控制台来运行开发服务器,并且安装起来稍微容易一些。
编辑settings.py模块并将EMAIL_BACKEND变量添加到文件末尾:
myproject / settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'配置路由
密码重置过程需要四个视图:
一个页面,其中包含启动重置过程的表格;成功页面,说明该过程已启动,指示用户检查其垃圾邮件文件夹等;用于检查通过电子邮件发送的令牌的页面;告诉用户重置是否成功的页面。视图是内置的,我们不需要执行任何操作。我们要做的就是将路由添加到urls.py并创建模板。
myproject / urls.py (查看完整的文件内容)
url ( r'^reset/$' , auth_views . PasswordResetView . as_view ( template_name = 'password_reset.html' , email_template_name = 'password_reset_email.html' , subject_template_name = 'password_reset_subject.txt' ), name = 'password_reset' ), url ( r'^reset/done/$' , auth_views . PasswordResetDoneView . as_view ( template_name = 'password_reset_done.html' ), name = 'password_reset_done' ), url ( r'^reset/(?P<uidb64>[0-9A-Za-z_ \ -]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$' , auth_views . PasswordResetConfirmView . as_view ( template_name = 'password_reset_confirm.html' ), name = 'password_reset_confirm' ), url ( r'^reset/complete/$' , auth_views . PasswordResetCompleteView . as_view ( template_name = 'password_reset_complete.html' ), name = 'password_reset_complete' ), ]template_name密码重置视图中的参数是可选的。但是我认为重新定义它是个好主意,因此视图和模板之间的链接比仅使用默认值更为明显。
在templates文件夹中,以下模板文件:
password_reset.htmlpassword_reset_email.html:此模板是发送给用户的电子邮件的正文password_reset_subject.txt:此模板是电子邮件的主题行,应该是单个行文件password_reset_done.htmlpassword_reset_confirm.htmlpassword_reset_complete.html在开始实现模板之前,让我们准备一个新的测试文件。
我们只能添加一些基本测试,因为这些视图和表单已经在Django代码中进行了测试。我们将仅测试应用程序的细节。
在accounts / tests文件夹内创建一个名为test_view_password_reset.py的新测试文件。
密码重设视图
templates / password_reset.html
{% extends 'base_accounts.html' %} {% block title %} Reset your password {% endblock %} {% block content %} <div class= "row justify-content-center" > <div class= "col-lg-4 col-md-6 col-sm-8" > <div class= "card" > <div class= "card-body" > <h3 class= "card-title" > Reset your password </h3> <p> Enter your email address and we will send you a link to reset your password. </p> <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-primary btn-block" > Send password reset email </button> </form> </div> </div> </div> </div> {% endblock %}
帐户/测试/test_view_password_reset.py
from django.contrib.auth import views as auth_views from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.models import User from django.core import mail from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase class PasswordResetTests ( TestCase ): def setUp ( self ): url = reverse ( 'password_reset' ) self . response = self . client . get ( url ) def test_status_code ( self ): self . assertEquals ( self . response . status_code , 200 ) def test_view_function ( self ): view = resolve ( '/reset/' ) self . assertEquals ( view . func . view_class , auth_views . PasswordResetView ) def test_csrf ( self ): self . assertContains ( self . response , 'csrfmiddlewaretoken' ) def test_contains_form ( self ): form = self . response . context . get ( 'form' ) self . assertIsInstance ( form , PasswordResetForm ) def test_form_inputs ( self ): ''' The view must contain two inputs: csrf and email ''' self . assertContains ( self . response , '<input' , 2 ) self . assertContains ( self . response , 'type="email"' , 1 ) class SuccessfulPasswordResetTests ( TestCase ): def setUp ( self ): email = 'john@doe.com' User . objects . create_user ( username = 'john' , email = email , password = '123abcdef' ) url = reverse ( 'password_reset' ) self . response = self . client . post ( url , { 'email' : email }) def test_redirection ( self ): ''' A valid form submission should redirect the user to `password_reset_done` view ''' url = reverse ( 'password_reset_done' ) self . assertRedirects ( self . response , url ) def test_send_password_reset_email ( self ): self . assertEqual ( 1 , len ( mail . outbox )) class InvalidPasswordResetTests ( TestCase ): def setUp ( self ): url = reverse ( 'password_reset' ) self . response = self . client . post ( url , { 'email' : 'donotexist@email.com' }) def test_redirection ( self ): ''' Even invalid emails in the database should redirect the user to `password_reset_done` view ''' url = reverse ( 'password_reset_done' ) self . assertRedirects ( self . response , url ) def test_no_reset_email_sent ( self ): self . assertEqual ( 0 , len ( mail . outbox ))templates / password_reset_subject.txt
[Django Boards] Please reset your passwordtemplates / password_reset_email.html
Hi there, Someone asked for a password reset for the email address {{ email }} . Follow the link below: {{ protocol }} :// {{ domain }}{% url 'password_reset_confirm' uidb64 = uid token = token %} In case you forgot your Django Boards username: {{ user.username }} If clicking the link above doesn't work, please copy and paste the URL in a new browser window instead. If you've received this mail in error, it's likely that another user entered your email address by mistake while trying to reset a password. If you didn't initiate the request, you don't need to take any further action and can safely disregard this email. Thanks, The Django Boards Team
我们可以创建一个特定的文件来测试电子邮件。在accounts / tests文件夹中创建一个名为test_mail_password_reset.py的新文件:
帐户/测试/test_mail_password_reset.py
from django.core import mail from django.contrib.auth.models import User from django.urls import reverse from django.test import TestCase class PasswordResetMailTests ( TestCase ): def setUp ( self ): User . objects . create_user ( username = 'john' , email = 'john@doe.com' , password = '123' ) self . response = self . client . post ( reverse ( 'password_reset' ), { 'email' : 'john@doe.com' }) self . email = mail . outbox [ 0 ] def test_email_subject ( self ): self . assertEqual ( '[Django Boards] Please reset your password' , self . email . subject ) def test_email_body ( self ): context = self . response . context token = context . get ( 'token' ) uid = context . get ( 'uid' ) password_reset_token_url = reverse ( 'password_reset_confirm' , kwargs = { 'uidb64' : uid , 'token' : token }) self . assertIn ( password_reset_token_url , self . email . body ) self . assertIn ( 'john' , self . email . body ) self . assertIn ( 'john@doe.com' , self . email . body ) def test_email_to ( self ): self . assertEqual ([ 'john@doe.com' ,], self . email . to )该测试用例获取应用程序发送的电子邮件,并检查主题行,正文内容以及发送给谁的电子邮件。
密码重置完成视图
templates / password_reset_done.html
{% extends 'base_accounts.html' %} {% block title %} Reset your password {% endblock %} {% block content %} <div class= "row justify-content-center" > <div class= "col-lg-4 col-md-6 col-sm-8" > <div class= "card" > <div class= "card-body" > <h3 class= "card-title" > Reset your password </h3> <p> Check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder. </p> <a href= " {% url 'login' %} " class= "btn btn-secondary btn-block" > Return to log in </a> </div> </div> </div> </div> {% endblock %}
帐户/测试/test_view_password_reset.py
from django.contrib.auth import views as auth_views from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase class PasswordResetDoneTests ( TestCase ): def setUp ( self ): url = reverse ( 'password_reset_done' ) self . response = self . client . get ( url ) def test_status_code ( self ): self . assertEquals ( self . response . status_code , 200 ) def test_view_function ( self ): view = resolve ( '/reset/done/' ) self . assertEquals ( view . func . view_class , auth_views . PasswordResetDoneView )密码重置确认视图
templates / password_reset_confirm.html
{% extends 'base_accounts.html' %} {% block title %} {% if validlink %} Change password for {{ form.user.username }} {% else %} Reset your password {% endif %} {% endblock %} {% block content %} <div class= "row justify-content-center" > <div class= "col-lg-6 col-md-8 col-sm-10" > <div class= "card" > <div class= "card-body" > {% if validlink %} <h3 class= "card-title" > Change password for @ {{ form.user.username }} </h3> <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-success btn-block" > Change password </button> </form> {% else %} <h3 class= "card-title" > Reset your password </h3> <div class= "alert alert-danger" role= "alert" > It looks like you clicked on an invalid password reset link. Please try again. </div> <a href= " {% url 'password_reset' %} " class= "btn btn-secondary btn-block" > Request a new password reset link </a> {% endif %} </div> </div> </div> </div> {% endblock %}只能使用电子邮件中发送的链接访问此页面。看起来像这样:http : //127.0.0.1 : 8000/reset/Mw/4po-2b5f2d47c19966e294a1/
在开发阶段,请从控制台的电子邮件中获取此链接。
如果链接有效:
或如果链接已被使用:
帐户/测试/test_view_password_reset.py
from django.contrib.auth.tokens import default_token_generator from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode from django.contrib.auth import views as auth_views from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase class PasswordResetConfirmTests ( TestCase ): def setUp ( self ): user = User . objects . create_user ( username = 'john' , email = 'john@doe.com' , password = '123abcdef' ) ''' create a valid password reset token based on how django creates the token internally: https://github.com/django/django/blob/1.11.5/django/contrib/auth/forms.py#L280 ''' self . uid = urlsafe_base64_encode ( force_bytes ( user . pk )) . decode () self . token = default_token_generator . make_token ( user ) url = reverse ( 'password_reset_confirm' , kwargs = { 'uidb64' : self . uid , 'token' : self . token }) self . response = self . client . get ( url , follow = True ) def test_status_code ( self ): self . assertEquals ( self . response . status_code , 200 ) def test_view_function ( self ): view = resolve ( '/reset/{uidb64}/{token}/' . format ( uidb64 = self . uid , token = self . token )) self . assertEquals ( view . func . view_class , auth_views . PasswordResetConfirmView ) def test_csrf ( self ): self . assertContains ( self . response , 'csrfmiddlewaretoken' ) def test_contains_form ( self ): form = self . response . context . get ( 'form' ) self . assertIsInstance ( form , SetPasswordForm ) def test_form_inputs ( self ): ''' The view must contain two inputs: csrf and two password fields ''' self . assertContains ( self . response , '<input' , 3 ) self . assertContains ( self . response , 'type="password"' , 2 ) class InvalidPasswordResetConfirmTests ( TestCase ): def setUp ( self ): user = User . objects . create_user ( username = 'john' , email = 'john@doe.com' , password = '123abcdef' ) uid = urlsafe_base64_encode ( force_bytes ( user . pk )) . decode () token = default_token_generator . make_token ( user ) ''' invalidate the token by changing the password ''' user . set_password ( 'abcdef123' ) user . save () url = reverse ( 'password_reset_confirm' , kwargs = { 'uidb64' : uid , 'token' : token }) self . response = self . client . get ( url ) def test_status_code ( self ): self . assertEquals ( self . response . status_code , 200 ) def test_html ( self ): password_reset_url = reverse ( 'password_reset' ) self . assertContains ( self . response , 'invalid password reset link' ) self . assertContains ( self . response , 'href="{0}"' . format ( password_reset_url ))密码重置完成视图
templates / password_reset_complete.html
{% extends 'base_accounts.html' %} {% block title %} Password changed! {% endblock %} {% block content %} <div class= "row justify-content-center" > <div class= "col-lg-6 col-md-8 col-sm-10" > <div class= "card" > <div class= "card-body" > <h3 class= "card-title" > Password changed! </h3> <div class= "alert alert-success" role= "alert" > You have successfully changed your password! You may now proceed to log in. </div> <a href= " {% url 'login' %} " class= "btn btn-secondary btn-block" > Return to log in </a> </div> </div> </div> </div> {% endblock %}
帐户/测试/test_view_password_reset.py (查看完整的文件内容)
from django.contrib.auth import views as auth_views from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase class PasswordResetCompleteTests ( TestCase ): def setUp ( self ): url = reverse ( 'password_reset_complete' ) self . response = self . client . get ( url ) def test_status_code ( self ): self . assertEquals ( self . response . status_code , 200 ) def test_view_function ( self ): view = resolve ( '/reset/complete/' ) self . assertEquals ( view . func . view_class , auth_views . PasswordResetCompleteView )