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

前言

在本節課中,咱們將深刻理解兩個基本概念: URLs 和 Forms。在這個過程當中,咱們還將學習其它不少概念,如建立可重用模板和安裝第三方庫。同時咱們還將編寫大量單元測試。css

若是你是從這個系列教程的 part 1 跟着這個教程一步步地編寫項目,你可能須要在開始以前更新 models.py:html

boards/models.py前端

class Topic(models.Model): # other fields... # Add `auto_now_add=True` to the `last_updated` field last_updated = models.DateTimeField(auto_now_add=True) class Post(models.Model): # other fields... # Add `null=True` to the `updated_by` field updated_by = models.ForeignKey(User, null=True, related_name='+') 

如今在已經激活的 virtualenv 環境中執行命令:python

python manage.py makemigrations
python manage.py migrate

若是在你的程序中 update_by 字段中已經有了 null=True 且 last_updated 字段中有了 auto_now_add=True,你能夠放心地忽略上面這步操做。git

若是你更喜歡使用個人代碼做爲出發點,你能夠在 GitHub 上找到它。本項目如今的代碼,能夠在 v0.2-lw 標籤下找到。下面是連接:程序員

https://github.com/sibtc/django-beginners-guide/tree/v0.2-lwgithub

咱們的開發就從這裏開始。web

URLs

隨着咱們項目的開發,咱們須要實現一個新的功能,就是列出某個板塊下的全部主題列表,再來回顧一下,你能夠看到上一節中咱們畫的線框圖。正則表達式

咱們將從 myproject 目錄中編寫 urls.py 開始:數據庫

myproject/urls.py

from django.conf.urls import url from django.contrib import admin from boards import views urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'), url(r'^admin/', admin.site.urls), ] 

如今咱們花點時間來分析一下 urlpatterns 和 url

URL 調度器(dispatcher) 和 URLconf (URL configuration) 是 Django 應用中的基礎部分。在開始的時候,這個看起來讓人很困惑;我記得我第一次開始使用 Django 開發的時候也有一段時間學起來很困難。

事實上,Django開發團隊正在致力於將路由語法簡化(譯註:就是將原來url函數替換成 path 函數,目前django2.0已經正式使用新的路由語法)

一個項目能夠有不少 urls.py 分佈在多個應用(app)中。Django 須要一個 url.py 做爲入口。這個特殊的 urls.py 叫作 根路由配置(root URLconf)。它被定義在 settings.py 中。

myproject/settings.py

ROOT_URLCONF = 'myproject.urls'

它已經自動配置好了,你不須要去改變它任何東西。

當 Django 接受一個請求(request), 它就會在項目的 URLconf 中尋找匹配項。他從 urlpatterns 變量的第一條開始,而後在每一個 url 中去匹配請求的 URL。

若是 Django 找到了一個匹配路徑,他會把請求(request)發送給 url 的第二個參數 視圖函數(view function)。urlpatterns 中的順序很重要,由於 Django 一旦找到匹配就會中止日後搜索。若是 Django 在 URLconf 中沒有找到匹配項,他會經過 Page Not Found 的錯誤處理代碼拋出一個 404 異常。

這是 url 函數的剖析:

def url(regex, view, kwargs=None, name=None): # ... 
  • regex: 匹配 URL patterns 的正則表達式。注意:正則表達式會忽略掉 GET 或者 POST 後面的參數。在一個 http://127.0.0.1:8000/boards/?page=2 的請求中,只有 /boards/ 會被處理。
  • view: 視圖函數被用來處理用戶請求,同時它還能夠是 django.conf.urls.include 函數的返回值,它將引用一個外部的urls.py文件,例如,你可使用它來定義一組特定於應用的 URLs,使用前綴將其包含在根 URLconf 中。咱們會在後面繼續探討這個概念。
  • kwargs:傳遞給目標視圖函數的任意關鍵字參數,它一般用於在可重用視圖上進行一些簡單的定製,咱們不是常用它。
  • name:: 該 URL 的惟一標識符。這是一個很是重要的特徵。要始終記得爲你的 URLs 命名。因此,很重要的一點是:不要在 views(視圖) 或者 templates(模板) 中硬編碼 URL,而是經過它的名字去引用 URL。

基礎 URLs 路由

基礎URL建立起來很容易。就只是個匹配字符串的問題。好比說,咱們想建立一個 "about" 頁面,能夠這樣定義:

from django.conf.urls import url from boards import views urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^about/$', views.about, name='about'), ] 

咱們也能夠建立更深層一點的 URL 結構

from django.conf.urls import url from boards import views urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^about/$', views.about, name='about'), url(r'^about/company/$', views.about_company, name='about_company'), url(r'^about/author/$', views.about_author, name='about_author'), url(r'^about/author/vitor/$', views.about_vitor, name='about_vitor'), url(r'^about/author/erica/$', views.about_erica, name='about_erica'), url(r'^privacy/$', views.privacy_policy, name='privacy_policy'), ] 

這是一些簡單的 URL 路由的例子,對於上面全部的例子,視圖函數都是下面這個結構:

def about(request): # do something... return render(request, 'about.html') def about_company(request): # do something else... # return some data along with the view... return render(request, 'about_company.html', {'company_name': 'Simple Complex'}) 

高級 URLs 路由

更高級的URL路由使用方法是經過正則表達式來匹配某些類型的數據並建立動態 URL

例如,要建立一個我的資料的頁面,諸如 github.com/vitorfs or twitter.com/vitorfs(vitorfs 是個人用戶名) 這樣,咱們能夠像如下幾點這樣作:

from django.conf.urls import url from boards import views urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile'), ] 

它會匹配 Django 用戶模型裏面全部有效的用戶名。

如今咱們能夠看到上面的例子是一個很寬鬆的 URL。這意味大量的 URL patterns 都會被它匹配,由於它定義在 URL 的根,而不像 /profile// 這樣。在這種狀況下,若是咱們想定義一個 /about/ 的URL,咱們要把它定義在這個 username URL pattern 的前面:

