撰寫你的第一個 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)

請注意,一旦我們在所有這些視圖中完成此操作,我們就不再需要匯入 loaderHttpResponse(如果您的 detailresultsvote 仍然有存根方法,您會想要保留 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 部分,以了解表單處理和通用視圖的基本知識。

回到頂部