在本節課中,咱們將深刻理解兩個基本概念: 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
隨着咱們項目的開發,咱們須要實現一個新的功能,就是列出某個板塊下的全部主題列表,再來回顧一下,你能夠看到上一節中咱們畫的線框圖。正則表達式
咱們將從 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): # ...
基礎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'})
更高級的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.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 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 類的快速總結:
運行測試:
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.