完整的Django入門指南學習筆記3
前言
在本節課中,我們將深入理解兩個基本概念: URLs 和 Forms。在這個過程中,我們還將學習其它很多概念,如建立可重用模板和安裝第三方庫。同時我們還將編寫大量單元測試。
如果你是從這個系列教程的 part 1 跟著這個教程一步步地編寫專案,你可能需要在開始之前更新 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='+')
現在在已經啟用的 virtualenv 環境中執行命令:
python manage.py makemigrations python manage.py migrate
如果在你的程式中 update_by
欄位中已經有了 null=True
且 last_updated
欄位中有了 auto_now_add=True
,你可以放心地忽略上面這步操作。
如果你更喜歡使用我的程式碼作為出發點,你可以在 GitHub 上找到它。本專案現在的程式碼,可以在 v0.2-lw 標籤下找到。下面是連結:
https://github.com/sibtc/django-beginners-guide/tree/v0.2-lw
我們的開發就從這裡開始。
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>[\[email protected]+-]+)/$', 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>[\[email protected]+-]+)/$', 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>[\[email protected]+-]+) |
舉例 | url(r'^profile/(?P<username>[\[email protected]+-]+)/$', 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. --&g