範例:投票選項(1)
-
<style> .markdown-body img:not(.emoji) { border-radius: 5px; box-shadow: 1px 1px 5px dimgrey; } </style>
【 範例】線上投票-視圖、網址、範本
使用者透過瀏覽器存取 Django 專案網站服務時,大致流程如下圖:
- Django 專案服務接收到來自使用者瀏覽器發出的網頁請求,便搜尋已定義的路徑規則,看是否有對應的視圖可以處理網頁請求,並依符合的路徑規則上的定義,擷取參數,再交由視圖處理
- 視圖(views)接手網址處理器的後續工作,在此進行程式邏輯作業。若需要與資料庫溝通,則透過 `models.py` 內定義的資料模型,將回應所需的資料準備好之後,載入相對應的頁面範本,將資料轉成所需的樣態後,再回應給使用者瀏覽器前一篇我們透過 Django 內建的管理後臺事先建立了幾筆測試資料,接下來著手撰寫幾個功能,帶大家看一下網址(urls)、視圖(views)、範本是如何使用的。
我們要建立第一個網頁,開啟 `poll/urls.py` ,修改為以下程式碼。
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('default.urls')), ]
- 修改++第 18 行++,從 `django.urls` 增加引用 `include` 這個函式
- 新增++第 21 行++,將 `default` 這個應用程式所定義的路徑規則包含進專案的路徑規則內投票主題列表
網址
新增檔案 `default/urls.py` ,定義應用程式 `default` 的路徑規則。
from django.urls import path from . import views urlpatterns = [ path('poll/', views.PollList.as_view()), ]
視圖
開啟 `default/views.py` ,新增++第 2 - 8 行++:
from django.shortcuts import render from django.views.generic import ListView from .models import * # 投票主題列表 class PollList(ListView): model = Poll
- 新增++第 2 行++,引用 Django 內建的通用視圖類別 `ListView`
- 新增++第 3 行++,從目前模組(`.`)的 `models` 引用所有資料表的定義
- 新增++第 6-7 行++,自行定義一個名為 `PollList` 的類別繼承自 Django 的 `ListView` 類別,它所參考的資模型是 `Poll`
- `django.views.generic` 裡定義了一些通用的視圖類別,可以用來協助我們對資料庫進行溝通與處理,其中 `ListView` 就是用來從資料庫中篩選記錄清單,再交由頁面範本呈現
- 這些通用的視圖類別的 `model` 屬性是用來指定欲操作的資料模型
- `ListView` 的衍生類別,在未指定頁面範本的情況之下,預設會到範本資料夾的「定義參考資料模型的應用程式」資料夾下,取用名為「資料模型_list.html」來當作回應時的頁面範本
- 使用的資料模型 `Poll` 是定義在 `default` 這個應用程式之下
- 預設使用的頁面範本即為 `default/poll_list.html`範本
頁面範本要放在哪裡?
在專案的 settings.py 設定檔中有一段關於頁面範本處理的設定值:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
- ++第 58 行++的 `DIRS` 可以指定頁面範本的搜尋資料夾,如果想要將整個專案的頁面範本集中統一管理的話,可以在這裡指定要搜尋的路徑
- ++第 59 行++,`APP_DIRS` 是用來設定是否要自動搜尋應用程式內的 `templates` 資料夾做為頁面範本的搜尋路徑,預設是啟用的。因此我們可以直接在 `default` 這個應用程式的資料夾中新增一個 `templates` 資料夾,用來存放頁面範本。
1. 建立頁面範本資料夾 `default/templates` 再建立其下的 `default` 資料夾
2. 為了維持整個網站頁面間外觀的一致性,先新增網站頁面範本檔案 `poll/default/templates/base.html`:<!DOCTYPE html> <html lang="zh-hant"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>線上投票</title> </head> <body> <div id="main-content"> {% block content %}{% endblock %} </div> </body> </html>
- 頁面範本基本上就是 HTML 檔,但是在裡面放置一些有特殊意義的標記,用來對視圖傳入的資料進行處理
- ++第 7 行++利用 Django 的範本標記定義一個名為 `content` 的區塊,其預設值為空。 `{% block %}` 與 `{% endblock %}` 標記必須成對出現。
- 在這邊定義的區塊,其內容可以被繼承它的頁面範本所覆蓋。頁面上依需求可定義多個不同名稱的區塊。
3. 新增投票主題列表頁面範本 `default/templates/default/poll_list.html`{% extends "base.html" %} {% block content %} <h1>投票主題</h1> <ul> {% for poll in poll_list %} <li>{{ poll.date_created }} <a href="{{ poll.id }}/">{{ poll.subject }}</a></li> {% endfor %} </ul> {% endblock %}
- ++第 1 行++,指明這個頁面範本要參照 `base.html` 並對其進行擴充(extends)
- ++第 3-12 行++,這邊又看到一組 `{% block content %}` 以及 `{% endblock %}` 的組合,重新定義了名為 `content` 的區塊的內容,而這個 `content` 區塊將會取代原本 `base.html` 範本中所定義的 `content` 區塊的內容。
- ++第 6-8 行++,以一個迴圈將 `poll_list` 清單的項目逐一取出,在每一回合都將取得的項目命名為 `poll`。`poll_list` 是視圖中的 `PollList` 產生出來的清單,並自動傳給 `poll_list.html` 這個範本進行處理產生結果頁面
- ++第 7 行++,在 Django 的頁面範本中是以 `{{ }}` 表示要輸出變數的內容,例如:`{{ poll.date_created }}` 的意思是要在此處輸出 `poll` 的 `date_created` 欄位的內容【說明】
通用視圖在未特別指定頁面範本的情況下,會自動將所處的其所參考的資料模型所屬的應用程式名稱加在頁面範本檔案之前。以本例來說,`Poll` 是在定義在 `default` 應用程式之下,繼承 `ListView` 的 `PollList`,預設會到範本搜尋路徑取用 `default/poll_list.html` 當做頁面範本。
【說明】
在專案設定值未指定範本搜尋路徑,而 `APP_DIRS` 被啟用的狀況下,會自動將 `default` 應用程式資料夾下的 `templates` 加入搜尋路徑,因此 `poll_list.html` 可以被放在 `default/templates/default/` 底下。
啟動網站,在網址列輸入 `http://localhost/poll/` 會出現以下頁面:
投票主題檢視
接著來實作投票主題檢視的功能。前面投票主題列表的頁面上,我們為每個投票主題安置了一個連結 `<a href="{{ poll.id }}/">{{ poll.subject }}</a>`,因此,點按頁面上的連結時,會連結到 `/poll/投票主題編號/` 的路徑。
絕對路徑與相對路徑檢視投票主題的連結標籤 `href` 屬性寫的明明是 `{{ poll.id }}/`,看起來應該是連到 `投票主題編號/`,為什麼說會連結到 `/poll/投票主題編號/` 的路徑? 這就跟瀏覽器在處理絕對路徑與相對路徑有關係了。
- **絕對路徑**
- 連結的目標路徑以斜線(`/`)開頭,會直接取代瀏覽器網址列主機後方的路徑。
- 例:目前的頁面網址為 `https://www.example.com/path1/dir1/page1`,點按頁面上的連結,其目標為 `/path2/dir2/page2` 的話,會瀏覽 `https://www.example.com/path2/dir2/page2`
- **相對路徑**
- 連結的目標路徑非以斜線開頭,表示為相對目前所在頁面的路徑,會將瀏覽器目前網址列最後一個斜線(`/`)以後的路徑取代成指定的路徑
- 例:目前的頁面網址為 `https://www.example.com/path1/dir1/page1`,點按頁面上的連結,其目標為 `path2/dir2/page2` 的話,會瀏覽 `https://www.example.com/path1/dir1/path2/dir2/page2`因為投票主題列表頁面的路徑為 `/poll/` 而頁面上的每個投票主題的連結目標為 `投票主題編號/`,因此它實際連結到的頁面路徑就為 `/poll/投票主題編號/`。
網址
修改檔案 `default/urls.py`:
urlpatterns = [ path('poll/', views.PollList.as_view()), path('poll/<int:pk>/', views.PollDetail.as_view()), ]
- 新增++第 6 行++,指定 `poll/整數/` 這樣的路徑請求交由 `views` 的 `PollDetail` 來處理
- `<int:pk>` 表示路徑的這個部份若是整數(int)的話,將這部份擷取出來,當成所對應視圖(View)的參數,而這個參數命名為 `pk`
- 若使用者以 `poll/1234/` 來存取網站,會符合這個路徑規則,而 `1234` 會被擷取出來轉成整數並存入 `pk` 中
- 若使用者以 `poll/abcd/` 來存取網站,因為 `abcd` 並不是整數,因此這個請求沒有相對應的路徑規則,Django會回傳錯誤代碼 404 表示網站無此頁面視圖
修改檔案 `default/views.py`:
from django.shortcuts import render from django.views.generic import ListView, DetailView from .models import *
- 修改++第 2 行++,從 `django.views.generic` 另外再多引用 `DetailView` 這個通用視圖類別
# 投票主題檢視 class PollDetail(DetailView): model = Poll # 取得額外資料供頁面範本顯示 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) options = Option.objects.filter(poll_id=self.kwargs['pk']) context['options'] = options return context
- 新增++第 10-17 行++,新增定義一個名為 `PollDetail` 的類別繼承自 Django 的 `DetailView` 類別,它所參考的資模型是 `Poll`
- `django.views.generic` 裡定義的 `DetailView` 是用來從資料庫中找到特定一筆記錄,再交由頁面範本呈現。至於是哪一筆特定記錄,還記得前面在新增了一條 `poll/<int:pk>/` 的路徑規則嗎? `DetailView` 預設會透過 `pk` 參數,將其視為要尋找的紀錄的主鍵值(Primary Key),從資料庫找把主鍵值為與 `pk` 相等的那筆記錄找出來。
- ++第 13-17 行++,由於在檢視投票主題的時候,需要列出這個主題的投票選項,透過定義 `get_context_data()` 方法,將額外的資料傳遞給頁面範本。`context` 中包含要傳遞給頁面範本的資料。
- ++第 14 行++,先透過親類別(Parent Class)定義的 `get_context_data()` 來取得 `context`。因為 `PollDetail` 繼承自 `DetailView`,因此這行的意思是先透過 `DetailView` 中定義的 `get_context_data()` 方法來取得 `context`。
- ++第 15 行++,從 `Option` 資料表中,篩選出所有 `poll_id` 欄位值與參數 `pk` 相等的記錄,並指定給 `options`
- ++第 16 行++,在 `context` 中增加一個項目 `option_list`,其值為++第 15 行++所取得的 `options`。
- ++第 17 行++,回傳修改後的 `context`,交由頁面範本處理輸出
- 因為參考的資料模型是 `Poll`,繼承的視圖為 `DetailView`,預設會自動載入 `poll_detail.html` 這個頁面範本來產生結果頁面範本
新增投票主題檢視頁面範本檔案 `default/templates/default/poll_detail.html`
{% extends "base.html" %} {% block content %} <h1>{{ poll.subject }}</h1> <ul> {% for option in options %} <li>{{ option.title }}</li> {% endfor %} </ul> {% endblock %}
投票
這個範例的投票動作很簡單,不記錄投票的其他相關資訊,例:投票者、投票時間...等,僅單純增加票數而已。因此之前在定義資料模型的時候僅透過 `Option` 中的 `count` 欄位來記錄選項被投了幾次。
投票的流程簡化如下:在檢視投票主題時,可以直接點選該主題下的投票選項進行投票,投票後自動返回所屬投票主題檢視頁面。
網址
修改`default/urls.py`:
urlpatterns = [ path('poll/', views.PollList.as_view()), path('poll/<int:pk>/', views.PollDetail.as_view()), path('option/<int:pk>/', views.PollVote.as_view()), ]
- 新增++第 7 行++,當使用者的存取路徑是 `option/整數/` 的型式,則對應到 `PollVote` 進行後續處理。
視圖
修改 `default/views.py`:
from django.shortcuts import render from django.views.generic import ListView, DetailView, RedirectView from .models import *
- 修改++第 2 行++,從 `django.views.generic` 額外引用 `RedirectView`。
- `RedirectView` 的作用是轉址到其他頁面,由於投完票之後要自動返回所屬投票主題,因此透過 `RedirectView` 來實做投票增加票數後轉址回該選項所屬投票主題頁面的功能。## 投票 class PollVote(RedirectView): def get_redirect_url(self, *args, **kwargs): option = Option.objects.get(id=self.kwargs['pk']) option.count += 1 # 將選項的票數+1 option.save() # 儲存至資料庫 return "/poll/"+str(option.poll_id)+"/"
- 新增++第 19-25 行++,自訂 `PollVote` 繼承自 `RedirectView` 通用視圖類別
- ++第 21 - 25 行++,定義 `get_redirect_url()` 方法,用來回傳要轉址的目標路徑。
- ++第 22 行++,從 `Option` 資料模型中取得一筆資料,它的 `id` 欄位值必須要與網址處理器擷取的 `pk` 參數相同,取得的這筆記錄記在 `option`
- ++第 23 行++,將 `option` 這筆記錄中的 `count` 欄位的值遞增 1 再放回 `count` 欄位中,效果等同 `option.count = option.count + 1`
- ++第 24 行++,將上一行修改過的內容,寫回資料庫
- ++第 25 行++,回傳要轉址的目標路徑【說明】
如果要轉址的目的頁面是固定不變的,不需要定義 `get_redirect_url()` 這個方法,可以直接指定 `redirect_url` 這個屬性的值就可以了,例:
class StaticRedirectView(RedirectView): redirect_url = "/"
範本
在增加了票數之後,就直接轉址回所屬的投票主題檢視頁面,所以這個功能不需要定義頁面範本。
但是要修改投票主題檢視的頁面範本,將投票的功能的連結補上。修改 `default/templates/default/poll_detail.html`
{% extends "base.html" %} {% block content %} <h1>{{ poll.subject }}</h1> <div>小提示!直接按選項文字就可以投票囉!</div> <ul> {% for option in options %} <li> <a href="/option/{{ option.id }}/">{{ option.title }}</a> : {{ option.count }} 票 </li> {% endfor %} </ul> {% endblock %}
- 修改++第 9 行++,將原本的選項文字做成連結,並加上票數。