Django初學者入門指南5-存儲數(shù)據(jù)(譯&改)
Django初學者入門指南7-部署發(fā)布(譯&改)--施工中
簡介
在本教程中椭微,我們將深入研究兩個基本概念:URLs和Form眨八。在這個過程中痪寻,我們將探討許多其他概念焕阿,如創(chuàng)建可重用模板和安裝第三方庫怔球。我們還將編寫大量的單元測試。
如果您從第一部分開始就遵循本教程系列,編寫項目代碼并逐步遵循教程荸百,則可能需要在開始前更新models.py:
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='+')
更新好后,在虛擬環(huán)境中執(zhí)行下面的命令進行遷移更新:
python manage.py makemigrations
python manage.py migrate
如果已經給updated_by
屬性配置了null=True
滨攻,last_updated
屬性配置了auto_now_add=True
够话,那么你就可以不用進行上面的修改蓝翰。
如果需要直接使用源代碼的話,可以通過GitHub直接獲取女嘲。當前狀態(tài)的項目代碼可以在發(fā)布的標簽v0.2-lw下找到畜份,也可以直接點擊下面的鏈接前往獲取:
https://github.com/sibtc/django-beginners-guide/tree/v0.2-lw
接下來我們繼續(xù)開發(fā)吧欣尼。
URLs
繼續(xù)開發(fā)我們的應用程序爆雹,現(xiàn)在必須實現(xiàn)一個新的頁面來列出屬于某個給定版塊Board的所有主題Topic。簡單回顧一下媒至,下面可以看到我們在上一個教程中繪制的線框圖:
我們先來編輯myproject目錄下的urls.py文件:
<details>
<summary>原始版本</summary>
原始版本的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),
]
</details>
修訂版本的myproject/urls.py
from django.urls import re_path
from django.contrib import admin
from boards import views
urlpatterns = [
re_path(r'^$', views.home, name='home'),
re_path(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'),
re_path(r'^admin/', admin.site.urls),
]
這次讓我們花點時間分析一下urlpatterns
和url
。
URL調度器和URLconf(URL配置)是Django應用程序的基本部分拒啰。一開始驯绎,它可能看起來很混亂;還記得當我第一次開始使用Django開發(fā)時谋旦,經歷了一段艱難的時期剩失。
事實上,現(xiàn)在Django開發(fā)人員正在研究一個簡化路由語法的方案. 但現(xiàn)在在版本1.11里册着,讓我們試著理解它是如何工作的吧拴孤。
一個項目可以有很多urls.py在應用程序中分發(fā),但是Django需要一個urls.py作為出發(fā)點甲捏。這個特別的urls.py稱為root URLconf演熟,它被定義在settings.py文件。
myproject/settings.py
ROOT_URLCONF = 'myproject.urls'
它在創(chuàng)建項目時就自動生成好了司顿,這里就不再需要去修改了芒粹。
當Django收到請求時,它就會在項目的URLconf中搜索匹配項大溜。它從urlpatterns
變量的第一條開始化漆,逐條嘗試與請求的url
進行匹配。
如果Django找到匹配項钦奋,它將通過re_path
(url
也相同)方法的第二個參數(shù)把請求傳遞給view function座云。urlpatterns
中的順序很重要,因為Django一旦找到匹配項就會停止搜索付材。如果Django在URLconf中找不到匹配項朦拖,它將引發(fā)一個404異常,就是Page Not Found的錯誤代碼厌衔。
以下是對url
和re_path
函數(shù)的剖析:
def url(regex, view, kwargs=None, name=None):
# ...
# re_path的用法與url完全相同贞谓,Django 2.x版本開始,不建議使用url方法葵诈,所以使用re_path方法
- regex: 這個就是用于匹配請求url的正則表達式裸弦,需要注意的是它不會匹配到url中的請求參數(shù),比如http://127.0.0.1:8000/boards/?page=2作喘,正則表達式只會嘗試匹配/boards/部分理疙,其他則忽略掉了。
- view: 指定用于響應請求url的頁面函數(shù)泞坦,它同樣也可以支持通過include函數(shù)引入其他子文件目錄的urls.py文件窖贤。例如,可以使用它來定義一組特定于應用程序的url贰锁,并使用前綴將其包含在根URLconf中赃梧。稍后我們將對這個概念進行更多的探討。
- kwargs: 傳遞到目標頁面的任意參數(shù)豌熄,它通常用于對可重用視圖進行一些簡單的自定義授嘀,實際場景中不經常用它。
- name: 給定URLs的唯一標識符锣险,這是一個非常重要的特性蹄皱,一定要記住給你的網址命名。通過這種方式芯肤,您就可以通過更改regex來更改整個項目中的特定URLs巷折。因此,不要在視圖或模板中硬編碼URLs崖咨,并且始終使用URLs的名稱來引用URLs锻拘,這一點很重要。
基礎URLs正則表達式
url的創(chuàng)建非常簡單击蹲,這只是一個字符串匹配的問題署拟。假設我們想要創(chuàng)建一個about頁面,可以這樣定義:
myproject/urls.py
<details>
<summary>原始版本</summary>
from django.conf.urls import url
from boards import views
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^about/$', views.about, name='about'),
]
</details>
<details open>
<summary>修訂版本</summary>
from django.urls import re_path
from boards import views
urlpatterns = [
re_path(r'^$', views.home, name='home'),
re_path(r'^about/$', views.about, name='about'),
]
</details>
我們也可以創(chuàng)建更深層級的URL:
<details>
<summary>原始版本</summary>
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'),
]
</details>
<details open>
<summary>修訂版本</summary>
from django.urls import re_path
from boards import views
urlpatterns = [
re_path(r'^$', views.home, name='home'),
re_path(r'^about/$', views.about, name='about'),
re_path(r'^about/company/$', views.about_company, name='about_company'),
re_path(r'^about/author/$', views.about_author, name='about_author'),
re_path(r'^about/author/vitor/$', views.about_vitor, name='about_vitor'),
re_path(r'^about/author/erica/$', views.about_erica, name='about_erica'),
re_path(r'^privacy/$', views.privacy_policy, name='privacy_policy'),
]
</details>
以上都是一些URL路由的例子际邻,針對這個路由芯丧,還需要在頁面的函數(shù)中定義下面的函數(shù):
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路由的進階用法是通過利用regex匹配特定類型的數(shù)據(jù)并創(chuàng)建動態(tài)URL來實現(xiàn)的。
例如創(chuàng)建一個用戶個人資料頁面世曾,例如github.com/vitorfs
或者twitter.com/vitorfs
缨恒,其中vitorfs
是我的用戶名,可以通過下面的方式實現(xiàn):
<details>
<summary>原始版本</summary>
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'),
]
</details>
<details open>
<summary>修訂版本</summary>
from django.urls import re_path
from boards import views
urlpatterns = [
re_path(r'^$', views.home, name='home'),
re_path(r'^(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile'),
]
</details>
通過這種方式轮听,將匹配到Django用戶模型的所有有效用戶的姓名骗露。
或許你注意到了,上面的URL正則匹配的范圍非常大血巍,因為它定義在了根url而不是類似于/profile/<username>這樣的url萧锉。在這種情況下,我們定義的/about/的匹配就需要提到它的前面述寡,就像下面這樣:
<details>
<summary>原始版本</summary>
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'),
]
</details>
<details open>
<summary>修訂版本</summary>
from django.urls import re_path
from boards import views
urlpatterns = [
re_path(r'^$', views.home, name='home'),
re_path(r'^about/$', views.about, name='about'),
re_path(r'^(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile'),
]
</details>
如果about
頁面是在username URL模式之后定義柿隙,Django將永遠找不到它叶洞,因為單詞about
將匹配username的正則表達式,并且由頁面user_profile
響應而不是about
頁面函數(shù)禀崖。
但是這樣定義還是有一些副作用衩辟。例如從現(xiàn)在起,必須禁止使用about
作為用戶名波附,因為如果選擇about
作為其用戶名艺晴,此人將永遠看不到他的個人資料頁面。
旁注: 如果你想為個人資料頁面定義更加合理的URL路由掸屡,建議使用/u/vitorfs/或者是/@vitorfs/這樣帶有前綴標識符的方式封寞。
不過如果你依然想使用前面的方式定義URL路由,那么需要用到一個用戶名的禁用文字列表:github.com/shouldbee/reserved-usernames仅财”肪浚或者是這個我自學Django時,我自己創(chuàng)建的禁用文字列表:github.com/vitorfs/parsifal/满着。
這樣的沖突是非常容易發(fā)生的谦炒,拿GitHub來說:他們有一個URL路由來展示你當前關注的項目或用戶:github.com/watching。如果有人用watching
作為用戶名风喇,那么他就沒有辦法訪問到他自己的個人資料頁面宁改。同樣的,通過github.com/watching/repositories我們本該看到我們關注的項目魂莫,但是卻可能訪問到這個用戶的項目列表还蹲,類似我的項目列表:github.com/vitorfs/repositories。
這種URL路由的整體思想是創(chuàng)建動態(tài)頁面耙考,其中URL的一部分將用作某個資源的標識符谜喊,該資源將用于組成頁面。例如倦始,該標識符可以是整數(shù)ID或字符串斗遏。
首先,我們將使用Board的ID為Topic創(chuàng)建一個動態(tài)頁面鞋邑,讓我們再看看在URL部分開頭給出的示例:
<details>
<summary>原始版本</summary>
url(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics')
</details>
<details open>
<summary>修訂版本</summary>
re_path(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics')
</details>
正則表達式\d+
匹配的是至少一位數(shù)字的整數(shù)诵次。我們會通過這個數(shù)字去數(shù)據(jù)庫中查詢Board類型的實例。再看下正則表達式的寫法(?P<pk>\d+)
枚碗,這種寫法會讓Django將這個正則表達式匹配到的字符串賦值給變量pk
逾一。
所以對應的在views.py中實現(xiàn)方法:
def board_topics(request, pk):
# do something...
因為我們使用了(?P<pk>\d+)
這樣的正則表達式,所以在board_topics
接收的變量就必須是pk
肮雨。
如果不需要指定該參數(shù)的名稱遵堵,那么可以使用下面的寫法:
<details>
<summary>原始版本</summary>
url(r'^boards/(\d+)/$', views.board_topics, name='board_topics')
</details>
<details open>
<summary>修訂版本</summary>
re_path(r'^boards/(\d+)/$', views.board_topics, name='board_topics')
</details>
這樣我們就可以定義成:
def board_topics(request, board_id):
# do something...
或者是:
def board_topics(request, id):
# do something...
定義成什么樣的名字不重要,使用命名參數(shù)可以讓我們更清楚url中匹配的各個參數(shù),在使用更多變量更大url時陌宿,更加容易理解锡足。
旁注: PK 和 ID 的區(qū)別?
PK 就是Primary Key壳坪,它是訪問模型主鍵的快捷方式舱污,所有Django模型都有這個屬性。
在大多數(shù)情況下弥虐,使用pk
屬性與id
相同。這是因為如果我們不為模型定義主鍵媚赖,Django將自動創(chuàng)建一個名為id
的AutoField
并默認為主鍵霜瘪。但如果你為一個模型定義了一個不同的主鍵,假設字段obj.email
或者obj.pk
去訪問它了颖对。
使用URLs API
是時候寫些代碼了。讓我們來實現(xiàn)版塊的主題列表頁面吧磨隘。
首先在urls.py中添加一個新的url路由:
<details>
<summary>原始版本</summary>
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),
]
</details>
<details open>
<summary>修訂版本</summary>
myproject/urls.py
from django.urls import re_path
from django.contrib import admin
from boards import views
urlpatterns = [
re_path(r'^$', views.home, name='home'),
re_path(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'),
re_path(r'^admin/', admin.site.urls),
]
</details>
然后再實現(xiàn)頁面方法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>
提示: 現(xiàn)在我們先臨時創(chuàng)建固定的html文件番捂,在后續(xù)的教程中會教大家使用可復用的html模板.
現(xiàn)在我們在瀏覽器中打開http://127.0.0.1:8000/boards/1/个唧,應該會看到如下的頁面:
讓我們寫一點測試用例吧,打開tests.py并添加如下內容:
boards/tests.py
# from django.core.urlresolvers import reverse # 注意現(xiàn)在新版本放到了下面
from django.urls import resolve, reverse
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
方法设预,在這個方法里徙歼,我們創(chuàng)建了一個Board實例來執(zhí)行測試用例。因為Django測試工具不會對當前數(shù)據(jù)庫進行測試鳖枕。為了運行測試魄梯,Django會動態(tài)創(chuàng)建一個新數(shù)據(jù)庫,應用所有模型遷移宾符,運行測試酿秸,完成后銷毀測試數(shù)據(jù)庫。
所以我們需要在setUp
方法中準備用于測試的數(shù)據(jù)魏烫,以便模擬測試場景辣苏。
-
test_board_topics_view_success_status_code
:用于檢測是否能為當前已有的Board對象數(shù)據(jù)返回正確的狀態(tài)碼(200)。 -
test_board_topics_view_not_found_status_code
:用于檢測是否能為當前沒有的Board對象數(shù)據(jù)返回找不到數(shù)據(jù)的狀態(tài)碼(404)则奥。 -
test_board_topics_url_resolves_board_topics_view
:用于檢測Django是否用正確的頁面響應方法來響應指定的URL考润。
讓我們運行測試用例吧:
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
沒有通過,拋出了異常boards.models.DoesNotExist: Board matching query does not exist.
在配置為DEBUG=False
的生產環(huán)境中读处,訪問者將看到一個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頁面。稍后我們可以自行定制這個404頁面粥脚。
這是一個非常常用的測試用例窃肠,實際上Django有一個現(xiàn)成的方法來返回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'...
妥了,我們繼續(xù)后面的開發(fā)吧树灶。
下一步是在屏幕中創(chuàng)建導航鏈接纤怒。主頁應該有一個鏈接,將訪問者鏈接到一個給定的Board的主題頁天通。類似地泊窘,主題頁面也應該有一個指向主頁的鏈接。
我們先來為主頁HomeTests
編寫一些測試用例:
boards/tests.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))
注意現(xiàn)在也為HomeTests添加了一個setUp方法像寒,這是因為現(xiàn)在我們需要一個Board實例烘豹,同時還將url和response移動到setUp,這樣就可以在新的測試中重用相同的參數(shù)了诺祸。
這里的新測試是test_home_view_contains_link_topics_page
,使用assertContents方法來測試響應體是否包含給定的文本携悯。在測試中檢測的文本是a
標簽的href
部分。這就等同于測試響應體是否包含文本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'...
讓我們繼續(xù)修改代碼來通過這個單元測試蚌卤。
修改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。第一個參數(shù)是URL的name(在URLconf中定義奥秆,即urls.py)逊彭,則可以根據(jù)需要傳遞任意數(shù)量的參數(shù)。
如果它是一個像主頁那樣的簡單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'...
好了,我們可以在瀏覽器里查看了:
現(xiàn)在我們來寫回到首頁的代碼悼瘾,先寫測試用例:
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'...
更新模板html:
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應用程序的一個基礎部分。有了這些知識亥宿,我們就可以繼續(xù)開發(fā)了走净。接下來拇舀,我會給出一些常用URL Patterns献烦,以便可以更好的掌握這個知識牌借。
常用URL Patterns
比較深一點的技巧是regex,所以我準備了一個最常用的URL Patterns的列表。當需要一個特定的URL時悟狱,你可以隨時引用這個列表静浴。
Primary Key AutoField | Value |
---|---|
Regex | (?P<pk>\d+) |
Example | url(r'^questions/(?P<pk>\d+)/$', views.question, name='question') |
Valid URL | /questions/934/ |
Captures | {'pk': '934'} |
Slug Field | Value |
---|---|
Regex | (?P<slug>[-\w]+) |
Example | url(r'^posts/(?P<slug>[-\w]+)/$', views.post, name='post') |
Valid URL | /posts/hello-world/ |
Captures | {'slug': 'hello-world'} |
Slug Field with Primary Key | Value |
---|---|
Regex | (?P<slug>[-\w]+)-(?P<pk>\d+) |
Example | url(r'^blog/(?P<slug>[-\w]+)-(?P<pk>\d+)/$', views.blog_post, name='blog_post') |
Valid URL | /blog/hello-world-159/ |
Captures | {'slug': 'hello-world', 'pk': '159'} |
Django User Username | Value |
---|---|
Regex | (?P<username>[\w.@+-]+) |
Example | url(r'^profile/(?P<username>[\w.@+-]+)/$', views.user_profile, name='user_profile') |
Valid URL | /profile/vitorfs/ |
Captures | {'username': 'vitorfs'} |
Year | Value |
---|---|
Regex | (?P<year>[0-9]{4}) |
Example | url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive, name='year') |
Valid URL | /articles/2016/ |
Captures | {'year': '2016'} |
Year / Month | Value |
---|---|
Regex | (?P<year>[0-9]{4})/(?P<month>[0-9]{2}) |
Example | url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive, name='month') |
Valid URL | /articles/2016/01/ |
Captures | {'year': '2016', 'month': '01'} |
如果需要查看更多其他例子,可以前往這里查看:常用URL Patterns.
可復用的模板
到目前為止挤渐,我們常常在復制和粘貼相同的內容到HTML文檔苹享,從長遠來看這是不可持續(xù)的。這也是一種不好的做法浴麻。
在本節(jié)中得问,我們將重構HTML模板,抽出可復用的部分软免,創(chuàng)建一個master page椭赋,并且只在各自模板寫它獨有的代碼。
在templates文件夾中創(chuàng)建一個名為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>
這將是我們的基礎頁面或杠,往后創(chuàng)建的每個模板,都會extends這個特殊的模板宣蔚。注意現(xiàn)在我們引入了{% block %}
標記向抢,它將在模板中預留一個位置,子模板(擴展該模板頁面的其他頁面)可以在該位置中插入代碼和HTML胚委。
在{% block title %}
的這個位置挟鸠,我們還設置了一個默認值,即Django Boards
亩冬。如果我們沒有在子模板中為{% block title %}
設置值艘希,則會使用該默認值。
現(xiàn)在讓我們重構兩個模板: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 %}
注意現(xiàn)在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 %}
<!-- 這里先留空营袜,我們后面會來完善 -->
{% endblock %}
在topics.html文件中撒顿,我們修改了{% block title %}
的值。注意這里使用{{ block.super }}
來獲取到了母模板中的值荚板。這里就將頁面的標題base.html定義為了Django Boards
凤壁。同樣Python
版塊的頁面,標題就會變?yōu)?code>Python - Django Boards跪另,而Random
版塊的標題就會變?yōu)?code>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'...
完美唧席!所有功能正常。
使用現(xiàn)在的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>
<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>
我所使用的頂部條樣式是:Bootstrap 4 Navbar Component.
我想將標題logo
的字體修改一下(.navbar-brand
)袱吆。
打開fonts.google.com厌衙,輸入Django Boards
或者其他任何你想使用的名稱,點擊apply to all fonts绞绒,檢索到你想使用的字體婶希。
將字體添加到母模板base.html中:
{% load static %}<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Django Boards{% endblock %}</title>
<link 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下創(chuàng)建一個文件app.css:
static/css/app.css
.navbar-brand {
font-family: 'Peralta', cursive;
}
表單(Forms)
表單用于處理用戶輸入,這是任何web應用程序或網站中非常常見的功能蓬衡。標準的方法是通過HTML表單喻杈,用戶輸入一些數(shù)據(jù),提交給服務器狰晚,然后服務器用它做一些事情筒饰。
表單處理是一項相當復雜的任務,因為它涉及到與應用程序的許多層進行交互壁晒。還有許多問題需要處理瓷们。例如,提交給服務器的所有數(shù)據(jù)都是字符串格式的秒咐,因此在對其進行任何操作之前谬晕,我們必須將其轉換為適當?shù)臄?shù)據(jù)類型(integer、float携取、date等)攒钳。我們必須驗證與應用程序的業(yè)務邏輯相關的數(shù)據(jù)。我們還必須正確地清理和清理數(shù)據(jù)雷滋,以避免諸如SQL注入和XSS攻擊之類的安全問題不撑。
好消息是Django Forms API使整個過程更加容易,自動化了這項工作的一大部分晤斩。而且焕檬,最終的結果是一個比大多數(shù)程序員自己能夠實現(xiàn)的更安全的代碼。所以澳泵,不管HTML表單有多簡單揩页,都要使用Django自帶的表單API。
如何使用表單
一開始烹俗,我想直接跳到表單API爆侣。但我認為花點時間來理解表單處理的底層細節(jié)是個好主意。否則幢妄,它最終會看起來像魔術兔仰,這是一件壞事,因為當事情出了問題蕉鸳,你不知道該去哪里尋找問題乎赴。
隨著對一些編程概念的深入理解忍法,我們可以感覺到對代碼的可控性更強。掌握控制權很重要榕吼,因為它讓我們更有信心地編寫代碼饿序。一旦我們知道了到底發(fā)生了什么,實現(xiàn)一個可預測行為的代碼就容易多了羹蚣。調試和查找錯誤也容易得多原探,因為知道在哪里去排查。
總之顽素,讓我們從實現(xiàn)下面的表單開始:
這是我們在上一個教程中繪制的線框之一咽弦。我現(xiàn)在意識到這可能是一個不好的例子,因為這個特殊的表單需要處理兩個不同模型的數(shù)據(jù):Topic(subject)和Post(message)胁出。
到目前為止型型,還有一個我們還沒有討論過的重要功能,那就是用戶身份驗證全蝶。我們應該只為經過身份驗證的用戶顯示此屏幕闹蒜。這樣我們就可以知道誰創(chuàng)建了Topic或Post。
現(xiàn)在讓我們抽象一些細節(jié)抑淫,重點了解如何將用戶輸入的內容保存到數(shù)據(jù)庫中嫂用。
首先,讓我們創(chuàng)建一個名為new_topic的新URL路由:
myproject/urls.py
<details>
<summary>原始版本</summary>
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),
]
</details>
<details open>
<summary>修訂版本</summary>
from django.urls import re_path
from django.contrib import admin
from boards import views
urlpatterns = [
re_path(r'^$', views.home, name='home'),
re_path(r'^boards/(?P<pk>\d+)/$', views.board_topics, name='board_topics'),
re_path(r'^boards/(?P<pk>\d+)/new/$', views.new_topic, name='new_topic'),
re_path(r'^admin/', admin.site.urls),
]
</details>
我們通過這種方式創(chuàng)建的url路由丈冬,可以讓我們在創(chuàng)建主題Topic時知道它屬于那一個版塊Board。
現(xiàn)在讓我們來創(chuàng)建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方法完全相同甘畅,不著急埂蕊,咱一步一步來。
我們還需要創(chuàng)建一個文件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 %}
這里我們只實現(xiàn)了頂部導航條的功能疏唾,注意新增了跳轉到版塊下的主題列表頁面board_topics.
打開鏈接http://127.0.0.1:8000/boards/1/new/蓄氧。我們可以看到下面的頁面:
我們還沒有為這個頁面編寫入口,直接將鏈接修改為http://127.0.0.1:8000/boards/2/new/槐脏,可以看到發(fā)起的主題切換到了另外一個版塊Python Board:
提示:
如果您沒有遵循上一教程中的步驟喉童,那么結果可能會有所不同。在我的例子中顿天,數(shù)據(jù)庫中有三個Board實例堂氯,分別是Django=1牌废、Python=2和Random=3排抬。這些數(shù)字是來自數(shù)據(jù)庫的id蹲蒲,從URL用于標識正確的資源模燥。
現(xiàn)在我們增加一點測試用例:
boards/tests.py
# from django.core.urlresolvers import reverse # 注意現(xiàn)在新版本放到了下面
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
: 創(chuàng)建了版塊Board示例供測試使用辽旋。 -
test_new_topic_view_success_status_code
: 檢查請求頁面的狀態(tài)碼 -
test_new_topic_view_not_found_status_code
: 檢查非法請求是否為404 -
test_new_topic_url_resolves_new_topic_view
: 檢查是否響應正確的頁面方法 -
test_new_topic_view_contains_link_back_to_board_topics_view
: 檢查是否能正常返回到版塊主題列表頁面
運行測試用例:
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'...
搞定溶其,讓我們開始創(chuàng)建表單吧。
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創(chuàng)建的原始HTML表單,它的樣子如下:
在<form>
標簽中,我們必須定義method
屬性,這將決定瀏覽器如何與服務器通信。HTTP規(guī)范定義了幾個請求方法(動詞)驾凶,在大多數(shù)情況下,我們將只使用GET和POST請求類型。
GET可能是最常見的請求類型,它用于從服務器檢索數(shù)據(jù)。每次單擊鏈接或直接在瀏覽器中鍵入URL時,都會創(chuàng)建一個GET請求。
當我們想更改服務器上的數(shù)據(jù)時使用POST腾啥。向服務器發(fā)送數(shù)據(jù)桑谍,而這些數(shù)據(jù)會導致資源狀態(tài)的改變贿条,就應該總是通過POST請求來發(fā)送。
Django使用CSRF Token(Cross-Site Request Forgery Token)保護所有POST請求摄咆。這是一種安全措施涩金,以避免外部站點或應用程序向我們的應用程序提交數(shù)據(jù)全度。每次應用程序收到POST咨堤,它都會首先查找CSRF Token嗜暴。如果請求沒有令牌,或者令牌無效,它將丟棄這次請求的數(shù)據(jù)。
csrf_token模板標記的結果:
{% csrf_token %}
這個實際上是一個和表單數(shù)據(jù)一起提交的隱藏字段:
<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>
下面就是我們如何通過表單和字段名獲取指定的數(shù)據(jù):
subject = request.POST['subject']
message = request.POST['message']
創(chuàng)建新主題的請求響應方法就可以這樣實現(xiàn):
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: 獲取當前登錄的用戶臼婆,而不是使用數(shù)據(jù)庫中的第一個用戶
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})
這個請求響應方法只考慮了理想中的用戶輸入情況彩届,獲取到足夠的數(shù)據(jù)并寫入數(shù)據(jù)庫。但是實際上用戶可能有很多異常提交誓酒,這就需要我們對用戶提交的數(shù)據(jù)進行校驗樟蠕,例如,用戶提交的subject超過255個字符.
因為我們還沒有實現(xiàn)用戶登錄認證的功能靠柑,所以現(xiàn)在我們暴力獲取的數(shù)據(jù)庫中第一個用戶數(shù)據(jù)User。其實我們可以很容易就獲取到當前登錄的用戶,這部分我們在后面的教程中詳細講解钧排。同樣我們也還沒有實現(xiàn)主題Topic的頁面周叮,展示某一主題下的所有帖子Post啥箭,所以當我們新建主題成功后尊蚁,直接跳轉到版塊頁面。
點擊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屬性,這個屬性是由Django利用反向關系自動創(chuàng)建的。在前面我們創(chuàng)建了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
這行代碼的意思就是將我們通過pk
獲取的board
賦值給了新創(chuàng)建的主題實例的外鍵ForeignKey(Board)
户魏,這樣賦值以后琳袄,就將這個Topic實例關聯(lián)到了這個版塊Board實例上。
使用board.topics.all
而不是board.topics
的原因是board.topics
是一個關系管理器Related Manager纺酸,類似我們之前提到的用在board.objects
上的模型類管理器Model Manager窖逗。 所以如果要訪問該版塊下的的主題列表,就需要通過board.topics.all()
去訪問餐蔬。如果需要篩選的話碎紊,可以通過board.topics.filter(subject__contains='Hello')
這樣的方式去獲取指定的主題。
另外我們需要注意的是樊诺,在python語法里仗考,調用方法需要使用括號,例如:board.topics.all()
词爬。但是在Django模板編寫代碼時秃嗜,我們不使用括號,所以這里是board.topics.all
顿膨。
第二個問題就是關于外鍵ForeignKey
:
{{ topic.starter.username }}
通過符號點.
锅锨,我們幾乎可以訪問User模型的任何屬性。例如我們想要用戶的電子郵件恋沃,我們可以使用topic.starter.email
.
已經修改了topic.html模板必搞,讓我們再創(chuàng)建一個按鈕跳轉到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">
<!-- 中間的代碼略了,這里不要照抄 -->
</table>
{% endblock %}
讓我們在創(chuàng)建一個測試用例來確保這個按鈕能跳轉到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
∷≈蓿現(xiàn)在這個測試用例現(xiàn)在檢測所有的頁面跳轉是否正常塔橡。
測試表單頁面
在我們用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') # <- 注意這里
# ...
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())
現(xiàn)在tests.py這個測試文件開始變得越來越大霜第。后面我們會將它拆開來葛家,現(xiàn)在我們先把代碼寫在這里。
-
setUp
: 新增了User.objects.create_user
來創(chuàng)建User實例用于測試泌类。 -
test_csrf
: CSRF Token是POST請求的必要組成部分癞谒,所以我們必須保證所有網頁都需要包含它。 -
test_new_topic_valid_post_data
: 檢測是否發(fā)送有效的數(shù)據(jù)組合末誓,必須創(chuàng)建主題和帖子實例。 -
test_new_topic_invalid_post_data
: 檢測傳空數(shù)據(jù)是否按我們預想的進行響應书蚪。 -
test_new_topic_invalid_post_data_empty_fields
: 和前一個類似喇澡,我們傳其他的數(shù)據(jù)看應用程序是否校驗了數(shù)據(jù)有效性。
讓我們運行一下這個測試吧:
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讓這些測試通過。
使用Django Forms API創(chuàng)建表單
自從我們開始使用表單以來为流,我們已經走了很長的路呕屎,是時候使用forms API了。
Django的django.forms
模塊中提供了Forms API敬察。Django主要有兩種方式的表單:forms.Form
以及forms.ModelForm
秀睛。Form
類是一個通用的表單實現(xiàn),我們可以使用它來處理與應用程序中的模型沒有直接關聯(lián)的數(shù)據(jù)莲祸。而ModelForm
是Form
的子類蹂安,它與模型類關聯(lián)。
讓我們創(chuàng)建一個名為forms.py在boards'文件夾中:
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']
這是我們的第一個表單锐帜,它是一個與Topic模型相關聯(lián)的ModelForm
田盈。Meta類中fields
列表中的subject
是指Topic類中的subject
字段。現(xiàn)在缴阎,我們定義了一個名為message
的額外字段允瞧。這是指我們要保存的Post中的消息。
我們需要重構我們的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})
這就是我們使用Django表單的方式蛮拔,把無關的代碼屏蔽掉:
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建炫,則表示用戶正在向服務器提交一些數(shù)據(jù)贸典。所以我們實例化一個表單實例,將POST數(shù)據(jù)傳遞給表單:form=NewTopicForm(requst.POST)
踱卵。
然后廊驼,我們要求Django驗證數(shù)據(jù)据过,檢查表單是否有效,如果我們可以將其保存在數(shù)據(jù)庫中:if form.is_valid():
妒挎。如果表單有效绳锅,就將數(shù)據(jù)保存在數(shù)據(jù)庫中form.save()
,save()
方法返回保存到數(shù)據(jù)庫中的模型實例酝掩。由于這是一個Topic表單鳞芙,它將返回創(chuàng)建的Topic實例:topic = form.save()
。操作完成后常見的做法是將用戶重定向到其他地方期虾,既可以避免用戶按F5重新提交表單原朝,也可以保證應用程序的流程。
如果數(shù)據(jù)無效镶苞,Django將向表單添加一個錯誤列表喳坠,頁面不執(zhí)行任何操作,并在最后一條語句中返回錯誤: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不僅僅處理和驗證數(shù)據(jù)牍白,它還為我們生成HTML脊凰。
讓我們將new_topic.html改造一下,全部使用Django的表單:
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_table
茂腥、form.as_ul
笙各、form.as_p
,通過這些可以快速組織我們需要的數(shù)據(jù)础芍。正如方法名稱所表示的杈抢,as_table
使用table標簽來格式化輸入,as_ul
則直接生成HTML列表等等仑性。
讓我們看看它的樣子:
我們之前的界面看起來好多了惶楼,對吧?馬上我們就把它改酷炫诊杆。
現(xiàn)在看起來很零散歼捐,但相信我,這背后有很多東西晨汹,它功能非常強大豹储。如果表單有50個字段,則只需輸入{{ form.as_p }}
淘这。
而且剥扣,使用forms API后打厘,Django將驗證數(shù)據(jù)并向每個字段添加錯誤消息岖沛。讓我們嘗試提交一個空表單:
** 提示:**
當你提交信息時如果看見這個:那并不是Django的樣式斧散,這是瀏覽器自帶的格式校驗凭迹。可以添加novalidate
屬性來關閉這個樣式晦炊,如<form method="post" novalidate>
鞠鲜。
你可以保留這個標簽,沒有任何問題断国。這只是因為我們的表單現(xiàn)在非常簡單贤姆,而且我們沒有太多的數(shù)據(jù)驗證要看。
另一個需要注意的重要事項是:沒有所謂的客戶端驗證稳衬。JavaScript驗證或瀏覽器驗證只是為了可用性目的霞捡。同時還可以減少對服務器的請求數(shù)。數(shù)據(jù)驗證應該始終在服務器端完成宋彼,在服務器端我們應該是可以完全控制數(shù)據(jù)才能保證安全性弄砍。
它還可以自定義提示文案仙畦,可以在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 Forms樣式
讓我們優(yōu)化一下表單頁面吧输涕。
當使用Bootstrap或者其他前端框架時,我喜歡使用一個Django包django-widget-tweaks慨畸。它使我們能夠更好地控制渲染過程莱坎,保證不影響架構的情況下添加自定義擴展項目。讓我們先安裝這個工具:
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內置的弱卡,它屬于我們剛安裝的第三方框架乃正。使用這個標簽的前提必須在前面添加{% load widget_tweaks %}
,通過這種方式我們可以根據(jù)特定的條件分配類婶博。
下面是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" %}
現(xiàn)在讓我們重新實現(xiàn)一下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>
實現(xiàn)的結果就是:
這里我們有三種不同的狀態(tài):
- Initial state: 無數(shù)據(jù)狀態(tài)
-
Invalid: 我們添加
.is-invalid
的CSS class并且為它添加錯誤信息.invalid-feedback
,錯誤信息會被渲染成紅色凡人。 -
Valid: 我們添加
.is-valid
CSS class名党,驗證通過后會渲染成綠色告知用戶可以繼續(xù)填寫。
可復用的表單模板
模板代碼看起來有點復雜挠轴,對吧传睹?好消息是我們可以在整個項目中重用這個片段。
在templates文件夾下岸晦,創(chuàng)建一個新的文件夾includes:
myproject/
|-- myproject/
| |-- boards/
| |-- myproject/
| |-- templates/
| | |-- includes/ <-- 這里!
| | |-- base.html
| | |-- home.html
| | |-- new_topic.html
| | +-- topics.html
| +-- manage.py
+-- venv/
再在includes文件夾下創(chuàng)建一個文件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模板欧啤,這里我們加載剛創(chuàng)建的表單模板睛藻。
下一個需要實現(xiàn)表單的頁面上,只需要加上{% include 'includes/form.html' %}
就可以去自動渲染表單了堂油。
增加更多的測試用例
現(xiàn)在我們使用Django自己的Forms API了修档,讓我們寫一些測試用例來測試它吧:
boards/tests.py
# ... other imports
from .forms import NewTopicForm
class NewTopicTests(TestCase):
# ... other tests
def test_contains_form(self): # <- 新增
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): # <- 更新
'''
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
,這里我們從context數(shù)據(jù)中獲取表單實例府框,并檢查它是否是NewTopicForm
吱窝。而在我們以前的測試用例中,我們增加了self.assertTrue(form.errors)
來確保表單數(shù)據(jù)無效時會顯示錯誤迫靖。
小結
在本教程中院峡,我們重點介紹url、可重用模板和表單系宜。和往常一樣照激,還實現(xiàn)了幾個測試用例,這就是開發(fā)健壯性的基石盹牧。
我們的測試文件開始變得越來越大俩垃,所以在下一個教程中,我們將對其進行重構以提高可維護性汰寓,從而維持代碼庫的健康成長口柳。
我們還需要與登錄用戶進行交互。在下一個教程中有滑,我們將學習有關身份驗證的所有內容以及如何保護我們的數(shù)據(jù)跃闹。
項目的源代碼可以在GitHub上找到。項目的當前狀態(tài)可以在發(fā)布標簽v0.3-lw下找到毛好。下面的鏈接將帶您找到正確的位置:
https://github.com/sibtc/django-beginners-guide/tree/v0.3-lw
上一節(jié):Django初學者入門指南2-基礎知識(譯&改)
下一節(jié):Django初學者入門指南4-安全認證(譯&改)