撰寫你的第一個 Django 應用程式,第三部分¶
本教學從教學 2結束的地方開始。我們將繼續開發網路投票應用程式,並將重點放在建立公共介面 – 「視圖」。
在哪裡取得協助
如果您在學習本教學時遇到困難,請前往常見問題的取得協助部分。
概觀¶
視圖是 Django 應用程式中網頁的「類型」,通常具有特定功能和特定範本。例如,在部落格應用程式中,您可能會看到以下視圖
部落格首頁 – 顯示最新的幾篇文章。
文章「詳細」頁面 – 單一文章的永久連結頁面。
以年為基礎的封存頁面 – 顯示指定年份中所有有文章的月份。
以月為基礎的封存頁面 – 顯示指定月份中所有有文章的天數。
以天為基礎的封存頁面 – 顯示指定日期中的所有文章。
留言動作 – 處理發布給定文章的留言。
在我們的投票應用程式中,我們將有以下四個視圖
問題「索引」頁面 – 顯示最新的幾個問題。
問題「詳細」頁面 – 顯示問題文字,沒有結果,但帶有投票表單。
問題「結果」頁面 – 顯示特定問題的結果。
投票動作 – 處理特定問題中特定選項的投票。
在 Django 中,網頁和其他內容由視圖提供。每個視圖都由一個 Python 函數(或方法,在基於類的視圖的情況下)表示。Django 會透過檢查請求的 URL(準確來說,是網域名稱後面的 URL 部分)來選擇視圖。
現在,您在網路上可能已經遇到過像 ME2/Sites/dirmod.htm?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B
這樣的奇特 URL。您會很高興知道 Django 允許我們使用比這更優雅的URL 模式。
URL 模式是 URL 的一般形式 – 例如:/newsarchive/<year>/<month>/
。
為了從 URL 取得視圖,Django 使用所謂的「URLconf」。URLconf 將 URL 模式對應到視圖。
本教學提供 URLconf 使用的基本說明,您可以參考URL 調度器以取得更多資訊。
撰寫更多視圖¶
現在讓我們在 polls/views.py
中新增更多視圖。這些視圖稍微不同,因為它們需要一個參數
polls/views.py
¶def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
透過新增下列path()
呼叫,將這些新視圖連線到 polls.urls
模組中
polls/urls.py
¶from django.urls import path
from . import views
urlpatterns = [
# ex: /polls/
path("", views.index, name="index"),
# ex: /polls/5/
path("<int:question_id>/", views.detail, name="detail"),
# ex: /polls/5/results/
path("<int:question_id>/results/", views.results, name="results"),
# ex: /polls/5/vote/
path("<int:question_id>/vote/", views.vote, name="vote"),
]
在您的瀏覽器中查看「/polls/34/」。它會執行 detail()
函式並顯示您在 URL 中提供的任何 ID。嘗試「/polls/34/results/」和「/polls/34/vote/」 – 這些會顯示預留位置的結果和投票頁面。
當有人從您的網站請求頁面時 – 例如,「/polls/34/」,Django 會載入 mysite.urls
Python 模組,因為它是由 ROOT_URLCONF
設定所指向的。它會找到名為 urlpatterns
的變數,並依序走訪這些模式。在 'polls/'
找到相符項後,它會移除相符的文字("polls/"
),並將剩餘的文字 – "34/"
– 送至「polls.urls」URLconf 進行進一步處理。在那裡,它會比對 '<int:question_id>/'
,導致呼叫 detail()
視圖,如下所示
detail(request=<HttpRequest object>, question_id=34)
question_id=34
部分來自 <int:question_id>
。使用角括號「擷取」URL 的一部分,並將其作為關鍵字引數傳送給視圖函式。字串的 question_id
部分定義將用於識別相符模式的名稱,而 int
部分是一個轉換器,決定哪些模式應符合 URL 路徑的這部分。冒號 (:
) 分隔轉換器和模式名稱。
撰寫實際執行某些操作的視圖¶
每個視圖都負責執行以下兩件事之一:傳回包含所請求頁面內容的HttpResponse
物件,或引發例外,例如Http404
。剩下的就取決於您。
您的視圖可以從資料庫讀取記錄,也可以不讀取。它可以使用範本系統(例如 Django 的系統)或第三方 Python 範本系統,也可以不使用。它可以使用任何您想要的 Python 程式庫來產生 PDF 檔案、輸出 XML、即時建立 ZIP 檔案,任何您想要的都可以。
Django 唯一的要求是 HttpResponse
。或者例外。
由於方便起見,讓我們使用 Django 自己的資料庫 API,我們在教學 2中介紹過。以下是新 index()
視圖的一個版本,它會依照發布日期,以逗號分隔顯示系統中最新的 5 個投票問題
polls/views.py
¶from django.http import HttpResponse
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5]
output = ", ".join([q.question_text for q in latest_question_list])
return HttpResponse(output)
# Leave the rest of the views (detail, results, vote) unchanged
不過,這裡有一個問題:頁面的設計硬式編碼在視圖中。如果您想要變更頁面的外觀,您必須編輯這個 Python 程式碼。因此,讓我們使用 Django 的範本系統,透過建立視圖可以使用的範本,將設計與 Python 分隔開來。
首先,在您的 polls
目錄中建立一個名為 templates
的目錄。Django 將在那裡尋找範本。
您的專案的 TEMPLATES
設定描述 Django 如何載入和呈現範本。預設設定檔設定一個 DjangoTemplates
後端,其 APP_DIRS
選項設定為 True
。依照慣例,DjangoTemplates
會在每個INSTALLED_APPS
中尋找「templates」子目錄。
在您剛建立的 templates
目錄中,建立另一個名為 polls
的目錄,然後在其中建立一個名為 index.html
的檔案。換句話說,您的範本應該位於 polls/templates/polls/index.html
。由於上述 app_directories
範本載入器的工作方式,您可以在 Django 中將此範本稱為 polls/index.html
。
範本命名空間
現在,我們可能可以直接將範本放在 polls/templates
中(而不是建立另一個 polls
子目錄),但這實際上是一個壞主意。Django 會選擇它找到的第一個名稱符合的範本,如果您在不同的應用程式中有一個名稱相同的範本,Django 將無法區分它們。我們需要能夠將 Django 指向正確的範本,而確保這一點的最佳方式是透過命名空間。也就是說,將這些範本放在另一個以應用程式本身命名的目錄中。
將以下程式碼放入該範本中
polls/templates/polls/index.html
¶{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
注意
為了縮短教學時間,所有範本範例都使用不完整的 HTML。在您自己的專案中,您應該使用完整的 HTML 文件。
現在讓我們更新 polls/views.py
中的 index
視圖,以使用樣板。
polls/views.py
¶from django.http import HttpResponse
from django.template import loader
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5]
template = loader.get_template("polls/index.html")
context = {
"latest_question_list": latest_question_list,
}
return HttpResponse(template.render(context, request))
這段程式碼載入名為 polls/index.html
的樣板,並傳遞一個上下文(context)。上下文是一個字典,將樣板變數名稱對應到 Python 物件。
透過將瀏覽器指向「/polls/」來載入頁面,您應該會看到一個項目符號清單,其中包含來自教學 2的「What’s up」問題。該連結指向問題的詳細頁面。
一個捷徑:render()
¶
載入樣板、填入上下文並傳回一個 HttpResponse
物件,其中包含渲染樣板的結果,這是一個非常常見的慣例。Django 提供了一個捷徑。以下是重新編寫的完整 index()
視圖:
polls/views.py
¶from django.shortcuts import render
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5]
context = {"latest_question_list": latest_question_list}
return render(request, "polls/index.html", context)
請注意,一旦我們在所有這些視圖中完成此操作,我們就不再需要匯入 loader
和 HttpResponse
(如果您的 detail
、results
和 vote
仍然有存根方法,您會想要保留 HttpResponse
)。
render()
函數將請求物件作為其第一個參數,樣板名稱作為其第二個參數,以及一個字典作為其可選的第三個參數。它會傳回一個 HttpResponse
物件,其中包含使用給定上下文渲染的給定樣板。
引發 404 錯誤¶
現在,讓我們處理問題詳細視圖 - 顯示給定投票問題文字的頁面。以下是視圖:
polls/views.py
¶from django.http import Http404
from django.shortcuts import render
from .models import Question
# ...
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, "polls/detail.html", {"question": question})
這裡的新概念:如果請求的 ID 問題不存在,則視圖會引發 Http404
例外。
我們稍後會討論您可以放入 polls/detail.html
樣板的內容,但如果您想快速讓上面的範例運作,只需包含以下內容的檔案:
polls/templates/polls/detail.html
¶{{ question }}
現在就可以讓您開始了。
一個捷徑:get_object_or_404()
¶
使用 get()
,如果物件不存在,則引發 Http404
是一個非常常見的慣例。Django 提供了一個捷徑。以下是重新編寫的 detail()
視圖:
polls/views.py
¶from django.shortcuts import get_object_or_404, render
from .models import Question
# ...
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, "polls/detail.html", {"question": question})
get_object_or_404()
函數將 Django 模型作為其第一個參數,以及任意數量的關鍵字引數,這些引數會傳遞到模型管理器的 get()
函數。如果物件不存在,它會引發 Http404
。
哲學
為什麼我們使用輔助函數 get_object_or_404()
,而不是在較高的層級自動捕獲 ObjectDoesNotExist
例外,或者讓模型 API 引發 Http404
而不是 ObjectDoesNotExist
呢?
因為這會將模型層耦合到視圖層。Django 的首要設計目標之一是保持鬆散耦合。django.shortcuts
模組中引入了一些受控制的耦合。
還有一個 get_list_or_404()
函數,其運作方式與 get_object_or_404()
相同,只是使用 filter()
而不是 get()
。如果清單為空,它會引發 Http404
。
使用樣板系統¶
回到投票應用程式的 detail()
視圖。給定上下文變數 question
,以下是 polls/detail.html
樣板的可能外觀:
polls/templates/polls/detail.html
¶<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
樣板系統使用點查找語法來存取變數屬性。在 {{ question.question_text }}
的範例中,Django 首先對物件 question
執行字典查找。如果失敗,則會嘗試屬性查找 - 在此案例中有效。如果屬性查找失敗,它會嘗試清單索引查找。
方法調用發生在 {% for %}
迴圈中:question.choice_set.all
會被解譯為 Python 程式碼 question.choice_set.all()
,其會傳回一個 Choice
物件的可迭代物件,並適合在 {% for %}
標籤中使用。
請參閱樣板指南以取得更多關於樣板的資訊。
移除樣板中的硬編碼 URL¶
請記住,當我們在 polls/index.html
樣板中撰寫問題的連結時,該連結部分硬編碼,如下所示:
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
這種硬編碼、緊密耦合的方法的問題是,在具有大量樣板的專案中變更 URL 會變得具有挑戰性。但是,由於您在 polls.urls
模組中的 path()
函數中定義了 name
引數,因此您可以使用 {% url %}
樣板標籤來移除對 URL 設定中定義的特定 URL 路徑的依賴。
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
其運作方式是查找 polls.urls
模組中指定的 URL 定義。您可以在下方看到明確定義「detail」URL 名稱的位置:
...
# the 'name' value as called by the {% url %} template tag
path("<int:question_id>/", views.detail, name="detail"),
...
如果您想要將投票詳細視圖的 URL 變更為其他內容,例如變更為類似 polls/specifics/12/
而不是在樣板(或多個樣板)中進行變更,您可以在 polls/urls.py
中變更它:
...
# added the word 'specifics'
path("specifics/<int:question_id>/", views.detail, name="detail"),
...
命名空間 URL 名稱¶
教學專案只有一個應用程式,即 polls
。在實際的 Django 專案中,可能會有多達五個、十個、二十個或更多的應用程式。Django 如何區分它們之間的 URL 名稱?例如,polls
應用程式具有 detail
視圖,而同一個專案上的一個網誌應用程式也可能具有該視圖。當使用 {% url %}
樣板標籤時,如何讓 Django 知道要為 URL 建立哪個應用程式視圖?
答案是將命名空間新增至您的 URLconf。在 polls/urls.py
檔案中,新增一個 app_name
來設定應用程式命名空間:
polls/urls.py
¶from django.urls import path
from . import views
app_name = "polls"
urlpatterns = [
path("", views.index, name="index"),
path("<int:question_id>/", views.detail, name="detail"),
path("<int:question_id>/results/", views.results, name="results"),
path("<int:question_id>/vote/", views.vote, name="vote"),
]
現在將您的 polls/index.html
樣板從:
polls/templates/polls/index.html
¶<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
變更為指向命名空間的詳細視圖:
polls/templates/polls/index.html
¶<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
當您熟悉撰寫視圖時,請閱讀本教學課程的第 4 部分,以了解表單處理和通用視圖的基本知識。