from django.conf.urls import url from boards import views urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^about/$', views.about, name='about'), url(r'^(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile'), ] 

若是這個 "about" 頁面定義在 username URL pattern 後面,Django 將永遠找不到它,由於 "about" 這個單詞會先被 username 的正則表達式所匹配到,視圖函數 user_profile 將會被執行而不是執行 about

此外,這有一些反作用。例如,從如今開始,咱們要把 "about" 視爲禁止使用的username,由於若是有用戶將 "about" 做爲他們的username,他們將永遠不能看到他們的我的資料頁面,而看到的about頁面。

若是你想給用戶我的主頁設置一個很酷的主頁的URL,那麼避免衝突最簡單的方法是添加一個前綴,例如:/u/vitorfs,或者像 Medium 同樣使用 @ 做爲前綴 /@vitorfs/。

這些 URL 路由的主要思想是當 URL 的一部分被看成某些資源(這些資源用來構成某個頁面)的標識的時候就去建立一個動態頁面。好比說,這個標識能夠是一個整數的 ID 或者是一個字符串。

開始的時候,咱們使用 Board ID 去建立 Topics列表的動態頁面。讓咱們再來看一下我在 URLs 開頭的部分給出的例子:

url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics') 

正則表達式中的 \d+ 會匹配一個任意大小的整數值。這個整數值用來從數據庫中取到指定的 Board。如今注意咱們這樣寫這個正則表達式 (?P<pk>\d+),這是告訴 Django 將捕獲到的值放入名爲 pk 的關鍵字參數中。

這時咱們爲它寫的一個視圖函數:

def board_topics(request, pk): # do something... 

由於咱們使用了 (?P<pk>\d+) 正則表達式,在 board_topics函數中,關鍵字參數必須命名爲 pk。

若是你想在視圖函數使用任意名字的參數,那麼能夠這樣定義:

url(r'^boards/(\d+)/$', views.board_topics, name='board_topics') 

而後在視圖函數能夠這樣定義:

def board_topics(request, board_id): # do something... 

或者這樣:

def board_topics(request, id): # do something... 

名字可有可無,可是使用命名參數是一個很好的作法,由於,當咱們有個更大的URL去捕獲多個 ID 和變量時,這會更便於咱們閱讀。

PK or ID? PK 表示主鍵(Primary key),這是訪問模型的主鍵ID的簡寫方法,全部Django模型都有這個屬性,更多的時候,使用pk屬性和使用id是同樣的,這是由於若是咱們沒有給model定義主鍵時,Django將自動建立一個 AutoField 類型的字段,名字叫作 id,它就是主鍵。 若是你給model定義了一個不一樣的主鍵,例如,假設 email 是你的主鍵,你就能夠這樣訪問:obj.email 或者 obj.pk,兩者是等價的。

使用 URLs API

如今到了寫代碼的時候了。咱們來實現我在開頭提到的主題列表頁面

首先,編輯 urls.py, 添加新的 URL 路由

myproject/urls.py

from django.conf.urls import url from django.contrib import admin from boards import views urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'), url(r'^admin/', admin.site.urls), ] 

如今建立視圖函數 board_topics

boards/views.py

from django.shortcuts import render from .models import Board def home(request): # code suppressed for brevity def board_topics(request, pk): board = Board.objects.get(pk=pk) return render(request, 'topics.html', {'board': board}) 

在 templates 目錄中,建立一個名爲 topics.html 的模板:

templates/topics.html

{% load static %}<!DOCTYPE html>
<html> <head> <meta charset="utf-8"> <title>{{ board.name }}</title> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> </head> <body> <div class="container"> <ol class="breadcrumb my-4"> <li class="breadcrumb-item">Boards</li> <li class="breadcrumb-item active">{{ board.name }}</li> </ol> </div> </body> </html> 

注意:咱們如今只是建立新的 HTML 模板。不用擔憂,在下一節中我會向你展現如何建立可重用模板。

如今在瀏覽器中打開 URL http://127.0.0.1:8000/boards/1/ ,結果應該是下面這個頁面:

如今到了寫一些測試的時候了!編輯 test.py,在文件底部添加下面的測試:

boards/tests.py

from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase from .views import home, board_topics from .models import Board class HomeTests(TestCase): # ... class BoardTopicsTests(TestCase): def setUp(self): Board.objects.create(name='Django', description='Django board.') def test_board_topics_view_success_status_code(self): url = reverse('board_topics', kwargs={'pk': 1}) response = self.client.get(url) self.assertEquals(response.status_code, 200) def test_board_topics_view_not_found_status_code(self): url = reverse('board_topics', kwargs={'pk': 99}) response = self.client.get(url) self.assertEquals(response.status_code, 404) def test_board_topics_url_resolves_board_topics_view(self): view = resolve('/boards/1/') self.assertEquals(view.func, board_topics) 

這裏須要注意幾件事情。此次咱們使用了 setUp 方法。在這個方法中,咱們建立了一個 Board 實例來用於測試。咱們必須這樣作,由於 Django 的測試機制不會針對當前數據庫跑你的測試。運行 Django 測試時會即時建立一個新的數據庫,應用全部的model(模型)遷移 ,運行測試完成後會銷燬這個用於測試的數據庫。

所以在 setUp 方法中,咱們準備了運行測試的環境,用來模擬場景。

  • test_board_topics_view_success_status_code 方法:測試 Django 是否對於現有的 Board 返回 status code(狀態碼) 200(成功)。
  • test_board_topics_view_not_found_status_code 方法:測試 Django 是否對於不存在於數據庫的 Board 返回 status code 404(頁面未找到)。
  • test_board_topics_url_resolves_board_topics_view 方法:測試 Django 是否使用了正確的視圖函數去渲染 topics。

如今來運行一下測試:

python manage.py test

輸出以下:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.E...
======================================================================
ERROR: test_board_topics_view_not_found_status_code (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
# ...
boards.models.DoesNotExist: Board matching query does not exist.

----------------------------------------------------------------------
Ran 5 tests in 0.093s

FAILED (errors=1)
Destroying test database for alias 'default'...

測試 test_board_topics_view_not_found_status_code 失敗。咱們能夠在 Traceback 中看到返回了一個 exception(異常) 「boards.models.DoesNotExist: Board matching query does not exist.」

在 DEBUG=False 的生產環境中,訪問者會看到一個 500 Internal Server Error 的頁面。可是這不是咱們但願獲得的。

咱們想要一個 404 Page Not Found 的頁面。讓咱們來重寫咱們的視圖函數。

boards/views.py

from django.shortcuts import render from django.http import Http404 from .models import Board def home(request): # code suppressed for brevity def board_topics(request, pk): try: board = Board.objects.get(pk=pk) except Board.DoesNotExist: raise Http404 return render(request, 'topics.html', {'board': board}) 

從新測試一下:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.042s

OK
Destroying test database for alias 'default'...

好極了!如今它按照預期工做。

這是 Django 在 DEBUG=False 的狀況下顯示的默認頁面。稍後,咱們能夠自定義 404 頁面去顯示一些其餘的東西。

這是一個常見的用法。事實上, Django 有一個快捷方式去獲得一個對象,或者返回一個不存在的對象 404。

所以讓咱們再來重寫一下 board_topics 函數:

from django.shortcuts import render, get_object_or_404 from .models import Board def home(request): # code suppressed for brevity def board_topics(request, pk): board = get_object_or_404(Board, pk=pk) return render(request, 'topics.html', {'board': board}) 

修改了代碼,測試一下。

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.052s

OK
Destroying test database for alias 'default'...

沒有破壞任何東西。咱們能夠繼續咱們的開發。

下一步是在屏幕上建立一個導航連接。主頁應該有一個連接指引訪問者去訪問指定板塊下面的主題列表頁面。一樣地,topics 頁面也應當有一個返回主頁的連接。

咱們能夠先爲 HomeTests 類編寫一些測試:

boards/test.py

class HomeTests(TestCase): def setUp(self): self.board = Board.objects.create(name='Django', description='Django board.') url = reverse('home') self.response = self.client.get(url) def test_home_view_status_code(self): self.assertEquals(self.response.status_code, 200) def test_home_url_resolves_home_view(self): view = resolve('/') self.assertEquals(view.func, home) def test_home_view_contains_link_to_topics_page(self): board_topics_url = reverse('board_topics', kwargs={'pk': self.board.pk}) self.assertContains(self.response, 'href="{0}"'.format(board_topics_url)) 

注意到如今咱們一樣在 HomeTests 中添加了 setUp 方法。這是由於咱們如今須要一個 Board 實例,而且咱們將 url 和 response 移到了 setUp,因此咱們能在新測試中重用相同的 response。

這裏的新測試是 test_home_view_contains_link_to_topics_page。咱們使用 assertContains 方法來測試 response 主體部分是否包含給定的文本。咱們在測試中使用的文本是 a 標籤的 href 部分。因此基本上咱們是在測試 response 主體是否包含文本 href="/boards/1/"

讓咱們運行這個測試:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....F.
======================================================================
FAIL: test_home_view_contains_link_to_topics_page (boards.tests.HomeTests)
----------------------------------------------------------------------
# ...

AssertionError: False is not true : Couldn't find 'href="/boards/1/"' in response

----------------------------------------------------------------------
Ran 6 tests in 0.034s

FAILED (failures=1)
Destroying test database for alias 'default'...

如今咱們能夠編寫能經過這個測試的代碼。

編寫 home.html 模板:

templates/home.html

<!-- code suppressed for brevity -->
<tbody> {% for board in boards %} <tr> <td> <a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a> <small class="text-muted d-block">{{ board.description }}</small> </td> <td class="align-middle">0</td> <td class="align-middle">0</td> <td></td> </tr> {% endfor %} </tbody> <!-- code suppressed for brevity --> 

咱們只改動了這一行:

{{ board.name }}

變爲:

<a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a> 

始終使用 {% url %} 模板標籤去寫應用的 URL。第一個參數是 URL 的名字(定義在 URLconf, 即 urls.py),而後你能夠根據需求傳遞任意數量的參數。

若是是一個像主頁這種簡單的 URL, 那就是 {% url 'home' %}。 保存文件而後再運行一下測試:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.037s

OK
Destroying test database for alias 'default'...

很棒!如今咱們能夠看到它在瀏覽器是什麼樣子。

如今輪到返回的連接了,咱們能夠先寫測試:

boards/tests.py

class BoardTopicsTests(TestCase): # code suppressed for brevity... def test_board_topics_view_contains_link_back_to_homepage(self): board_topics_url = reverse('board_topics', kwargs={'pk': 1}) response = self.client.get(board_topics_url) homepage_url = reverse('home') self.assertContains(response, 'href="{0}"'.format(homepage_url)) 

運行測試:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F.....
======================================================================
FAIL: test_board_topics_view_contains_link_back_to_homepage (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
# ...

AssertionError: False is not true : Couldn't find 'href="/"' in response

----------------------------------------------------------------------
Ran 7 tests in 0.054s

FAILED (failures=1)
Destroying test database for alias 'default'...

更新主題列表模版:

templates/topics.html

{% load static %}<!DOCTYPE html> <html> <head><!-- code suppressed for brevity --></head> <body> <div class="container"> <ol class="breadcrumb my-4"> <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item active">{{ board.name }}</li> </ol> </div> </body> </html> 

運行測試:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.061s

OK
Destroying test database for alias 'default'...

就如我以前所說的, URL 路由是一個 web 應用程序的基本組成部分。有了這些知識,咱們才能繼續開發。下一步是完成 URL 的部分,你會看到一些使用 URL patterns 的總結。

實用URL模式列表

技巧部分是正則表達式。我準備了一個最經常使用的 URL patterns 的列表。當你須要一個特定的 URL 時你能夠參考這個列表。


主鍵-自增字段

內容 代碼
正則表達式 (?P<pk>\d+)
舉例 url(r'^questions/(?P<pk>\d+)/$', views.question, name='question')
有效 URL /questions/934/
捕獲數據 {'pk': '934'}
____  
Slug 字段  
內容 代碼
正則表達式 (?P<slug>[-\w]+)
舉例 url(r'^posts/(?P<slug>[-\w]+)/$', views.post, name='post')
有效 URL /posts/hello-world/
捕獲數據 {'slug': 'hello-world'}
____  
有主鍵的 Slug 字段  
內容 代碼
正則表達式 (?P<slug>[-\w]+)-(?P<pk>\d+)
舉例 url(r'^blog/(?P<slug>[-\w]+)-(?P<pk>\d+)/$', views.blog_post, name='blog_post')
有效 URL /blog/hello-world-159/
捕獲數據 {'slug': 'hello-world', 'pk': '159'}
____  

Django 用戶名

內容 代碼
正則表達式 (?P<username>[\w.@+-]+)
舉例 url(r'^profile/(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile')
有效 URL /profile/vitorfs/
捕獲數據 {'username': 'vitorfs'}
____  

Year

內容 代碼
正則表達式 (?P<year>[0-9]{4})
舉例 url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive, name='year')
有效 URL /articles/2016/
捕獲數據 {'year': '2016'}
____  

Year / Month

內容 代碼
正則表達式 (?P<year>[0-9]{4})/(?P<month>[0-9]{2})
舉例 url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive, name='month')
有效 URL /articles/2016/01/
捕獲數據 {'year': '2016', 'month': '01'}

你能夠在這篇文章中看到更多關於正則表達式匹配的細節:List of Useful URL Patterns


複用模板

到目前爲止,咱們一直在複製和粘貼 HTML 文檔的多個部分。從長遠來看是不可行的。這也是一個壞的作法。

在這一節咱們將重寫 HTML 模板,建立一個 master page(母版頁),其餘模板添加它所獨特的部分。

在 templates 文件夾中建立一個名爲 base.html 的文件:

templates/base.html

{% load static %}<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>{% block title %}Django Boards{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> </head> <body> <div class="container"> <ol class="breadcrumb my-4"> {% block breadcrumb %} {% endblock %} </ol> {% block content %} {% endblock %} </div> </body> </html> 

這是咱們的母版頁。每一個咱們建立的模板都 extend(繼承) 這個特殊的模板。如今咱們介紹 {% block %} 標籤。它用於在模板中保留一個空間,一個"子"模板(繼承這個母版頁的模板)能夠在這個空間中插入代碼和 HTML。

在 {% block title %} 中咱們還設置了一個默認值 "Django Boards."。若是咱們在子模板中未設置 {% block title %} 的值它就會被使用。

如今讓咱們重寫咱們的兩個模板: home.html 和 topics.html。

templates/home.html

{% extends 'base.html' %} {% block breadcrumb %} <li class="breadcrumb-item active">Boards</li> {% endblock %} {% block content %} <table class="table"> <thead class="thead-inverse"> <tr> <th>Board</th> <th>Posts</th> <th>Topics</th> <th>Last Post</th> </tr> </thead> <tbody> {% for board in boards %} <tr> <td> <a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a> <small class="text-muted d-block">{{ board.description }}</small> </td> <td class="align-middle">0</td> <td class="align-middle">0</td> <td></td> </tr> {% endfor %} </tbody> </table> {% endblock %} 

home.html 的第一行是 {% extends 'base.html' %}。這個標籤用來告訴 Django 使用 base.html 做爲母版頁。以後,咱們使用 blocks 來放置這個頁面獨有的部分。

templates/topics.html

{% extends 'base.html' %} {% block title %} {{ board.name }} - {{ block.super }} {% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item active">{{ board.name }}</li> {% endblock %} {% block content %} <!-- just leaving it empty for now. we will add core here soon. --> {% endblock %} 

在 topics.html 中,咱們改變了 {% block title %} 的默認值。注意咱們能夠經過調用 {{ block.super }} 來重用 block 的默認值。這裏咱們使用的網頁標題是 base.html 中定義的 "Django Boards"。因此對於 "Python" 的 board 頁面,這個標題是 "Python - Django Boards",對於 "Random" board 標題會是 "Random - Django Boards"。

如今咱們運行測試,而後會看到咱們沒有破壞任何東西:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.067s

OK
Destroying test database for alias 'default'...

棒極了!一切看起來都很成功。

如今咱們有了 bast.html 模板,咱們能夠很輕鬆地在頂部添加一個菜單塊:

templates/base.html

{% load static %}<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>{% block title %}Django Boards{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> </head> <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> </body> </html> 

我使用的 HTML 是 Bootstrap 4 Navbar 組件 的一部分。

我喜歡的一個比較好的改動是改變頁面的 "logo"(.navbar-brand)。

前往 fonts.google.com,輸入 "Django Boards" 或者任何你項目給定的名字而後點擊 apply to all fonts(應用於全部字體)。瀏覽一下,找到一個你喜歡的字體。

在 bast.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' %}"> </head> <body> <!-- code suppressed for brevity --> </body> </html> 

如今在 static/css 文件夾中建立一個新的 CSS 文件命名爲 app.css:

static/css/app.css

.navbar-brand { font-family: 'Peralta', cursive; } 


表單處理

Forms(表單) 用來處理咱們的輸入。這在任何 web 應用或者網站中都是很常見的任務。標準的作法是經過 HTML 表單實現,用戶輸入一些數據,將其提交給服務器,而後服務器處理它。

表單處理是一項很是複雜的任務,由於它涉及到與應用多個層面的交互。有不少須要關心的問題。例如,提交給服務器的全部數據都是字符串的形式,因此在咱們使用它以前須要將其轉換爲須要的數據類型(整形,浮點型,日期等)。咱們必須驗證有關應用程序業務邏輯的數據。咱們還須要妥善地清理和審查數據,以免一些諸如 SQL 注入和 XSS 攻擊等安全問題。

好消息是,Django Forms API 使整個過程變的更加簡單,從而實現了大量工做的自動化。並且,最終的結果比大多數程序員本身去實現的代碼更加安全。因此,無論 HTML 的表單多麼簡單,老是使用Form API。

本身實現表單

起初,我想直接跳到表單 API。可是我以爲花點時間去了解一下表單處理的基本細節是一個不錯的主意。不然,這玩意兒將會看起來像魔術同樣,這是一件壞事,由於當出現錯誤時,你將不知道怎麼去找到問題所在。

隨着對一些編程概念的深刻理解,咱們能夠感受到本身能更好地掌控一些狀況。掌控是很重要的,由於它讓咱們寫代碼的時候更有信心。一旦咱們能確切地知道發生了什麼,實現可預見行爲的代碼就容易多了。調試和查找錯誤也變得很容易,由於你知道去哪裏查找。

不管如何,讓咱們開始實現下面的表單:

這是咱們在前一個教程繪製的線框圖。我如今意識到這個多是一個很差的例子,由於這個特殊的表單涉及處處理兩個不一樣模型數據:Topic(subject) 和 Post(message)。

還有一點很重要的咱們到如今爲止還沒討論過,就是用戶認證。咱們應該只爲登陸認證過的用戶去顯示這個頁面。經過這種方式,咱們才能知道是誰建立了 Topic 或者 Post。

如今讓咱們抽象一些細節,重點了解一下怎麼在數據庫中保存用戶的輸入。

首先,先建立一個新的 URL 路由,命名爲 new_topic:

myproject/urls.py

from django.conf.urls import url from django.contrib import admin from boards import views urlpatterns = [ url(r'^$', views.home, name='home'), 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), ] 

咱們建立的這個 URL 能幫咱們標識正確的 Board

如今來建立 new_topic 的 視圖函數:

boards/views.py

from django.shortcuts import render, get_object_or_404 from .models import Board def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) return render(request, 'new_topic.html', {'board': board}) 

目前爲止, new_topic 的視圖函數看起來和 board_topics 剛好相同。這是故意的,讓咱們一步步地來。

如今咱們須要一個名爲 new_topic.html 的模板:

templates/new_topic.html

{% extends 'base.html' %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} {% endblock %} 

如今咱們只有 breadcrumb 導航。注意咱們包含了返回到 board_topics 視圖 URL。

打開 URL http://127.0.0.1:8000/boards/1/new/。顯示結果是下面這個頁面:

咱們依然尚未實現到達這個新頁面的方法,可是若是咱們將 URL 改成 http://127.0.0.1:8000/boards/2/new/,它會把咱們帶到 Python Board 的頁面:

注意:若是你沒有跟着上一節課程一步步地作,你的結果和個人可能有些不同。在我這個例子中,個人數據庫有 3 個 Board 實例,分別是 Django = 1, Python = 2, 和 Random = 3。這些數字是數據庫中的 ID,用來找到正確的資源。 `

咱們能夠增長一些測試了:

boards/tests.py

from django.core.urlresolvers import reverse from django.urls import resolve from django.test import TestCase from .views import home, board_topics, new_topic from .models import Board class HomeTests(TestCase): # ... class BoardTopicsTests(TestCase): # ... class NewTopicTests(TestCase): def setUp(self): Board.objects.create(name='Django', description='Django board.') def test_new_topic_view_success_status_code(self): url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.get(url) self.assertEquals(response.status_code, 200) def test_new_topic_view_not_found_status_code(self): url = reverse('new_topic', kwargs={'pk': 99}) response = self.client.get(url) self.assertEquals(response.status_code, 404) def test_new_topic_url_resolves_new_topic_view(self): view = resolve('/boards/1/new/') self.assertEquals(view.func, new_topic) def test_new_topic_view_contains_link_back_to_board_topics_view(self): new_topic_url = reverse('new_topic', kwargs={'pk': 1}) board_topics_url = reverse('board_topics', kwargs={'pk': 1}) response = self.client.get(new_topic_url) self.assertContains(response, 'href="{0}"'.format(board_topics_url)) 

關於咱們的測試中新的 NewTopicTests 類的快速總結:

  • setUp:建立一個測試中使用的 Board 實例
  • test_new_topic_view_success_status_cod:檢查發給 view 的請求是否成功
  • test_new_topic_view_not_found_status_code:檢查當 Board 不存在時 view 是否會拋出一個 404 的錯誤
  • test_new_topic_url_resolves_new_topic_view:檢查是否正在使用正確的 view
  • test_new_topic_view_contains_link_back_to_board_topics_view:確保導航能回到 topics 的列表

運行測試:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...........
----------------------------------------------------------------------
Ran 11 tests in 0.076s

OK
Destroying test database for alias 'default'...

成功,如今咱們能夠去開始建立表單了。

templates/new_topic.html

{% extends 'base.html' %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post"> {% csrf_token %} <div class="form-group"> <label for="id_subject">Subject</label> <input type="text" class="form-control" id="id_subject" name="subject"> </div> <div class="form-group"> <label for="id_message">Message</label> <textarea class="form-control" id="id_message" name="message" rows="5"></textarea> </div> <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %} 

這是一個使用 Bootstrap 4 提供的 CSS 類手動建立的 HTML 表單。它看起來是這個樣子:

在 <form> 標籤中,咱們定義了 method 屬性。它會告訴瀏覽器咱們想如何與服務器通訊。HTTP 規範定義了幾種 request methods(請求方法)。可是在大部分狀況下,咱們只須要使用 GET 和 POST 兩種 request(請求)類型。

GET 多是最多見的請求類型了。它用於從服務器請求數據。每當你點擊了一個連接或者直接在瀏覽器中輸入了一個網址時,你就建立一個 GET 請求。

POST 用於當咱們想更改服務器上的數據的時候。通常來講,每次咱們發送數據給服務器都會致使資源狀態的變化,咱們應該使用 POST 請求發送數據。

Django 使用 CSRF Token(Cross-Site Request Forgery Token) 保護全部的 POST 請求。這是一個避免外部站點或者應用程序向咱們的應用程序提交數據的安全措施。應用程序每次接收一個 POST 時,都會先檢查 CSRF Token。若是這個 request 沒有 token,或者這個 token是無效的,它就會拋棄提交的數據。

csrf_token 的模板標籤:

{% csrf_token %}

它是與其餘表單數據一塊兒提交的隱藏字段:

<input type="hidden" name="csrfmiddlewaretoken" value="jG2o6aWj65YGaqzCpl0TYTg5jn6SctjzRZ9KmluifVx0IVaxlwh97YarZKs54Y32">

另一件事是,咱們須要設置 HTML 輸入的 name,name 將被用來在服務器獲取數據。

<input type="text" class="form-control" id="id_subject" name="subject"> <textarea class="form-control" id="id_message" name="message" rows="5"></textarea> 

下面是示範咱們如何檢索數據:

subject = request.POST['subject']
message = request.POST['message']

因此,從 HTML 獲取數據而且開始一個新的 topic 視圖的簡單實現能夠這樣寫:

from django.contrib.auth.models import User from django.shortcuts import render, redirect, get_object_or_404 from .models import Board, Topic, Post def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) if request.method == 'POST': subject = request.POST['subject'] message = request.POST['message'] user = User.objects.first() # TODO: 臨時使用一個帳號做爲登陸用戶 topic = Topic.objects.create( subject=subject, board=board, starter=user ) post = Post.objects.create( message=message, topic=topic, created_by=user ) return redirect('board_topics', pk=board.pk) # TODO: redirect to the created topic page return render(request, 'new_topic.html', {'board': board}) 

這個視圖函數只考慮能接收數據而且保存進數據庫的樂觀合法的 path,可是還缺乏一些部分。咱們沒有驗證數據。用戶能夠提交空表單或者提交一個大於 255 個字符的 subject。

到目前爲止咱們都在對 User 字段進行硬編碼,由於咱們尚未實現身份驗證。有一個簡單的方法來識別登陸的用戶。咱們會在下一個課程將這一塊。此外,咱們尚未實現列出 topic 的全部 posts 的視圖,實現了它,咱們就能夠將用戶重定向到列出全部主題的列表頁面。

點擊 Post 按鈕提交表單:

看起來成功了。可是咱們尚未實現主題的列表頁面,因此沒有東西能夠看。讓咱們來編輯 templates/topics.html 來實現一個合適的列表:

templates/topics.html

{% extends 'base.html' %} {% block title %} {{ board.name }} - {{ block.super }} {% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item active">{{ board.name }}</li> {% endblock %} {% block content %} <table class="table"> <thead class="thead-inverse"> <tr> <th>Topic</th> <th>Starter</th> <th>Replies</th> <th>Views</th> <th>Last Update</th> </tr> </thead> <tbody> {% for topic in board.topics.all %} <tr> <td>{{ topic.subject }}</td> <td>{{ topic.starter.username }}</td> <td>0</td> <td>0</td> <td>{{ topic.last_updated }}</td> </tr> {% endfor %} </tbody> </table> {% endblock %} 

咱們建立的 Topic 顯示在這上面了。

這裏有兩個新概念。

咱們首次使用 Board 模型中的 topics 屬性。topics 屬性由 Django 使用反向關係自動建立。在以前的步驟中,咱們建立了一個 Topic 實例:

def new_topic(request, pk):
    board = get_object_or_404(Board, pk=pk)

    # ...

    topic = Topic.objects.create(
        subject=subject,
        board=board,
        starter=user
    )

在 board=board 這行,咱們設置了 Topic 模型中的 board 字段,它是 ForeignKey(Board)。所以,咱們的 Board 實例就知道了與它關聯的 Topic 實例。

之因此咱們使用 board.topics.all 而不是 board.topics,是由於 board.topics 是一個 Related Manager,它與 Model Manager 很類似,一般在 board.objects 可獲得。因此,要返回給定 board 的全部 topic 咱們必須使用 board.topics.all(),要過濾一些數據,咱們能夠這樣用 board.topics.filter(subject__contains='Hello')

另外一個須要注意的是,在 Python 代碼中,咱們必須使用括號:board.topics.all(),由於 all() 是一個方法。在使用 Django 模板語言寫代碼的時候,在一個 HTML 模板文件裏面,咱們不使用括號,就只是 board.topics.all

第二件事是咱們在使用 ForeignKey

{{ topic.starter.username }}

使用一個點加上屬性這種寫法,咱們幾乎能夠訪問 User 模型的全部屬性。若是咱們想獲得用戶的 email,咱們可使用 topic.starter.email

咱們已經修改了 topics.html 模板,讓咱們建立一個能讓咱們轉到 new topic 頁面的按鈕:

templates/topics.html

{% block content %} <div class="mb-4"> <a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">New topic</a> </div> <table class="table"> <!-- code suppressed for brevity --> </table> {% endblock %} 

咱們能夠寫一個測試以確保用戶能夠經過此頁面訪問到 New Topic 頁面:

boards/tests.py

class BoardTopicsTests(TestCase): # ... def test_board_topics_view_contains_navigation_links(self): board_topics_url = reverse('board_topics', kwargs={'pk': 1}) homepage_url = reverse('home') new_topic_url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.get(board_topics_url) self.assertContains(response, 'href="{0}"'.format(homepage_url)) self.assertContains(response, 'href="{0}"'.format(new_topic_url)) 

我在這裏基本上重命名了 test_board_topics_view_contains_link_back_to_homepage 方法並添加了一個額外的 assertContains。這個測試如今負責確保咱們的 view 包含所需的導航連接。

測試表單

在咱們使用 Django 的方式編寫以前的表單示例以前, 讓咱們先爲表單處理寫一些測試:

boards/tests.py

''' new imports below '''
from django.contrib.auth.models import User from .views import new_topic from .models import Board, Topic, Post class NewTopicTests(TestCase): def setUp(self): Board.objects.create(name='Django', description='Django board.') User.objects.create_user(username='john', email='john@doe.com', password='123') # <- included this line here # ... def test_csrf(self): url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.get(url) self.assertContains(response, 'csrfmiddlewaretoken') def test_new_topic_valid_post_data(self): url = reverse('new_topic', kwargs={'pk': 1}) data = { 'subject': 'Test title', 'message': 'Lorem ipsum dolor sit amet' } response = self.client.post(url, data) self.assertTrue(Topic.objects.exists()) self.assertTrue(Post.objects.exists()) def test_new_topic_invalid_post_data(self): '''  Invalid post data should not redirect  The expected behavior is to show the form again with validation errors  ''' url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.post(url, {}) self.assertEquals(response.status_code, 200) def test_new_topic_invalid_post_data_empty_fields(self): '''  Invalid post data should not redirect  The expected behavior is to show the form again with validation errors  ''' url = reverse('new_topic', kwargs={'pk': 1}) data = { 'subject': '', 'message': '' } response = self.client.post(url, data) self.assertEquals(response.status_code, 200) self.assertFalse(Topic.objects.exists()) self.assertFalse(Post.objects.exists()) 

首先, test.py 文件變的愈來愈大。咱們會盡快改進它,將測試分爲幾個文件。但如今,讓咱們先保持這個狀態。

  • setUp:包含 User.objects.create_user 以建立用於測試的 User 實例。

  • test_csrf:因爲 CSRF Token 是處理 Post 請求的基本部分,咱們須要保證咱們的 HTML 包含 token。

  • test_new_topic_valid_post_data:發送有效的數據並檢查視圖函數是否建立了 Topic 和 Post 實例。

  • test_new_topic_invalid_post_data:發送一個空字典來檢查應用的行爲。

  • test_new_topic_invalid_post_data_empty_fields:相似於上一個測試,可是此次咱們發送一些數據。預期應用程序會驗證而且拒絕空的 subject 和 message。

運行這些測試:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........EF.....
======================================================================
ERROR: test_new_topic_invalid_post_data (boards.tests.NewTopicTests)
----------------------------------------------------------------------
Traceback (most recent call last):
...
django.utils.datastructures.MultiValueDictKeyError: "'subject'"

======================================================================
FAIL: test_new_topic_invalid_post_data_empty_fields (boards.tests.NewTopicTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/vitorfs/Development/myproject/django-beginners-guide/boards/tests.py", line 115, in test_new_topic_invalid_post_data_empty_fields
    self.assertEquals(response.status_code, 200)
AssertionError: 302 != 200

----------------------------------------------------------------------
Ran 15 tests in 0.512s

FAILED (failures=1, errors=1)
Destroying test database for alias 'default'...

有一個失敗的測試和一個錯誤。兩個都與驗證用戶的輸入有關。不要試圖用當前的實現來修復它,讓咱們經過使用 Django Forms API 來經過這些測試

建立表單正確的姿式

自從咱們開始使用 Forms,咱們已經走了很長一段路。終於,是時候使用 Forms API 了。

Forms API 可在模塊 django.forms 中獲得。Django 使用兩種類型的 form:forms.Form 和 forms.ModelFormForm 類是通用的表單實現。咱們可使用它來處理與應用程序 model 沒有直接關聯的數據。ModelForm 是 Form 的子類,它與 model 類相關聯。

在 boards 文件夾下建立一個新的文件 forms.py

boards/forms.py

from django import forms from .models import Topic class NewTopicForm(forms.ModelForm): message = forms.CharField(widget=forms.Textarea(), max_length=4000) class Meta: model = Topic fields = ['subject', 'message'] 

這是咱們的第一個 form。它是一個與 Topic model 相關聯的 ModelForm。Meta 類裏面 fields 列表中的 subject 引用 Topic 類中的 subject field(字段)。如今注意到咱們定義了一個叫作 message 的額外字段。它用來引用 Post 中咱們想要保存的 message。

如今咱們須要重寫咱們的 views.py:

boards/views.py

from django.contrib.auth.models import User from django.shortcuts import render, redirect, get_object_or_404 from .forms import NewTopicForm from .models import Board, Topic, Post def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) user = User.objects.first() # TODO: get the currently logged in user if request.method == 'POST': form = NewTopicForm(request.POST) if form.is_valid(): topic = form.save(commit=False) topic.board = board topic.starter = user topic.save() post = Post.objects.create( message=form.cleaned_data.get('message'), topic=topic, created_by=user ) return redirect('board_topics', pk=board.pk) # TODO: redirect to the created topic page else: form = NewTopicForm() return render(request, 'new_topic.html', {'board': board, 'form': form}) 

這是咱們在 view(視圖) 中處理 form(表單) 的方式。讓咱們去掉一些多餘的部分,只看表單處理的核心部分:

if request.method == 'POST':
    form = NewTopicForm(request.POST)
    if form.is_valid():
        topic = form.save()
        return redirect('board_topics', pk=board.pk)
else:
    form = NewTopicForm()
return render(request, 'new_topic.html', {'form': form})

首先咱們判斷請求是 POST 仍是 GET。若是請求是 POST,這意味着用戶向服務器提交了一些數據。因此咱們實例化一個將 POST 數據傳遞給 form 的 form 實例:form = NewTopicForm(request.POST)

而後,咱們讓 Django 驗證數據,檢查 form 是否有效,咱們可否將其存入數據庫:if form.is_valid():。若是表單有效,咱們使用 form.save() 將數據存入數據庫。save() 方法返回一個存入數據庫的 Model 實例。 因此,由於這是一個 Topic form, 因此它會返回 topic = form.save() 建立的 Topic。而後,通用的路徑是把用戶重定向到其餘地方,以免用戶經過按 F5 從新提交表單,而且保證應用程序的流程走向。

如今,若是數據是無效的,Django 會給 form 添加錯誤列表。而後,視圖函數不會作任何處理而且返回最後一句: return render(request, 'new_topic.html', {'form': form})。這意味着咱們須要更新 new_topic.html 以顯示錯誤。

若是請求是 GET,咱們只須要使用 form = NewTopicForm() 初始化一個新的空表單。

讓咱們運行測試並觀察狀況:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...............
----------------------------------------------------------------------
Ran 15 tests in 0.522s

OK
Destroying test database for alias 'default'...

咱們甚至修復了最後兩個測試。

Django Forms API 不只僅是處理和驗證數據。它還爲咱們生成 HTML。

templates/new_topic.html

{% extends 'base.html' %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %} 

這個 form 有三個渲染選項:form.as_tableform.as_ul 和 form.as_p。這是一個快速的渲染表單全部字段的方法。顧名思義,as_table 使用 table 標籤來格式化輸入,as_ul 使用 li 標籤。

看看效果:

咱們之前的 form 看起來更好,是吧?咱們將當即修復它。

它看起來很破,可是相信我;它背後有不少東西。它很是強大。好比,若是咱們的表單有 50 個字段,咱們能夠經過鍵入 {{ form.as_p }} 來顯示全部字段。

此外,使用 Forms API,Django 會驗證數據而且向每一個字段添加錯誤消息。讓咱們嘗試提交一個空的表單:

注意: 若是你提交表單時看到相似這樣的東西:這不是 Django 致使的,而是你的瀏覽器進行預驗證。要禁用它能夠在你的表單標籤中添加 novalidate 屬性:

你能夠不修改它,不會有問題。這只是由於咱們的表單如今很是簡單,並且咱們沒有太多的數據驗證能夠看到。

另一件須要注意的事情是:沒有 「只客戶端驗證」 這樣的事情。JavaScript 驗證或者瀏覽器驗證僅用於可用性目的。同時也減小了對服務器的請求數量。數據驗證應該始終在服務器端完成,這樣咱們能夠徹底掌控數據。 `

它還能夠處理在 Form 類或者 Model 類中定義的幫助文本。

boards/forms.py

from django import forms from .models import Topic class NewTopicForm(forms.ModelForm): message = forms.CharField( widget=forms.Textarea(), max_length=4000, help_text='The max length of the text is 4000.' ) class Meta: model = Topic fields = ['subject', 'message'] 

咱們也能夠爲表單字段設置額外的屬性:

boards/forms.py

from django import forms from .models import Topic class NewTopicForm(forms.ModelForm): message = forms.CharField( widget=forms.Textarea( attrs={'rows': 5, 'placeholder': 'What is on your mind?'} ), max_length=4000, help_text='The max length of the text is 4000.' ) class Meta: model = Topic fields = ['subject', 'message'] 

用BootStrap 表單渲染

如今讓咱們把事情作得更完善。

當使用 Bootstrap 或者其餘的前端庫時,我比較喜歡使用一個叫作 django-widget-tweaks 的 Django 庫。它可讓咱們更好地控制渲染的處理,在保證默認值的狀況下,只需在上面添加額外的自定義設置。

開始安裝它:

pip install django-widget-tweaks

添加到 INSTALLED_APPS

myproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'widget_tweaks',

    'boards',
]

如今可使用它了:

templates/new_topic.html

{% extends 'base.html' %} {% load widget_tweaks %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% render_field field class="form-control" %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %} 

這就是咱們使用的 django-widget-tweaks 的效果。首先,咱們使用 {% load widget_tweaks %} 模板標籤將其加載到模板。而後這樣使用它:

{% render_field field class="form-control" %}

render_field 不屬於 Django;它存在於咱們安裝的包裏面。要使用它,咱們須要傳遞一個表單域實例做爲第一個參數,而後咱們能夠添加任意的 HTML 屬性去補充它。這頗有用由於咱們能夠根據特定的條件指定類。

一些 render_field 模板標籤的例子:

{% render_field form.subject class="form-control" %}
{% render_field form.message class="form-control" placeholder=form.message.label %}
{% render_field field class="form-control" placeholder="Write a message!" %}
{% render_field field style="font-size: 20px" %}

如今要實現 Bootstrap 4 驗證標籤,咱們能夠修改 new_topic.html 模板。

templates/new_topic.html

<form method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% if form.is_bound %} {% if field.errors %} {% render_field field class="form-control is-invalid" %} {% for error in field.errors %} <div class="invalid-feedback"> {{ error }} </div> {% endfor %} {% else %} {% render_field field class="form-control is-valid" %} {% endif %} {% else %} {% render_field field class="form-control" %} {% endif %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %} <button type="submit" class="btn btn-success">Post</button> </form> 

