完整的Django入門指南學習筆記4

前言

這一章節將會全面介紹 Django 的身份認證系統,咱們將實現註冊、登陸、註銷、密碼重置和密碼修改的整套流程。css

同時你還會了解到如何保護某些試圖以防未受權的用戶訪問,以及如何訪問已登陸用戶的我的信息。html

在接下來的部分,你會看到一些和身份驗證有關線框圖,將在本教程中實現。以後是一個全新Django 應用的初始化設置。至今爲止咱們一直在一個名叫 boards 的應用中開發。不過,全部身份認證相關的內容都將在另外一個應用中,這樣能更良好的組織代碼。 python

線框圖

咱們必須更新一下應用的線框圖。首先,咱們須要在頂部菜單添加一些新選項,若是用戶未經過身份驗證,應該有兩個按鈕:分別是註冊和登陸按鈕。jquery

若是用戶已經經過身份認證,咱們應該顯示他們的名字,和帶有「個人帳戶」,「修改密碼」,「登出」這三個選項的下拉框git

在登陸頁面,咱們須要一個帶有username和password的表單, 一個登陸的按鈕和可跳轉到註冊頁面和密碼重置頁面的連接。github

在註冊頁面,咱們應該有包含四個字段的表單:username,email address, password和password confirmation。同時,也應該有一個可以訪問登陸頁面連接。ajax

在密碼重置頁面上,只有email address字段的表單。正則表達式

以後,用戶在點擊帶有特殊token的重置密碼連接之後,用戶將被重定向到一個頁面,在那裏他們能夠設置新的密碼。sql


初始設置

要管理這些功能,咱們能夠在另外一個應用(app)中將其拆解。在項目根目錄中的 manage.py 文件所在的同一目錄下,運行如下命令以建立一個新的app:數據庫

django-admin startapp accounts

項目的目錄結構應該以下:

myproject/
 |-- myproject/
 |    |-- accounts/     <-- 新建立的app
 |    |-- boards/
 |    |-- myproject/
 |    |-- static/
 |    |-- templates/
 |    |-- db.sqlite3
 |    +-- manage.py
 +-- venv/

下一步,在 settings.py 文件中將 accounts app 添加到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',
]

如今開始,咱們將會在 accounts 這個app下操做。


註冊

咱們從建立註冊視圖開始。首先,在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), ] 

注意,咱們以不一樣的方式從accounts app 導入了views模塊

from accounts import views as accounts_views 

咱們給 accounts 的 views 指定了別名,不然它會與boards 的views 模塊發生衝突。稍後咱們能夠改進urls.py 的設計,但如今,咱們只關注身份驗證功能。

如今,咱們在 accounts app 中編輯 views.py,新建立一個名爲signup的視圖函數:

accounts/views.py

from django.shortcuts import render def signup(request): return render(request, 'signup.html') 

接着建立一個新的模板,取名爲signup.html:

templates/signup.html

{% extends 'base.html' %} {% block content %} <h2>Sign up</h2> {% endblock %} 

在瀏覽器中打開 http://127.0.0.1:8000/signup/ ,看看是否程序運行了起來:

接下來寫點測試用例:

accounts/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=success)以及 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'...

對於認證視圖(註冊、登陸、密碼重置等),咱們不須要頂部條和breadcrumb導航欄,但仍然可以複用base.html 模板,不過咱們須要對它作出一些修改,只須要微調:

templates/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 %} <!-- 這裏 --> <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 %} <!-- 這裏 --> </body> </html> 

我在 base.html 模板中標註了註釋,表示新加的代碼。塊代碼{% block stylesheet %}{% endblock %} 表示添加一些額外的CSS,用於某些特定的頁面。

代碼塊{% block body %} 包裝了整個HTML文檔。咱們能夠只有一個空的文檔結構,以充分利用base.html頭部。注意,還有一個結束的代碼塊{% endblock body %},在這種狀況下,命名結束標籤是一種很好的實踐方法,這樣更容易肯定結束標記的位置。

如今,在signup.html模板中,咱們使用{% block body %}代替了 {% block content %}

templates/signup.html

{% extends 'base.html' %} {% block body %} <h2>Sign up</h2> {% endblock %} 

是時候建立註冊表單了。Django有一個名爲 UserCreationForm的內置表單,咱們就使用它吧:

accounts/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}) 

templates/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模板使它看起來更好:

templates/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出於安全考慮的特性。在默認的狀況下,Django將全部字符串視爲不安全的,會轉義全部可能致使問題的特殊字符。但在這種狀況下,咱們能夠信任它。

templates/includes/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 }} <!-- 新的代碼 --> </small> {% endif %} </div> {% endfor %} 

