1. 程式人生 > >完整的Django入門指南學習筆記4

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

前言

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

同時你還會了解到如何保護某些試圖以防未授權的使用者訪問,以及如何訪問已登入使用者的個人資訊。

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

線框圖

我們必須更新一下應用的線框圖。首先,我們需要在頂部選單新增一些新選項,如果使用者未通過身份驗證,應該有兩個按鈕:分別是註冊和登入按鈕。

如果使用者已經通過身份認證,我們應該顯示他們的名字,和帶有“我的賬戶”,“修改密碼”,“登出”這三個選項的下拉框

在登入頁面,我們需要一個帶有username和password的表單, 一個登入的按鈕和可跳轉到註冊頁面和密碼重置頁面的連結。

在註冊頁面,我們應該有包含四個欄位的表單:username,email address, password和password confirmation。同時,也應該有一個能夠訪問登入頁面連結。

在密碼重置頁面上,只有email address欄位的表單。

之後,使用者在點選帶有特殊token的重置密碼連結以後,使用者將被重定向到一個頁面,在那裡他們可以設定新的密碼。


初始設定

要管理這些功能,我們可以在另一個應用(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': '[email protected]', '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

f