效果是:

因此,咱們有三種不一樣的渲染狀態:

  • Initial state:表單沒有數據(不受約束)
  • Invalid:咱們添加了 .is-invalid 這個 CSS class 並將錯誤消息添加到具備 .invalid-feedback class 的元素中
  • Valid:咱們添加了 .is-valid 的 CSS class,以綠色繪製表單域,並向用戶反饋它是否可行。

複用表單模板

模板看起來有點複雜,是吧?有個好消息是咱們能夠在項目中重複使用它。

在 templates 文件夾中,建立一個新的文件夾命名爲 includes:

myproject/
 |-- myproject/
 |    |-- boards/
 |    |-- myproject/
 |    |-- templates/
 |    |    |-- includes/    <-- here!
 |    |    |-- base.html
 |    |    |-- home.html
 |    |    |-- new_topic.html
 |    |    +-- topics.html
 |    +-- manage.py
 +-- venv/

在 includes 文件夾中,建立一個 form.html:

templates/includes/form.html

{% load widget_tweaks %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% if form.is_bound %} {% if field.errors %} {% render_field field class="form-control is-invalid" %} {% for error in field.errors %} <div class="invalid-feedback"> {{ error }} </div> {% endfor %} {% else %} {% render_field field class="form-control is-valid" %} {% endif %} {% else %} {% render_field field class="form-control" %} {% endif %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %} 