咱們主要在以前的模板中,將選項safe 添加到field.help_text{{ field.help_text|safe }}.

保存form.html文件,而後再次檢測註冊頁面:

如今,讓咱們在signup視圖中實現業務邏輯:

accounts/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}) 

表單處理有一個小細節:login函數重命名爲auth_login以免與內置login視圖衝突)。

(編者注:我重命名了login 函數重命名爲auth_login ,但後來我意識到Django1.11對登陸視圖LoginView具備基於類的視圖,所以不存在與名稱衝突的風險。在比較舊的版本中,有一個auth.loginauth.view.login ,這會致使一些混淆,由於一個是用戶登陸的功能,另外一個是視圖。

簡單來講:若是你願意,你能夠像login 同樣導入它,這樣作不會形成任何問題。)

若是表單是有效的,那麼咱們經過user=form.save()建立一個User實例。而後將建立的用戶做爲參數傳遞給auth_login函數,手動驗證用戶。以後,視圖將用戶重定向到主頁,保持應用程序的流程。

讓咱們來試試吧,首先,提交一些無效數據,不管是空表單,不匹配的字段仍是已有的用戶名。

如今填寫表單並提交,檢查用戶是否已建立並重定向到主頁。

在模板中引用已認證的用戶

咱們要怎麼才能知道上述操做是否有效呢?咱們能夠編輯base.html模板來在頂部欄上添加用戶名稱:

templates/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 %} 

測試註冊視圖

咱們來改進測試用例:

accounts/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) 

咱們稍微改變了SighUpTests類,定義了一個setUp方法,將response對象移到那裏,如今咱們測試響應中是否有表單和CSRF token。

如今咱們要測試一個成功的註冊功能。此次,讓咱們來建立一個新類,以便於更好地組織測試。

accounts/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()) 

將Email字段添加到表單

一切都正常,但還缺失 email address字段。UserCreationForm不提供 email 字段,可是咱們能夠對它進行擴展。

在accounts 文件夾中建立一個名爲forms.py的文件:

accounts/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') 

如今,咱們不須要在views.py 中使用UserCreationForm,而是導入新的表單SignUpForm,而後使用它:

accounts/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輸入:

accounts/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 的空文件。

如今,將test.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中修復導入:

accounts/tests/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 

咱們在應用程序模塊內部使用相對導入,以便咱們能夠自由地從新命名Django應用程序,而無需修復全部絕對導入。

如今讓咱們建立一個新的測試文件來測試SignUpForm,添加一個名爲test_form_signup.py的新測試文件:

accounts/tests/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,以包含用戶的名字和姓氏,那麼即便咱們沒有破壞任何東西,咱們也可能最終不得不修復一些測試用例。

這些警報頗有用,由於它們有助於提升認識,特別是新手第一次接觸代碼,它能夠幫助他們自信地編碼。

改進註冊模板

讓咱們稍微討論一下,在這裏,咱們可使用Bootstrap4 組件來使它看起來不錯。

訪問: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這個文件:

static/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,並使用Bootstrap4組件:

templates/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模塊導入了views ,咱們將其改名爲auth_views 以免與boards.views發生衝突。注意這個視圖有點不一樣: LogoutView.as_view()。這是一個Django的「基於類」的視圖,到目前爲止,咱們只將類實現爲python函數。基於類的視圖提供了一種更加靈活的方式來擴展和重用視圖。稍後咱們將討論更多這個主題。

打開settings.py文件,而後添加LOGOUT_REDIRECT_URL變量到文件的底部:

myproject/settings.py

LOGOUT_REDIRECT_URL = 'home'

在這裏咱們給變量指定了一個URL模型的名稱,以告訴Django當用戶退出登陸以後跳轉的地址。

在這以後,此次重定向就算完成了。只須要訪問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文件複製到那裏。

Bootstrap4還須要一個名爲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 %}後面添加腳本:

templates/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> 

若是你發現上面的說明很模糊,只須要直接在下面的連接下載文件

打開連接,右鍵另存爲

如今咱們能夠添加Bootstrap4下拉菜單了:

templates/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模板中:

templates/base.html

<a href="{% url 'login' %}" class="btn btn-outline-secondary">Log in</a> 

咱們能夠建立一個相似於註冊頁面的模板。建立一個名爲 login.html 的新文件:

templates/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的新模板:

templates/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中使用它:

templates/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 %} 

咱們有密碼重置的功能,所以如今讓咱們將其暫時保留爲#

templates/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>.

無登陸信息錯誤

若是咱們提交空白的登陸信息,咱們會獲得一些友好的錯誤提示信息:

可是,若是咱們提交一個不存在的用戶名或一個無效的密碼,如今就會發生這種狀況:

