範例:投票選項(2)
-
<style> .markdown-body img:not(.emoji) { border-radius: 5px; box-shadow: 1px 1px 5px dimgrey; } </style>
範例:線上投票-使用者表單
在前面的章節中使用的 Django 內建的後臺管理界面對自訂的資料模型 `Poll` 進行操作。但後臺管理界面較偏向一般化的操作,可能無法滿足我們對操作流程的要求,此時可以透過使用者表單在頁面上收集使用者的輸入值,藉此對資料模型所對應的資料表進行新增、刪除、修改等操作。
由於這個範例的資料比較單純,我們可以透過 Django 的通用編輯視圖(Generic editing views)類別來進行資料的「增(Create)」、「刪(Delete)」、「修(Update)」的動作。
新增投票主題
路徑
修改 `default/urls.py`,增加路徑規則。
from django.urls import path from . import views urlpatterns = [ path('poll/', views.PollList.as_view()), path('poll/<int:pk>/', views.PollDetail.as_view()), path('option/<int:pk>/', views.PollVote.as_view()), path('poll/create/', views.PollCreate.as_view()), ]
- 新增++第 8 行++,讓 `poll/create/` 對應到新增投票主題的視圖。
視圖
修改 `default/views.py`:
from django.shortcuts import render from django.views.generic import ListView, DetailView, RedirectView from .models import * from django.views.generic import CreateView, UpdateView, DeleteView
- 新增++第 4 行++,引用 `CreateView`, `UpdateView` 以及 `DeleteView`
# 新增投票主題 class PollCreate(CreateView): model = Poll fields = ['subject'] # 指定要顯示的欄位 success_url = '/poll/' # 成功新增後要導向的路徑
- ++第 28-32 行++,新增 `PollCreate` 繼承自 Django 的通用編輯視圖 `CreateView`
- ++第 30 行++,指定要參考操作的對象為 `Poll` 這個資料模型。`model` 屬性為必填項目,若不指定會顯示錯誤。
- ++第 31 行++,指定在表單上只要顯示 `subject` 欄位。通用編輯視圖的 `fields` 屬性是用來指定自動產生表單時要包含資料模型的哪些欄位,若僅要顯示部份欄位的話,只要將欲顯示的欄位名稱一一列在其後即可。若要包含全部欄位,則可將其值設為 `'__all__'`,例: `fields = '__all__'`。此屬性亦為必填項目,省略會顯示錯誤。
- ++第 32 行++,指定成功新增記錄後要轉址到的路徑,在本範例中,新增投票主題後會返回投票主題列表頁面(`/poll/`)。【說明】
通用視圖 `CreateView` 的衍生類別預設會以 `應用程式/資料模型_form.html` 作為頁面範本,以此列來說,預設範本檔案為 `default/poll_form.html`。如果想要指定頁面範本,可以透過 `template_name` 屬性來指定,例如:
class PollCreate(CreateView): model = Poll fields = '__all__' success_url = '/poll/' template_name = 'my_form.html'
範本
新增通用表單範本檔案 `default/templates/default/poll_form.html`
{% extends "base.html" %} {% block content %} <form action="" method="post"> {% csrf_token %} <table> {{ form.as_table }} </table> <input type="submit" value="送出"> </form> {% endblock %}
- ++第 4, 10 行++,`<form>` ... `</form>` 是用來在頁面上標記表單,它的 `action` 屬性用來指定使用者填寫的表單內容送出後要傳遞給那個網址進行處理,若不指定表示由原網址處理。另外 `method` 屬性則控制了資料遞送的方式,主要有 `get` 與 `post` 兩種,`get` 在表單送出後將資料以參數的形式附加在網址之後傳遞給處理程式,例: `/poll/create/?subject=班草大調查` 而 `post` 則是將表單資料當成 HTTP 請求的檔頭(Header)的一部份送出。
- ++第 5 行++,在此插入 CSRF Token,用驗證是否為偽照的表單資料,以增進表單的安全性。
- ++第 6, 8 行++,在頁面上標記此處為表格
- ++第 7 行++,`{{ form.as_table }}` 用來將視圖傳遞進來的 `form` 元件,以表格的形式一一列出所有的欄位
- ++第 9 行++,表單的送出按鈕。`<input>` 標籤是 HTML 的表單輸入元件,它的 `type` 屬性可指定輸入元件的類型,`submit` 這種輸入元件外觀上就是一顆按鈕,使用者點選後可將表單填寫的資料送出。而它的 `value` 屬性則是該按鈕上要顯示的文字。
CSRF token 是什麼?CSRF 是一種 Web 上的攻擊手法,全稱是 Cross Site Request Forgery,跨站請求偽造,簡單的說就是在別的網站上偽裝為本網站使用者的編輯操作,造成資料被竄改、刪除...等。網站透過 CSRF Token 的使用,可以檢驗請求的真實性,以確保網站的資料安全。細節可參考以下網站說明:
讓我們來談談 CSRF | TechBridge 技術共筆部落格(https://blog.techbridge.cc/2017/02/25/csrf-introduction)
為了方便使用功能,需要在頁面上放置新增投票主題的連結。假設我們要將這個功能放在投票主題列表的頁面上。
開啟 `default/templates/default/poll_list.html`,修改為以下程式碼。
{% extends "base.html" %} {% block content %} <h1>投票主題</h1> <a href="/poll/create/">新增投票主題</a> <ul> {% for poll in poll_list %} <li>{{ poll.date_created }} <a href="{{ poll.id }}/">{{ poll.subject }}</a></li> {% endfor %} </ul> {% endblock %}
- 新增++第 5 行++,在投票主題列表的頁面上加上「新增投票主題」的連結
修改投票主題(Update)
網址
開啟 `default/urls.py`,增加++第 9 行++的路徑規則:
from django.urls import path from . import views urlpatterns = [ path('poll/', views.PollList.as_view()), path('poll/<int:pk>/', views.PollDetail.as_view()), path('option/<int:pk>/', views.PollVote.as_view()), path('poll/create/', views.PollCreate.as_view()), path('poll/<int:pk>/update/', views.PollUpdate.as_view()), ]
- ++第 9 行++,將 `poll/整數/update/` 的請求交由 `PollUpdate` 處理,並將路徑中的整數部份擷取出來當成參數,命名為 `pk`。
視圖
開啟 `default/views.py`,增加以下程式碼:
# 修改投票主題 class PollUpdate(UpdateView): model = Poll fields = ['subject'] # 指定要顯示的欄位 success_url = '/poll/' # 成功新增後要導向的路徑
- 新增 `PollUpdate` 類別繼承自 Django 的通用編輯視圖 `UpdateView`,其內容同 `PollCreate`
- `UpdateView` 與 `CreateView` 不同的地方在於,`UpdateView` 會自動透過路徑處理器擷取出來的參數 `pk` 取得指定的記錄並放在變數 `object` 中傳遞給頁面範本,並自動將其內容預先填入頁面的表單中
- `UpdateView` 的衍生類別預設會找 `應用程式/資料模型_form.html` 當作頁面範本,以本例來說就是 `default/poll_form.html`範本
與新增投票主題共用頁面範本 `default/poll_form.html`,不另行設置。
刪除投票主題(Delete)
網址
開啟 `default/urls.py`,新增++第 10 行++路徑規則:
from django.urls import path from . import views urlpatterns = [ path('poll/', views.PollList.as_view()), path('poll/<int:pk>/', views.PollDetail.as_view()), path('option/<int:pk>/', views.PollVote.as_view()), path('poll/create/', views.PollCreate.as_view()), path('poll/<int:pk>/update/', views.PollUpdate.as_view()), path('poll/<int:pk>/delete/', views.PollDelete.as_view()), ]
視圖
開啟 `default/views.py`,增加以下程式碼:
# 刪除投票主題 class PollDelete(DeleteView): model = Poll success_url = '/poll/'
- 新增 `PollDelete` 繼承自通用編輯視圖 `DeleteView` 類別
- `DeleteView` 會透過參數 `pk` 取得欲刪除的指定紀錄,並儲存在變數 `object` 中傳遞給頁面範本
- `DeleteView` 的衍生類別預設會使用 `應用程式/資料模型_confirm_delete.html` 做為頁面範本,以此例即為 `default/poll_confirm_delete.html`。若要變更可透過 `template_name` 屬性指定欲使用的頁面範本範本
新增確認刪除頁面範本檔案 `default/templates/default/poll_confirm_delete.html`
{% extends "base.html" %} {% block content %} <h1>刪除紀錄</h1> <form action="" method="post"> <p>確定要刪除 {{ object }} 這筆紀錄嗎?</p> {% csrf_token %} <input type="submit" value="是的,我要刪除"> </form> {% endblock %}
- ++第 6 行++,透過 `{{ object }}` 標籤印出欲刪除紀錄的代表字串。若要改變紀錄代表字串的話,可定義或修改資料模型的 `__str__()` 方法。
更新投票主題列表範本開啟 `default/templates/default/poll_list.html`,修改為以下程式碼:
{% extends "base.html" %} {% block content %} <h1>投票主題</h1> <a href="/poll/create/">新增投票主題</a> <ul> {% for poll in poll_list %} <li> {{ poll.date_created }} <a href="{{ poll.id }}/">{{ poll.subject }}</a> | <a href="{{ poll.id }}/update/">修改</a> | <a href="{{ poll.id }}/delete/">刪除</a> | </li> {% endfor %} </ul> {% endblock %}
主要修改項目如下:
- 將原本單行的 `<li>`...`</li>` 拆成多行
- 新增++第 11, 12 行++的修改與刪除投票主題的連結新增/修改/刪除投票選項
路徑
urlpatterns = [ path('poll/', views.PollList.as_view(), name='poll_list'), path('poll/<int:pk>/', views.PollDetail.as_view(), name='poll_view'), path('option/<int:pk>/', views.PollVote.as_view(), name='poll_vote'), path('poll/create/', views.PollCreate.as_view()), path('poll/<int:pk>/update/', views.PollUpdate.as_view()), path('poll/<int:pk>/delete/', views.PollDelete.as_view()), path('option/create/<int:pid>/', views.OptionCreate.as_view(), name='option_create'), path('option/<int:pk>/update/', views.OptionUpdate.as_view(), name='option_edit'), path('option/<int:pk>/delete/', views.OptionDelete.as_view(), name='option_delete'), ]
- 修改++第 5, 6, 7 行++,透過 `name` 參數為路徑規則命名為 `poll_list` 、`poll_view` 以及 `poll_vote`,等等我們要示範另一種產生路徑的方式
- 新增++第 11 - 13 行++,分別對應新增投票選項、修改投票選項以及刪除投票選項的功能
- 我們希望在新增投票選項時能自動填入所屬的投票主題編號,因此將投票主題編號藏在路徑裡(`<int:pid>`),如果存取 `/option/create/1/` 表示要新增 `1` 號投票主題的選項視圖
修改 `default/views.py`:
from .models import * from django.views.generic import CreateView, UpdateView, DeleteView from django.urls import reverse
- 新增++第 5 行++,由 `django.urls` 引用 `reverse` 函式
新增投票選項
## 新增投票選項 class OptionCreate(CreateView): model = Option fields = ['title'] template_name = 'default/poll_form.html' # 成功新增選項後要導向其所屬的投票主題檢視頁面 def get_success_url(self): return '/poll/'+str(self.kwargs['pid'])+'/' # 表單驗證,在此填上選項所屬的投票主題 id def form_valid(self, form): form.instance.poll_id = self.kwargs['pid'] return super().form_valid(form)
- 新增 `OptionCreate` 繼承自通用編輯類別 `CreateView`
- ++第 48 行++,指定操作資料模型為 `Option`
- ++第 49 行++,自動產生表單時,僅產生 `title` 欄位就好
- ++第 50 行++,指定使用頁面範本 `default/poll_form.html`。你也可以新增 `default/option_form.html` 檔案,將 `default/poll_form.html` 的內容複製過來,這樣就不需要手動指定頁面範本了。但如果沒有要做特別的處理,其實讓它共用 `default/poll_form.html` 就好了。
- ++第 53-54 行++,定義 `get_success_url()` 方法來回傳成功新增資料後要轉向的路徑
- 與 `PollCreate`, `PollUpdate`, `PollDelete` 不同的地方是,上述這些操作結束後皆是轉向固定的路徑(`/poll/`,投票主題列表),而新增投票選項結束後要返回其所屬的投票主題檢視頁面,不同投票主題的選項要導向的路徑是不同的,所以無法直接指定 `success_url` 屬性就好,必須定義 `get_success_url()` 方法來動態產生欲導向的路徑。
- ++第 54 行++,回傳路徑 `/poll/投票主題id/`。 `pid` 是路徑規則中擷取出來的參數,表示選項所屬的投票主題的 `id`。
- ++第 56-58 行++,定義 `form_valid()` 方法來驗證使用者送出的表單內容。
- ++第 57 行++,在這個範例中,我們利用表單驗證的這個機制,自動填入選項的 `poll_id` 欄位。
- `form.instance` 表示表單所對應到的那筆紀錄,我們現在要指定那筆投票選項所屬的投票主題編號(`poll_id` 欄位)
- `self.kwargs['pid']` 表示由路徑中自動擷取到的 `pid` 參數
- ++第 58 行++,實際驗證表單的動作還是交由親類別(`super()`)的表單驗證方法來進行。若驗證成功,就會將紀錄寫入資料庫中。修改投票選項
## 修改投票選項 class OptionUpdate(UpdateView): model = Option fields = ['title'] template_name = 'default/poll_form.html' # 修改成功後返回其所屬投票主題檢視頁面 def get_success_url(self): return reverse('poll_view', args=[self.object.poll_id])
- 新增 `OptionUpdate` 繼承自通用編輯視圖 `UpdateView`,其餘屬性的意義請參考 `OptionCreate`。`UpdateView` 透過路徑處理器擷取的參數 `pk` 來取得主鍵值與 `pk` 相等的那筆紀錄,並記在 `object` 屬性中。
- ++第 67 行++,回傳所屬的投票主題檢視頁面的路徑。
- 由於其路徑規則中已無包含所屬投票主題的 `id`,而是欲修改的投票選項自身的主鍵(`pk`)值。
- `OptionUpdate` 類別會透過 `pk` 取得符合的投票選項紀錄,並被存在 `object` 屬性,故可透過 `self.object.poll_id` 取得此投票選項所屬的投票主題 `id`。
- 這邊我們改用另一種方式來產生路徑。前面修改路徑規則時,我們有將檢視投票主題的那條規則命名為 `poll_view`,這時候我們可以利用 `reverse()` 這個函式,反向透過指定的命名規則,反推產生它所對應到的路徑。
- `reverse()` 函式的第一個引數是要用哪一條路徑規則來反推路徑,本例我們使用 `poll_view` 這條規則
- 由於 `poll_view` 這條規則對應的路徑(`poll/<int:pk>/`)中包含了會變動的部份 `<int:pk>`,因此在產生相對應的路徑時,我們要指定這個部份應該填入什麼內容,有兩種指定方式:
- 以參數的順序來指定要填入的內容,這邊透過 `reverse()` 函式的 `args` 參數來傳入一個替換的清單,按照順序取代路徑規則裡會變動的部份。以此例來說,路徑只有一個會變動的 `<int:pk>`,因此清單僅需包含一項內容即可。
- 以參數的名稱來指定要填入的內容,可以透過 `reverse()` 函式的 `kwargs` 參數來傳入一個字典,分別指定路徑裡的每個參數要替換成什麼,例如:return reverse('poll_view', kwargs={'pk': self.object.poll_id})
為什麼要用 reverse 來產生路徑?
其實這種反向產生網址路徑的方法才是官方比較建議的做法。
之前所示範的方式是以字串自行組合出目的路徑,這樣比較直覺,但缺點是,日後若需調整路徑規則,就必須將視圖中所有相關的路徑全部修正到才行。
反向產生網址路徑的做法,必須先為路徑規則命名,在視圖中只需指定要透過哪條規則來產生路徑,並提供所需參數即可,剩下的事情 `reverse` 會處理。例如:要將原本的 `poll/<int:pk>/` 改為 `poll/view/<int:pk>`,僅需修改 `urls.py` 即可,`views.py` 就不需要做任何調整。如此一來,跟路徑相關的修改操作就可以全部集中在 `urls.py` 處理。
刪除投票選項
## 刪除投票選項 class OptionDelete(DeleteView): model = Option template_name = 'default/poll_confirm_delete.html' # 刪除成功後返回其所屬投票主題檢視頁面 def get_success_url(self): return reverse('poll_view', kwargs={'pk': self.object.poll_id})
- 新增 `OptionDelete` 繼承自通用編輯圖視 `DeleteView`
- ++第 76 行++,由名為 `poll_view` 的路徑規則,指定參數 `pk` 的值為欲被刪除的這筆紀錄的 `poll_id` 的值,反向產生路徑。
- `poll_view` 這條規則是 `poll/<int:pk>/`。路徑處理器的正常運作方式是由路徑擷取參數值,我們現在要反向藉由提供參數 `pk` 的值要求 Django 幫我們產生符合 `poll_view` 這條規則的網址路徑。範本
這3個功能在視圖中都透過 `template_name` 屬性指定使用投票主題所使用的頁面範本 `default/poll_form.html` 與 `default/poll_confirm_delete.html`,因此不另外建立頁面範本來對應這 3 個功能。
但是我們要修改「投票主題檢視」的頁面範本,將投票主題下的「投票選項」相關操作的連結放在頁面上,以方便使用。
開啟 `default/templates/default/poll_detail.html`,修改為以下程式碼:
{% extends "base.html" %} {% block content %} <h1>{{ poll.subject }}</h1> <a href="{% url 'option_create' poll.id %}">新增選項</a> <div>小提示!直接按選項文字就可以投票囉!</div> <ul> {% for option in options %} <li> <a href="{% url 'poll_vote' option.id %}">{{ option.title }}</a> : {{ option.count }} 票 | <a href="{% url 'option_edit' option.id %}">修改</a> | <a href="{% url 'option_delete' option.id %}">刪除</a> </li> {% endfor %} </ul> {% endblock %}
- 新增++第 5, 11, 12 行++,加入新增選項、修改選項、刪除選項的功能連結,這邊我們改以路徑規則來反推產生相對應的路徑的方式,而非先前自行組合路徑的方式。
- Django 的頁面範本提供了一個 `{% url %}` 標籤來提供與視圖內的 `reverse()` 函式類似功能,該標籤必須要指定欲使用的路徑規則名稱,若該路徑規則含有會變動的參數,必須按照順序一一列在路徑規則名稱之後,以空格隔開,例:
- `option_create` 這條規則所對應的路徑為 `option/create/<int:pid>`,此功能在路徑中的變數代表要將選項建立在哪個投票主題之下,所以可以用 `{% url 'option_create' poll.id %}` 來產生相對應的路徑。
- 修改++第 10 行++,原本是投票的路徑是 `/option/{{ option.id }}/`,這邊改以 `{% url 'poll_vote' option.id %}` 標籤來產生路徑根路徑重新導向
先前在測試網站的時候,是使用 `http://localhost/poll/` 來存取服務。但是若僅以主機名稱 `http://localhost/` 來存取網站,會看到如下圖 Page not found 的錯誤訊息。
錯誤訊息的頁面上列出目前網站已定義的路徑規則,目前已定義的 10 條規則中,並不包含空路徑(也就是網站主機位址後不加其他路徑)的定義,因為找不到相對應的處理函式,所以產生了錯誤訊息。
在這個範例最主要的功能就是投票,因此,我們希望僅以主機名稱來存取網站時,會直接顯示投票主題列表,所以必須將根路徑重新導向到 `poll/` 來顯示投票主題列表。
修改 `default/urls.py`
from django.urls import path from . import views from django.views.generic import RedirectView urlpatterns = [ path('', RedirectView.as_view(url='poll/')), path('poll/', views.PollList.as_view(), name='poll_list'), ]
- 新增++第 3 行++,引用 `RedirectView`
- 新增++第 6 行++,定義路徑規則,將空路徑 `''` 轉址導向到 `'poll/'`