本教程将全部是关于Django的身份验证系统。 我们将实现整个过程:注册,登录,注销,密码重置和密码更改。
您还将简要介绍如何保护未授权用户的某些视图以及如何访问登录用户的信息。
在下一节中,您将找到一些我们将在本教程中实现的与身份验证相关的页面线框。 之后,您将找到一个新的Django应用程序的初始设置。 到目前为止,我们一直致力于一个名为board的应用程序。 但是所有与身份验证相关的东西都可以存在于不同的应用程序中,因此可以更好地组织代码。
线框
我们必须更新应用程序的线框。 首先,我们将为顶级菜单添加新选项。 如果用户未经过身份验证,我们应该有两个按钮:注册并登录。
图1:未经过身份验证的用户的顶级菜单。
如果用户经过身份验证,我们应该显示他们的名字以及一个包含三个选项的下拉菜单:我的帐户,更改密码和注销。
图2:经过身份验证的用户的顶级菜单。
在登录页面上,我们需要一个包含用户名和密码的表单,一个包含主要操作(登录)的按钮和两个备用路径:注册页面和密码重置页面。
图3:登录页面
在注册页面上,我们应该有一个包含四个字段的表单: 用户名 , 电子邮件地址 , 密码和密码确认 。 用户还应该能够访问登录页面。
图4:注册页面
在密码重置页面上,我们将有一个只包含电子邮件地址的表单。
图5:密码重置
然后,在单击特殊令牌链接后,用户将被重定向到可以设置新密码的页面:
图6:更改密码
初始设置
要管理所有这些信息,我们可以在其他应用中将其细分。 在项目根目录中,在manage.py脚本所在的同一页面中,运行以下命令以启动新应用程序:
django-admin startapp accounts
项目结构现在应该是这样的:
myproject/ |-- myproject/ | |-- accounts/ <-- our new django app! | |-- boards/ | |-- myproject/ | |-- static/ | |-- templates/ | |-- db.sqlite3 | +-- manage.py +-- venv/
下一步,将accounts app应用到settings.py文件中的INSTALLED_APPS :
INSTALLED_APPS = [ 'django.contrib.admin' , 'django.contrib.auth' , 'django.contrib.contenttypes' , 'django.contrib.sessions' , 'django.contrib.messages' , 'django.contrib.staticfiles' , 'widget_tweaks' , 'accounts' , 'boards' , ]
从现在开始,我们将开发帐户应用程序。
注册
让我们从创建注册视图开始。 首先,在urls.py文件中创建一个新路由:
MyProject的/ urls.py
from django.conf.urls import url from django.contrib import admin from accounts import views as accounts_views from boards import views urlpatterns = [ url ( r'^$' , views . home , name = 'home' ), url ( r'^signup/$' , accounts_views . signup , name = 'signup' ), url ( r'^boards/(?P<pk> \ d+)/$' , views . board_topics , name = 'board_topics' ), url ( r'^boards/(?P<pk> \ d+)/new/$' , views . new_topic , name = 'new_topic' ), url ( r'^admin/' , admin . site . urls ), ]
请注意我们如何以不同的方式从帐户应用程序导入视图模块:
from accounts import views as accounts_views
我们给别名,否则,它会与董事会的意见发生冲突。 我们稍后可以改进urls.py设计。 但是现在,让我们关注身份验证功能。
现在编辑accounts应用程序中的views.py并创建一个名为signup的新视图:
账户/ views.py
from django.shortcuts import render def signup ( request ): return render ( request , 'signup.html' )
创建名为signup.html的新模板:
模板/ signup.html
{% extends 'base.html' %} {% block content %} <h2> Sign up </h2> {% endblock %}
在浏览器中打开URL http://127.0.0.1:8000/signup/ ,检查一切是否正常:
是时候写一些测试了:
账户/ tests.py
from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase from .views import signup class SignUpTests ( TestCase ): def test_signup_status_code ( self ): url = reverse ( 'signup' ) response = self . client . get ( url ) self . assertEquals ( response . status_code , 200 ) def test_signup_url_resolves_signup_view ( self ): view = resolve ( '/signup/' ) self . assertEquals ( view . func , signup )
测试状态代码(200 =成功)以及URL / signup /是否返回正确的视图功能。
python manage.py test
Creating test database for alias 'default'... System check identified no issues (0 silenced). .................. ---------------------------------------------------------------------- Ran 18 tests in 0.652s OK Destroying test database for alias 'default'...
对于身份验证视图(注册,登录,密码重置等),我们不会使用顶部栏或面包屑。 我们仍然可以使用base.html模板。 它只需要一些调整:
模板/ base.html文件
{% load static %}<!DOCTYPE html> <html> <head> <meta charset= "utf-8" > <title> {% block title %} Django Boards {% endblock %} </title> <link href= "https://fonts.googleapis.com/css?family=Peralta" rel= "stylesheet" > <link rel= "stylesheet" href= " {% static 'css/bootstrap.min.css' %} " > <link rel= "stylesheet" href= " {% static 'css/app.css' %} " > {% block stylesheet %}{% endblock %} <!-- HERE --> </head> <body> {% block body %} <!-- HERE --> <nav class= "navbar navbar-expand-lg navbar-dark bg-dark" > <div class= "container" > <a class= "navbar-brand" href= " {% url 'home' %} " > Django Boards </a> </div> </nav> <div class= "container" > <ol class= "breadcrumb my-4" > {% block breadcrumb %} {% endblock %} </ol> {% block content %} {% endblock %} </div> {% endblock body %} <!-- AND HERE --> </body> </html>
我在注释中标记了base.html模板中的新位。 区块{ % block stylesheet % }{ % endblock % } { % block stylesheet % }{ % endblock % } { % block stylesheet % }{ % endblock % }将用于添加额外的CSS,特定于某些页面。
区块{ % block body % } { % block body % } { % block body % }包装整个HTML文档。 我们可以使用它来获取利用base.html头部的空文档。 注意我们如何命名结束块{ % endblock body % } { % endblock body % } { % endblock body % } 。 在这种情况下,命名结束标记是一个好习惯,因此更容易识别结束标记。
现在在signup.html模板上,而不是使用{ % block content % } { % block content % } { % block content % } ,我们可以使用{ % block body % } { % block body % } { % block body % } 。
模板/ signup.html
{% extends 'base.html' %} {% block body %} <h2> Sign up </h2> {% endblock %}
是时候创建注册表单了。 Django有一个名为UserCreationForm的内置表单。 我们来使用它:
账户/ views.py
from django.contrib.auth.forms import UserCreationForm from django.shortcuts import render def signup ( request ): form = UserCreationForm () return render ( request , 'signup.html' , { 'form' : form })
模板/ signup.html
{% extends 'base.html' %} {% block body %} <div class= "container" > <h2> Sign up </h2> <form method= "post" novalidate > {% csrf_token %} {{ form.as_p }} <button type= "submit" class= "btn btn-primary" > Create an account </button> </form> </div> {% endblock %}
看起来有点乱,对吧? 我们可以使用我们的form.html模板使其看起来更好:
模板/ signup.html
{% extends 'base.html' %} {% block body %} <div class= "container" > <h2> Sign up </h2> <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-primary" > Create an account </button> </form> </div> {% endblock %}
呃,几乎就在那里。 目前,我们的form.html部分模板正在显示一些原始HTML。 这是一个安全功能。 默认情况下,Django将所有字符串视为不安全,转义可能导致问题的所有特殊字符。 但在这种情况下,我们可以相信它。
模板/包括/ form.html
{% load widget_tweaks %} {% for field in form %} <div class= "form-group" > {{ field.label_tag }} <!-- code suppressed for brevity --> {% if field.help_text %} <small class= "form-text text-muted" > {{ field.help_text | safe }} <!-- new code here --> </small> {% endif %} </div> {% endfor %}
基本上,在上一个模板中,我们向field.help_text添加了选项safe : { { field.help_text|safe } } { { field.help_text|safe } } { { field.help_text|safe } } 。
保存form.html文件,然后再次检查注册页面:
现在让我们在注册视图中实现业务逻辑:
账户/ views.py
from django.contrib.auth import login as auth_login from django.contrib.auth.forms import UserCreationForm from django.shortcuts import render , redirect def signup ( request ): if request . method == 'POST' : form = UserCreationForm ( request . POST ) if form . is_valid (): user = form . save () auth_login ( request , user ) return redirect ( 'home' ) else : form = UserCreationForm () return render ( request , 'signup.html' , { 'form' : form })
具有小细节的基本表单处理: 登录功能(重命名为auth_login以避免与内置登录视图发生冲突)。
注意:我将login函数重命名为auth_login ,但后来我意识到Django 1.11有一个基于类的视图,用于登录视图LoginView ,因此不存在冲突名称的风险。
在旧版本中有一个auth.login和auth.view.login ,它曾经引起一些混乱,因为一个是记录用户的功能,另一个是视图。
长话短说:如果你愿意,你可以像login一样导入它,它不会造成任何问题。
如果表单有效,则使用user = form.save()创建User实例。 然后,创建的用户作为参数传递给auth_login函数,手动验证用户。 之后,视图将用户重定向到主页,保持应用程序的流程。
我们来试试吧。 首先,提交一些无效数据。 空表单,不匹配字段或现有用户名:
现在填写表单并提交,检查用户是否已创建并重定向到主页:
在模板中引用经过身份验证的用户
我们怎么知道它是否有效? 好吧,我们可以编辑base.html模板,在顶栏上添加用户名:
模板/ base.html文件
{% block body %} <nav class= "navbar navbar-expand-sm navbar-dark bg-dark" > <div class= "container" > <a class= "navbar-brand" href= " {% url 'home' %} " > Django Boards </a> <button class= "navbar-toggler" type= "button" data-toggle= "collapse" data-target= "#mainMenu" aria-controls= "mainMenu" aria-expanded= "false" aria-label= "Toggle navigation" > <span class= "navbar-toggler-icon" ></span> </button> <div class= "collapse navbar-collapse" id= "mainMenu" > <ul class= "navbar-nav ml-auto" > <li class= "nav-item" > <a class= "nav-link" href= "#" > {{ user.username }} </a> </li> </ul> </div> </div> </nav> <div class= "container" > <ol class= "breadcrumb my-4" > {% block breadcrumb %} {% endblock %} </ol> {% block content %} {% endblock %} </div> {% endblock body %}
测试注册视图
我们现在改进我们的测试用例:
账户/ tests.py
from django.contrib.auth.forms import UserCreationForm from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase from .views import signup class SignUpTests ( TestCase ): def setUp ( self ): url = reverse ( 'signup' ) self . response = self . client . get ( url ) def test_signup_status_code ( self ): self . assertEquals ( self . response . status_code , 200 ) def test_signup_url_resolves_signup_view ( self ): view = resolve ( '/signup/' ) self . assertEquals ( view . func , signup ) def test_csrf ( self ): self . assertContains ( self . response , 'csrfmiddlewaretoken' ) def test_contains_form ( self ): form = self . response . context . get ( 'form' ) self . assertIsInstance ( form , UserCreationForm )
我们改变了一点SignUpTests类。 定义了一个setUp方法,将响应对象移动到那里。 然后,现在我们还测试响应中是否有表单和CSRF令牌。
现在我们将测试一个成功的注册。 这一次,让我们创建一个新类来组织更好的测试:
账户/ tests.py
from django.contrib.auth.models import User from django.contrib.auth.forms import UserCreationForm from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase from .views import signup class SignUpTests ( TestCase ): # code suppressed... class SuccessfulSignUpTests ( TestCase ): def setUp ( self ): url = reverse ( 'signup' ) data = { 'username' : 'john' , 'password1' : 'abcdef123456' , 'password2' : 'abcdef123456' } self . response = self . client . post ( url , data ) self . home_url = reverse ( 'home' ) def test_redirection ( self ): ''' A valid form submission should redirect the user to the home page ''' self . assertRedirects ( self . response , self . home_url ) def test_user_creation ( self ): self . assertTrue ( User . objects . exists ()) def test_user_authentication ( self ): ''' Create a new request to an arbitrary page. The resulting response should now have a `user` to its context, after a successful sign up. ''' response = self . client . get ( self . home_url ) user = response . context . get ( 'user' ) self . assertTrue ( user . is_authenticated )
运行测试。
使用类似的策略,现在让我们在数据无效时为注册测试创建一个新类:
from django.contrib.auth.models import User from django.contrib.auth.forms import UserCreationForm from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase from .views import signup class SignUpTests ( TestCase ): # code suppressed... class SuccessfulSignUpTests ( TestCase ): # code suppressed... class InvalidSignUpTests ( TestCase ): def setUp ( self ): url = reverse ( 'signup' ) self . response = self . client . post ( url , {}) # submit an empty dictionary def test_signup_status_code ( self ): ''' An invalid form submission should return to the same page ''' self . assertEquals ( self . response . status_code , 200 ) def test_form_errors ( self ): form = self . response . context . get ( 'form' ) self . assertTrue ( form . errors ) def test_dont_create_user ( self ): self . assertFalse ( User . objects . exists ())
将电子邮件字段添加到表单
一切正常,但…… 电子邮件地址字段丢失。 那么, UserCreationForm不提供电子邮件字段。 但我们可以扩展它。
在accounts文件夹中创建一个名为forms.py的文件:
账户/ forms.py
from django import forms from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User class SignUpForm ( UserCreationForm ): email = forms . CharField ( max_length = 254 , required = True , widget = forms . EmailInput ()) class Meta : model = User fields = ( 'username' , 'email' , 'password1' , 'password2' )
现在,让我们导入新表单SignUpForm ,而不是在views.py中使用UserCreationForm ,而是使用它:
账户/ views.py
from django.contrib.auth import login as auth_login from django.shortcuts import render , redirect from .forms import SignUpForm def signup ( request ): if request . method == 'POST' : form = SignUpForm ( request . POST ) if form . is_valid (): user = form . save () auth_login ( request , user ) return redirect ( 'home' ) else : form = SignUpForm () return render ( request , 'signup.html' , { 'form' : form })
只是这个小小的改变,一切都已经有效了:
请记住更改测试用例以使用SignUpForm而不是UserCreationForm :
from .forms import SignUpForm class SignUpTests ( TestCase ): # ... def test_contains_form ( self ): form = self . response . context . get ( 'form' ) self . assertIsInstance ( form , SignUpForm ) class SuccessfulSignUpTests ( TestCase ): def setUp ( self ): url = reverse ( 'signup' ) data = { 'username' : 'john' , 'email' : 'john@doe.com' , 'password1' : 'abcdef123456' , 'password2' : 'abcdef123456' } self . response = self . client . post ( url , data ) self . home_url = reverse ( 'home' ) # ...
之前的测试用例仍然会通过,因为自SignUpForm扩展UserCreationForm以来, 它就是 UserCreationForm的一个实例。
现在让我们考虑一下发生的事情。 我们添加了一个新的表单字段:
fields = ( 'username' , 'email' , 'password1' , 'password2' )
它会自动反映在HTML模板中。 这很好,对吧? 嗯,取决于。 如果将来新开发人员希望将SignUpForm重新用于其他内容,并为其添加一些额外的字段,该怎么办 ? 然后这些新字段也会显示在signup.html中 ,这可能不是所需的行为。 这种变化可能会被忽视,我们不希望有任何意外。
因此,让我们创建一个新测试,验证模板中的HTML输入:
账户/ tests.py
class SignUpTests ( TestCase ): # ... def test_form_inputs ( self ): ''' The view must contain five inputs: csrf, username, email, password1, password2 ''' self . assertContains ( self . response , '<input' , 5 ) self . assertContains ( self . response , 'type="text"' , 1 ) self . assertContains ( self . response , 'type="email"' , 1 ) self . assertContains ( self . response , 'type="password"' , 2 )
改进测试布局
好吧,我们正在测试输入和所有内容,但我们仍然需要测试表单本身。 不要只是继续在accounts / tests.py文件中添加测试,让我们稍微改进一下项目设计。
在accounts文件夹中创建一个名为tests的新文件夹。 然后,在tests文件夹中,创建一个名为__init__.py的空文件。
现在,将tests.py文件移动到tests文件夹中,并将其重命名为test_view_signup.py 。
最终结果应如下:
myproject/ |-- myproject/ | |-- accounts/ | | |-- migrations/ | | |-- tests/ | | | |-- __init__.py | | | +-- test_view_signup.py | | |-- __init__.py | | |-- admin.py | | |-- apps.py | | |-- models.py | | +-- views.py | |-- boards/ | |-- myproject/ | |-- static/ | |-- templates/ | |-- db.sqlite3 | +-- manage.py +-- venv/
请注意,由于我们在应用程序的上下文中使用相对导入,因此我们需要在新的test_view_signup.py中修复导入:
账户/测试/ test_view_signup.py
from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase from ..views import signup from ..forms import SignUpForm
我们在app模块中使用相对导入,因此我们可以自由地在以后重命名Django应用程序,而无需修复所有绝对导入。
现在让我们创建一个新的测试文件来测试SignUpForm 。 添加名为test_form_signup.py的新测试文件:
账户/测试/ test_form_signup.py
from django.test import TestCase from ..forms import SignUpForm class SignUpFormTest ( TestCase ): def test_form_has_fields ( self ): form = SignUpForm () expected = [ 'username' , 'email' , 'password1' , 'password2' ,] actual = list ( form . fields ) self . assertSequenceEqual ( expected , actual )
它看起来非常严格,对吗? 例如,如果将来我们必须更改SignUpForm ,以包含用户的名字和姓氏,我们可能最终必须修复一些测试用例,即使我们没有破坏任何东西。
这些警报很有用,因为它们有助于提高认知度,特别是对于第一次触摸代码的新手。 它可以帮助他们自信地编码。
改进注册模板
让我们对它进行一些工作。 在这里,我们可以使用Bootstrap 4卡组件使其看起来很好。
转到https://www.toptal.com/designers/subtlepatterns/并找到一个漂亮的背景图案,用作帐户页面的背景。 下载它,在静态文件夹中创建一个名为img的新文件夹,然后将图像放在那里。
然后,在static / css中创建一个名为accounts.css的新CSS文件。 结果应如下:
myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | |-- myproject/ | |-- static/ | | |-- css/ | | | |-- accounts.css <-- here | | | |-- app.css | | | +-- bootstrap.min.css | | +-- img/ | | | +-- shattered.png <-- here ( the name may be different, depending on the patter you downloaded ) | |-- templates/ | |-- db.sqlite3 | +-- manage.py +-- venv/
现在编辑accounts.css文件:
静态/ CSS / accounts.css
body { background-image : url(../img/shattered.png) ; } .logo { font-family : 'Peralta' , cursive ; } .logo a { color : rgba ( 0 , 0 , 0 , .9 ); } .logo a :hover , .logo a :active { text-decoration : none ; }
在signup.html模板中,我们可以更改它以使用新的CSS并使用Bootstrap 4卡组件:
模板/ signup.html
{% extends 'base.html' %} {% load static %} {% block stylesheet %} <link rel= "stylesheet" href= " {% static 'css/accounts.css' %} " > {% endblock %} {% block body %} <div class= "container" > <h1 class= "text-center logo my-4" > <a href= " {% url 'home' %} " > Django Boards </a> </h1> <div class= "row justify-content-center" > <div class= "col-lg-8 col-md-10 col-sm-12" > <div class= "card" > <div class= "card-body" > <h3 class= "card-title" > Sign up </h3> <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-primary btn-block" > Create an account </button> </form> </div> <div class= "card-footer text-muted text-center" > Already have an account? <a href= "#" > Log in </a> </div> </div> </div> </div> </div> {% endblock %}
有了它,这应该是我们现在的注册页面:
登出
为了在实现中保持自然流程,让我们添加注销视图。 首先,编辑urls.py以添加新路由:
MyProject的/ urls.py
from django.conf.urls import url from django.contrib import admin from django.contrib.auth import views as auth_views from accounts import views as accounts_views from boards import views urlpatterns = [ url ( r'^$' , views . home , name = 'home' ), url ( r'^signup/$' , accounts_views . signup , name = 'signup' ), url ( r'^logout/$' , auth_views . LogoutView . as_view (), name = 'logout' ), url ( r'^boards/(?P<pk> \ d+)/$' , views . board_topics , name = 'board_topics' ), url ( r'^boards/(?P<pk> \ d+)/new/$' , views . new_topic , name = 'new_topic' ), url ( r'^admin/' , admin . site . urls ), ]
我们从Django的contrib模块导入了视图 。 我们将其重命名为auth_views以避免与boards.views冲突。 请注意,此视图有点不同: LogoutView.as_view() 。 这是Django基于类的视图。 到目前为止,我们只将视图实现为Python函数。 基于类的视图提供了一种更灵活的方式来扩展和重用视图。 我们稍后会讨论更多这个问题。
打开settings.py文件并将LOGOUT_REDIRECT_URL变量添加到文件的底部:
的myproject / settings.py
LOGOUT_REDIRECT_URL = 'home'
在这里,我们传递了我们想要在注销后重定向用户的URL模式的名称。
在那之后,它已经完成了。 只需访问URL 127.0.0.1:8000/logout/ ,您就会被注销。 但请等一下。 在您注销之前,让我们为登录用户创建下拉菜单。
显示已认证用户的菜单
现在我们需要在base.html模板中进行一些调整。 我们必须添加一个带有注销链接的下拉菜单。
Bootstrap 4下拉组件需要jQuery才能工作。
首先,转到jquery.com/download/并下载压缩的生产jQuery 3.2.1版本。
在静态文件夹中,创建一个名为js的新文件夹。 将jquery-3.2.1.min.js文件复制到那里。
Bootstrap 4还需要一个名为Popper的库才能工作。 转到popper.js.org并下载最新版本。
在popper.js-1.12.5文件夹中,转到dist / umd并将文件popper.min.js复制到我们的js文件夹中。 注意这里; Bootstrap 4仅适用于umd / popper.min.js 。 因此,请确保您正在复制正确的文件。
如果您不再拥有所有Bootstrap 4文件,请从getbootstrap.com再次下载。
同样,将bootstrap.min.js文件复制到我们的js文件夹中。
最终结果应该是:
myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | |-- myproject/ | |-- static/ | | |-- css/ | | +-- js/ | | |-- bootstrap.min.js | | |-- jquery-3.2.1.min.js | | +-- popper.min.js | |-- templates/ | |-- db.sqlite3 | +-- manage.py +-- venv/
在base.html文件的底部,在{ % endblock body % } 之后添加脚本 { % endblock body % } { % endblock body % } :
模板/ base.html文件
{% load static %}<!DOCTYPE html> <html> <head> <meta charset= "utf-8" > <title> {% block title %} Django Boards {% endblock %} </title> <link href= "https://fonts.googleapis.com/css?family=Peralta" rel= "stylesheet" > <link rel= "stylesheet" href= " {% static 'css/bootstrap.min.css' %} " > <link rel= "stylesheet" href= " {% static 'css/app.css' %} " > {% block stylesheet %}{% endblock %} </head> <body> {% block body %} <!-- code suppressed for brevity --> {% endblock body %} <script src= " {% static 'js/jquery-3.2.1.min.js' %} " ></script> <script src= " {% static 'js/popper.min.js' %} " ></script> <script src= " {% static 'js/bootstrap.min.js' %} " ></script> </body> </html>
如果您发现说明令人困惑,只需使用以下直接链接下载文件:
- https://code.jquery.com/jquery-3.2.1.min.js
- https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js
- https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js
右键单击并将*保存链接为…。
现在我们可以添加Bootstrap 4下拉菜单:
模板/ base.html文件
<nav class= "navbar navbar-expand-sm navbar-dark bg-dark" > <div class= "container" > <a class= "navbar-brand" href= " {% url 'home' %} " > Django Boards </a> <button class= "navbar-toggler" type= "button" data-toggle= "collapse" data-target= "#mainMenu" aria-controls= "mainMenu" aria-expanded= "false" aria-label= "Toggle navigation" > <span class= "navbar-toggler-icon" ></span> </button> <div class= "collapse navbar-collapse" id= "mainMenu" > <ul class= "navbar-nav ml-auto" > <li class= "nav-item dropdown" > <a class= "nav-link dropdown-toggle" href= "#" id= "userMenu" data-toggle= "dropdown" aria-haspopup= "true" aria-expanded= "false" > {{ user.username }} </a> <div class= "dropdown-menu dropdown-menu-right" aria-labelledby= "userMenu" > <a class= "dropdown-item" href= "#" > My account </a> <a class= "dropdown-item" href= "#" > Change password </a> <div class= "dropdown-divider" ></div> <a class= "dropdown-item" href= " {% url 'logout' %} " > Log out </a> </div> </li> </ul> </div> </div> </nav>
我们来试试吧。 点击退出:
它的工作原理。 但无论用户是否登录,都会显示下拉列表。 不同的是,现在用户名是空的,我们只能看到一个箭头。
我们可以稍微改进一下:
<nav class= "navbar navbar-expand-sm navbar-dark bg-dark" > <div class= "container" > <a class= "navbar-brand" href= " {% url 'home' %} " > Django Boards </a> <button class= "navbar-toggler" type= "button" data-toggle= "collapse" data-target= "#mainMenu" aria-controls= "mainMenu" aria-expanded= "false" aria-label= "Toggle navigation" > <span class= "navbar-toggler-icon" ></span> </button> <div class= "collapse navbar-collapse" id= "mainMenu" > {% if user.is_authenticated %} <ul class= "navbar-nav ml-auto" > <li class= "nav-item dropdown" > <a class= "nav-link dropdown-toggle" href= "#" id= "userMenu" data-toggle= "dropdown" aria-haspopup= "true" aria-expanded= "false" > {{ user.username }} </a> <div class= "dropdown-menu dropdown-menu-right" aria-labelledby= "userMenu" > <a class= "dropdown-item" href= "#" > My account </a> <a class= "dropdown-item" href= "#" > Change password </a> <div class= "dropdown-divider" ></div> <a class= "dropdown-item" href= " {% url 'logout' %} " > Log out </a> </div> </li> </ul> {% else %} <form class= "form-inline ml-auto" > <a href= "#" class= "btn btn-outline-secondary" > Log in </a> <a href= " {% url 'signup' %} " class= "btn btn-primary ml-2" > Sign up </a> </form> {% endif %} </div> </div> </nav>
现在我们告诉Django如果用户登录则显示下拉菜单,如果没有,则显示登录和注册按钮:
登录
首先,添加一个新的URL路由:
MyProject的/ urls.py
from django.conf.urls import url from django.contrib import admin from django.contrib.auth import views as auth_views from accounts import views as accounts_views from boards import views urlpatterns = [ url ( r'^$' , views . home , name = 'home' ), url ( r'^signup/$' , accounts_views . signup , name = 'signup' ), url ( r'^login/$' , auth_views . LoginView . as_view ( template_name = 'login.html' ), name = 'login' ), url ( r'^logout/$' , auth_views . LogoutView . as_view (), name = 'logout' ), url ( r'^boards/(?P<pk> \ d+)/$' , views . board_topics , name = 'board_topics' ), url ( r'^boards/(?P<pk> \ d+)/new/$' , views . new_topic , name = 'new_topic' ), url ( r'^admin/' , admin . site . urls ), ]
在as_view()我们可以传递一些额外的参数,以覆盖默认值。 在这种情况下,我们指示LoginView在login.html上查找模板。
编辑settings.py并添加以下配置:
的myproject / settings.py
LOGIN_REDIRECT_URL = 'home'
此配置告诉Django在成功登录后重定向用户的位置。
最后,将登录URL添加到base.html模板:
模板/ base.html文件
<a href= " {% url 'login' %} " class= "btn btn-outline-secondary" > Log in </a>
我们可以创建类似于注册页面的模板。 创建一个名为login.html的新文件:
模板/ login.html的
{% extends 'base.html' %} {% load static %} {% block stylesheet %} <link rel= "stylesheet" href= " {% static 'css/accounts.css' %} " > {% endblock %} {% block body %} <div class= "container" > <h1 class= "text-center logo my-4" > <a href= " {% url 'home' %} " > Django Boards </a> </h1> <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" > Log in </h3> <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-primary btn-block" > Log in </button> </form> </div> <div class= "card-footer text-muted text-center" > New to Django Boards? <a href= " {% url 'signup' %} " > Sign up </a> </div> </div> <div class= "text-center py-2" > <small> <a href= "#" class= "text-muted" > Forgot your password? </a> </small> </div> </div> </div> </div> {% endblock %}
我们正在重复HTML模板。 让我们重构一下吧。
创建名为base_accounts.html的新模板:
模板/ base_accounts.html
{% extends 'base.html' %} {% load static %} {% block stylesheet %} <link rel= "stylesheet" href= " {% static 'css/accounts.css' %} " > {% endblock %} {% block body %} <div class= "container" > <h1 class= "text-center logo my-4" > <a href= " {% url 'home' %} " > Django Boards </a> </h1> {% block content %} {% endblock %} </div> {% endblock %}
现在在signup.html和login.html上使用它:
模板/ login.html的
{% extends 'base_accounts.html' %} {% block title %} Log in to Django Boards {% 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" > Log in </h3> <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-primary btn-block" > Log in </button> </form> </div> <div class= "card-footer text-muted text-center" > New to Django Boards? <a href= " {% url 'signup' %} " > Sign up </a> </div> </div> <div class= "text-center py-2" > <small> <a href= "#" class= "text-muted" > Forgot your password? </a> </small> </div> </div> </div> {% endblock %}
我们仍然没有密码重置URL,所以我们现在将其保留为# 。
模板/ signup.html
{% extends 'base_accounts.html' %} {% block title %} Sign up to Django Boards {% endblock %} {% block content %} <div class= "row justify-content-center" > <div class= "col-lg-8 col-md-10 col-sm-12" > <div class= "card" > <div class= "card-body" > <h3 class= "card-title" > Sign up </h3> <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-primary btn-block" > Create an account </button> </form> </div> <div class= "card-footer text-muted text-center" > Already have an account? <a href= " {% url 'login' %} " > Log in </a> </div> </div> </div> </div> {% endblock %}
请注意,我们添加了登录网址: <a href=”{% url ‘login’ %}”>Log in</a> 。
登录非字段错误
如果我们将表单提交为空,我们会得到一些很好的错误消息:
但是,如果我们提交的用户名不存在或密码无效,那么现在将会发生什么:
有点误导。 田地显示绿色,表明它们没问题。 此外,没有任何消息说什么。
这是因为表单有一种特殊类型的错误,称为非字段错误 。 它是与特定字段无关的错误集合。 让我们重构form.html部分模板以显示这些错误:
模板/包括/ form.html
{% load widget_tweaks %} {% if form.non_field_errors %} <div class= "alert alert-danger" role= "alert" > {% for error in form.non_field_errors %} <p {% if forloop.last %} class= "mb-0" {% endif %} > {{ error }} </p> {% endfor %} </div> {% endif %} {% for field in form %} <!-- code suppressed --> {% endfor %}
{ % if forloop.last % } { % if forloop.last % } { % if forloop.last % }只是一件小事。 因为p标签有一个margin-bottom 。 表单可能有几个非字段错误。 对于每个非字段错误,我们使用错误呈现p标记。 然后我正在检查它是否是渲染的最后一个错误。 如果是这样,我们添加一个Bootstrap 4 CSS类mb-0代表“margin bottom = 0”。 然后警报看起来并不奇怪,有一些额外的空间。 再次,只是一个非常小的细节。 我这样做只是为了保持间距的一致性。
我们仍然需要处理密码字段。 问题是,Django从未将密码字段的数据返回给客户端。所以,不要试图做一些聪明的事情,而是在某些情况下忽略is-valid和is-invalidCSS类。但我们的表单模板看起来很复杂。我们可以将一些代码移动到模板标记。
创建自定义模板标签
在board app中,创建一个名为templatetags的新文件夹。然后在此文件夹中创建两个名为__init__.py和form_tags.py的空文件。
结构应如下:
myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | | |-- migrations/ | | |-- templatetags/ <-- here | | | |-- __init__.py | | | +-- form_tags.py | | |-- __init__.py | | |-- admin.py | | |-- apps.py | | |-- models.py | | |-- tests.py | | +-- views.py | |-- myproject/ | |-- static/ | |-- templates/ | |-- db.sqlite3 | +-- manage.py +-- venv/
在form_tags.py文件中,让我们创建两个模板标记:
板/ templatetags / form_tags.py
from django import template register = template . Library () @register.filter def field_type ( bound_field ): return bound_field . field . widget . __class__ . __name__ @register.filter def input_class ( bound_field ): css_class = '' if bound_field . form . is_bound : if bound_field . errors : css_class = 'is-invalid' elif field_type ( bound_field ) != 'PasswordInput' : css_class = 'is-valid' return 'form-control {}' . format ( css_class )
这些是模板过滤器。他们的工作方式如下:
首先,我们将其加载到模板中,就像使用widget_tweaks或静态模板标记一样。请注意,创建此文件后,您必须手动停止开发服务器并再次启动它,以便Django可以识别新的模板标记。
{% load form_tags %}
然后,我们可以在模板中使用它们:
{{ form.username | field_type }}
将返回:
'TextInput'
或者在input_class的情况下:
{{ form.username | input_class }} <!-- if the form is not bound, it will simply return: --> 'form-control ' <!-- if the form is bound and valid: --> 'form-control is-valid' <!-- if the form is bound and invalid: --> 'form-control is-invalid'
现在更新form.html以使用新模板标记:
模板/包括/ form.html
{% load form_tags widget_tweaks %} {% if form.non_field_errors %} <div class= "alert alert-danger" role= "alert" > {% for error in form.non_field_errors %} <p {% if forloop.last %} class= "mb-0" {% endif %} > {{ error }} </p> {% endfor %} </div> {% endif %} {% for field in form %} <div class= "form-group" > {{ field.label_tag }} {% render_field field class = field | input_class %} {% for error in field.errors %} <div class= "invalid-feedback" > {{ error }} </div> {% endfor %} {% if field.help_text %} <small class= "form-text text-muted" > {{ field.help_text | safe }} </small> {% endif %} </div> {% endfor %}
好多了,对吧?降低了模板的复杂性。现在看起来更干净。它还解决了显示绿色边框的密码字段的问题:
测试模板标签
首先,让我们稍微组织一下董事会的测试。就像我们对帐户应用程序所做的那样,创建一个名为tests的新文件夹,添加__init__.py,复制tests.py并立即将其重命名为test_views.py。
添加一个名为test_templatetags.py的新空文件。
myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | | |-- migrations/ | | |-- templatetags/ | | |-- tests/ | | | |-- __init__.py | | | |-- test_templatetags.py <-- new file, empty for now | | | +-- test_views.py <-- our old file with all the tests | | |-- __init__.py | | |-- admin.py | | |-- apps.py | | |-- models.py | | +-- views.py | |-- myproject/ | |-- static/ | |-- templates/ | |-- db.sqlite3 | +-- manage.py +-- venv/
修复test_views.py导入:
板/测试/ test_views.py
from ..views import home , board_topics , new_topic from ..models import Board , Topic , Post from ..forms import NewTopicForm
执行测试只是为了确保一切正常。
板/测试/ test_templatetags.py
from django import forms from django.test import TestCase from ..templatetags.form_tags import field_type , input_class class ExampleForm ( forms . Form ): name = forms . CharField () password = forms . CharField ( widget = forms . PasswordInput ()) class Meta : fields = ( 'name' , 'password' ) class FieldTypeTests ( TestCase ): def test_field_widget_type ( self ): form = ExampleForm () self . assertEquals ( 'TextInput' , field_type ( form [ 'name' ])) self . assertEquals ( 'PasswordInput' , field_type ( form [ 'password' ])) class InputClassTests ( TestCase ): def test_unbound_field_initial_state ( self ): form = ExampleForm () # unbound form self . assertEquals ( 'form-control ' , input_class ( form [ 'name' ])) def test_valid_bound_field ( self ): form = ExampleForm ({ 'name' : 'john' , 'password' : '123' }) # bound form (field + data) self . assertEquals ( 'form-control is-valid' , input_class ( form [ 'name' ])) self . assertEquals ( 'form-control ' , input_class ( form [ 'password' ])) def test_invalid_bound_field ( self ): form = ExampleForm ({ 'name' : '' , 'password' : '123' }) # bound form (field + data) self . assertEquals ( 'form-control is-invalid' , input_class ( form [ 'name' ]))
我们创建了一个要在测试中使用的表单类,然后添加了涵盖两个模板标记中可能的场景的测试用例。
python manage.py test
Creating test database for alias 'default'... System check identified no issues (0 silenced). ................................ ---------------------------------------------------------------------- Ran 32 tests in 0.846s OK Destroying test database for alias 'default'...
重设密码
密码重置过程涉及一些令人讨厌的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.html
- password_reset_email.html:此模板是发送给用户的电子邮件正文
- password_reset_subject.txt:此模板是电子邮件的主题行,它应该是单行文件
- password_reset_done.html
- password_reset_confirm.html
- password_reset_complete.html
在我们开始实现模板之前,让我们准备一个新的测试文件。
我们可以添加一些基本测试,因为这些视图和表单已经在Django代码中进行了测试。我们将测试我们的应用程序的具体细节。
在accounts / tests文件夹中创建名为test_view_password_reset.py的新测试文件。
密码重置视图
模板/ 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 ))
模板/ password_reset_subject.txt
[Django Boards] Please reset your password
模板/ 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 )
此测试用例抓取应用程序发送的电子邮件,并检查主题行,正文内容以及发送给谁的电子邮件。
密码重置完成视图
模板/ 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 )
密码重置确认视图
模板/ 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-b5f2d47c19966e294a1 /
在开发阶段,从控制台中的电子邮件中获取此链接。
如果链接有效:
或者如果链接已被使用:
账户/测试/ 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 ))
密码重置完整视图
模板/ 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 %}
accounts / tests / 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 )
密码更改视图
此视图旨在供要更改其密码的登录用户使用。通常,这些表单由三个字段组成:旧密码,新密码和新密码确认。
myproject / urls.py (查看完整的文件内容)
url ( r'^settings/password/$' , auth_views . PasswordChangeView . as_view ( template_name = 'password_change.html' ), name = 'password_change' ), url ( r'^settings/password/done/$' , auth_views . PasswordChangeDoneView . as_view ( template_name = 'password_change_done.html' ), name = 'password_change_done' ),
这些视图仅适用于登录用户。他们使用名为的视图装饰器@login_required。此装饰器可防止未经授权的用户访问此页面。如果用户未登录,Django会将其重定向到登录页面。
现在我们必须在settings.py中定义应用程序的登录URL :
myproject / settings.py (查看完整的文件内容)
LOGIN_URL = 'login'
模板/ password_change.html
{% extends 'base.html' %} {% block title %} Change password {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item active" > Change password </li> {% endblock %} {% block content %} <div class= "row" > <div class= "col-lg-6 col-md-8 col-sm-10" > <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-success" > Change password </button> </form> </div> </div> {% endblock %}
模板/ password_change_done.html
{% extends 'base.html' %} {% block title %} Change password successful {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'password_change' %} " > Change password </a></li> <li class= "breadcrumb-item active" > Success </li> {% endblock %} {% block content %} <div class= "alert alert-success" role= "alert" > <strong> Success! </strong> Your password has been changed! </div> <a href= " {% url 'home' %} " class= "btn btn-secondary" > Return to home page </a> {% endblock %}
关于密码更改视图,我们可以实现与目前为止已经执行的类似测试用例。创建一个名为test_view_password_change.py的新测试文件。
我将在下面列出新类型的测试。您可以在代码片段旁边的视图完整文件内容链接中查看我为密码更改视图编写的所有测试。大多数测试与我们迄今为止所做的相似。我移动到外部文件以避免过于重复。
accounts / tests / test_view_password_change.py (查看完整的文件内容)
class LoginRequiredPasswordChangeTests ( TestCase ): def test_redirection ( self ): url = reverse ( 'password_change' ) login_url = reverse ( 'login' ) response = self . client . get ( url ) self . assertRedirects ( response , f '{login_url}?next={url}' )
上面的测试尝试在不登录的情况下访问password_change视图。预期的行为是将用户重定向到登录页面。
accounts / tests / test_view_password_change.py (查看完整的文件内容)
class PasswordChangeTestCase ( TestCase ): def setUp ( self , data = {}): self . user = User . objects . create_user ( username = 'john' , email = 'john@doe.com' , password = 'old_password' ) self . url = reverse ( 'password_change' ) self . client . login ( username = 'john' , password = 'old_password' ) self . response = self . client . post ( self . url , data )
这里我们定义了一个名为PasswordChangeTestCase的新类。它执行基本设置,创建用户并向password_change视图发出POST请求。在下一组测试用例中,我们将使用此类而不是TestCase类来测试成功的请求和无效的请求:
accounts / tests / test_view_password_change.py (查看完整的文件内容)
class SuccessfulPasswordChangeTests ( PasswordChangeTestCase ): def setUp ( self ): super () . setUp ({ 'old_password' : 'old_password' , 'new_password1' : 'new_password' , 'new_password2' : 'new_password' , }) def test_redirection ( self ): ''' A valid form submission should redirect the user ''' self . assertRedirects ( self . response , reverse ( 'password_change_done' )) def test_password_changed ( self ): ''' refresh the user instance from database to get the new password hash updated by the change password view. ''' self . user . refresh_from_db () self . assertTrue ( self . user . check_password ( 'new_password' )) def test_user_authentication ( self ): ''' Create a new request to an arbitrary page. The resulting response should now have an `user` to its context, after a successful sign up. ''' response = self . client . get ( reverse ( 'home' )) user = response . context . get ( 'user' ) self . assertTrue ( user . is_authenticated ) class InvalidPasswordChangeTests ( PasswordChangeTestCase ): def test_status_code ( self ): ''' An invalid form submission should return to the same page ''' self . assertEquals ( self . response . status_code , 200 ) def test_form_errors ( self ): form = self . response . context . get ( 'form' ) self . assertTrue ( form . errors ) def test_didnt_change_password ( self ): ''' refresh the user instance from the database to make sure we have the latest data. ''' self . user . refresh_from_db () self . assertTrue ( self . user . check_password ( 'old_password' ))
该refresh_from_db()方法确保我们拥有最新的数据状态。它强制Django再次查询数据库以更新数据。我们必须这样做,因为change_password视图更新了数据库中的密码。因此,为了测试密码是否真的改变了,我们必须从数据库中获取最新数据。
结论
对于大多数Django应用程序,身份验证是一种非常常见的用例。在本教程中,我们实现了所有重要的视图:注册,登录,注销,密码重置和更改密码。现在我们有了创建用户并对其进行身份验证的方法,我们将能够继续开发应用程序的其他视图。
我们仍然需要改进很多关于代码设计的东西:模板文件夹开始变得混乱太多的文件。该板应用的测试仍然杂乱无章。此外,我们必须开始重构新主题视图,因为现在我们可以检索登录用户。我们很快就会到达那一部分。
我希望你喜欢本教程系列的第四部分!第五部分将于2017年10月2日下周发布。如果您希望在第五部分结束时收到通知,您可以订阅我们的邮件列表。
该项目的源代码可在GitHub上获得。可以在发布标记v0.4-lw下找到项目的当前状态。以下链接将带您到正确的位置:
https://github.com/sibtc/django-beginners-guide/tree/v0.4-lw