有點誤導,這個區域是綠色的,代表它們是良好運行的,此外,沒有其餘額外的信息。

這是由於表單有一種特殊類型的錯誤,叫作 non-field errors。這是一組與特定字段無關的錯誤。讓咱們重構form.html部分模板以顯示這些錯誤:

templates/includes/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 %}只是一個小事情,由於p標籤有一個空白的margin-bottom.一個表單可能有幾個non-field error,咱們呈現了一個帶有錯誤的p標籤。而後我要檢查它是不是最後一次渲染的錯誤。若是是這樣的,咱們就添加一個 Bootstrap 4 CSS類 mb-0 ,它的做用是表明了「margin bottom = 0」(底部邊緣爲0)。這樣的話警告看起來就不那麼奇怪了而且多了一些額外的空間。這只是一個很是小的細節。我這麼作的緣由只是爲了保持間距的一致性。

儘管如此,咱們仍然須要處理密碼字段。問題在於,Django從不將密碼字段的數據返回給客戶端。所以,在某些狀況下,不要試圖作一次自做聰明的事情,咱們能夠直接忽略is-validis-invalid 的CSS類。可是咱們的表單模板看起來十分的複雜,咱們能夠將一些代碼移動到模板標記中去。

建立自定義模板標籤

在boards應用中,建立一個名爲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文件中,咱們建立兩個模板標籤:

boards/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 或static 模板標籤同樣。請注意,在建立這個文件後,你將不得不手動中止開發服務器並重啓它,以便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以使用新的模板標籤:

templates/includes/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 %} 

這樣的話就好多了是吧?這樣作下降了模板的複雜性,它如今看起來更加整潔。而且它還解決了密碼字段顯示綠色邊框的問題:

測試模板標籤

首先,讓咱們稍微組織一下boards的測試。就像咱們對account app 所作的那樣。建立一個新的文件夾名爲tests,添加一個init.py,複製test.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的導入問題:

boards/tests/test_views.py

from ..views import home, board_topics, new_topic from ..models import Board, Topic, Post from ..forms import NewTopicForm 

執行測試來確保一切都正常。

boards/tests/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的調試工具檢查電子郵件是否正確發送。

控制檯收發Email

這個主意來自於項目開發過程當中,而不是發送真實的電子郵件,咱們只須要記錄它們。咱們有兩種選擇:將全部電子郵件寫入文本文件或僅將其顯示在控制檯中。我發現第二個方式更加方便,由於咱們已經在使用控制檯來運行開發服務器,而且設置更容易一些。

編輯 settings.py模塊並將EMAIL_BACKEND變量添加到文件的末尾。

myproject/settings.py

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

配置路由

密碼重置過程須要四個視圖:

  • 帶有表單的頁面,用於啓動重置過程;
  • 一個成功的頁面,表示該過程已啓動,指示用戶檢查其郵件文件夾等;
  • 檢查經過電子郵件發送token的頁面
  • 一個告訴用戶重置是否成功的頁面

這些視圖是內置的,咱們不須要執行任何操做,咱們所須要作的就是將路徑添加到 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 的新測試文件。

密碼重置視圖

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 %} 

accounts/tests/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 password 

templates/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的新文件:

accounts/tests/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 %} 

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 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/

在開發階段,從控制檯中的電子郵件獲取此連接。

若是連接是有效的:

假若連接已經被使用:

accounts/tests/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 %} 

accounts/tests/test_view_password_reset.py (view complete file contents)

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 (view complete file contents)

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 (view complete file contents)

LOGIN_URL = 'login'

templates/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 %} 

templates/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 (view complete file contents)

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 (view complete file contents)

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 (view complete file contents)

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應用程序,身份驗證是一種很是常見的用例。在本教程中,咱們實現了全部重要視圖:註冊、登陸、註銷、密碼重置和更改密碼。如今咱們有了一種方法來建立用戶並進行身份驗證,咱們將可以繼續開發應用程序和其餘視圖。

咱們仍然須要改進不少關於代碼設計的問題:模板文件夾開始變得亂七八糟。 boards 應用測試仍然是混亂的。此外,咱們必須開始重構新的主題視圖,由於如今咱們能夠檢索登陸的用戶。咱們很快就將作到這一點。

我但願你喜歡本教程系列的第四部分!第五部分將於2017年10月2日下週發佈,若是您但願在第五部分結束的時候收到經過,請您訂閱咱們的郵件列表。

該項目的源代碼在GitHub上面可用,項目的當前狀態在發佈標籤v0.4-lw下能夠找到。連接以下:

https://github.com/sibtc/django-beginners-guide/tree/v0.4-lw

相關文章
相關標籤/搜索