如今來修改咱們的 new_topic.html 模板:

templates/new_topic.html

{% extends 'base.html' %} {% block title %}Start a New Topic{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li> <li class="breadcrumb-item active">New topic</li> {% endblock %} {% block content %} <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %} 

顧名思義,{% include %} 用來在其餘的模板中包含 HTML 模板。這是在項目中重用 HTML 組件的經常使用方法。

在下一個咱們實現的表單,咱們能夠簡單地使用 {% include 'includes/form.html' %} 去渲染它。

Adding More Tests

如今咱們在使用 Django 表單;咱們能夠添加更多的測試以確保它能運行順利

boards/tests.py

# ... other imports
from .forms import NewTopicForm class NewTopicTests(TestCase): # ... other tests def test_contains_form(self): # <- new test url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.get(url) form = response.context.get('form') self.assertIsInstance(form, NewTopicForm) def test_new_topic_invalid_post_data(self): # <- updated this one '''  Invalid post data should not redirect  The expected behavior is to show the form again with validation errors  ''' url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.post(url, {}) form = response.context.get('form') self.assertEquals(response.status_code, 200) self.assertTrue(form.errors) 

這是咱們第一次使用 assertIsInstance 方法。基本上咱們的處理是抓取上下文的表單實例,檢查它是不是一個 NewTopicForm。在最後的測試中,我添加了 self.assertTrue(form.errors) 以確保數據無效的時候表單會顯示錯誤。


總結

在這個課程,咱們學習了 URLs, 可重用模板和表單。像往常同樣,咱們也實現了幾個測試用例。這能使咱們開發中更自信。

咱們的測試文件變的愈來愈大,因此在下一節中,咱們重構它以提升它的可維護性,從而維持咱們代碼的增長。

咱們也達到了咱們須要與登陸的用戶進行交互的目的。在下一節,咱們學習了關於認證的一切知識和怎麼去保護咱們的視圖和資源。

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

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

相關文章
相關標籤/搜索