案例:線上投票
-
先前我們已經建立了一個簡單的線上投票網站,看起來有點陽春,接下來試著將輸出的頁面稍加美化一下。
如果對於 CSS 已經很熟悉了,可以直接自己撰寫樣式表。在這個案例中,我們直接引入市面上很多人使用的 CSS 框架 -- Bootstrap 來美化網站的外觀。
Bootstrap 官網:[https://getbootstrap.com/](https://getbootstrap.com/)
引用 Bootstrap 框架
在專案中加入 Bootstrap 框架有好幾種方式,可以將至官網下載後,將其當成專案靜態檔案的一部份。最簡便的方式則是透過網路上的 CDN 服務,直接引用所需要的檔案。以下兩種方式請選擇一種套用即可。
透過 CDN 服務引用
參考 Bootstrap 官網上的說明,將 `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"> <!-- 引用 Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <title>線上投票</title> </head> <body> <div id="main-content" class="container"> {% block content %}{% endblock %} </div> <!-- Bootstrap library --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> </body> </html>
- 插入++第 6 - 7 行
- ++第 7 行++的作用就是直接從 Bootstrap 的 CDN 引用 Bootstrap CSS 框架
- 修改++第 11, 13 行,為 `div` 元素套用 `container` 類別,讓內容在顯示的時候在周圍留些間隙,視覺上會比較舒適一些,不會太過擁擠
- 插入++第 14 - 15 行++,由 CDN 載入 Bootstrap 框架的 Javascript 函式庫,在本例中沒使用特殊的互動效果,這 2 行亦可省略
【CDN 是什麼】
內容傳遞網路(英語:Content delivery network或Content distribution network,縮寫:CDN)是指一種透過網際網路互相連接的電腦網路系統,利用最靠近每位使用者的伺服器,更快、更可靠地將音樂、圖片、影片、應用程式及其他檔案傳送給使用者,來提供高效能、可擴展性及低成本的網路內容傳遞給使用者。
來源:https://zh.wikipedia.org/wiki/內容傳遞網路
下載為專案的靜態檔案
指定靜態檔案存放路徑
先修改專案設定檔 `poll/settings.py`,指定靜態檔案存放的資料夾:
# Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ STATIC_URL = 'static/' STATICFILES_DIRS = [ BASE_DIR / "static", ]
- 新增++第 121 - 123 行++,透過 `STATICFILES_DIRS` 來指定靜態檔案的存放路徑
- ++第 124 行++的意思是,專案所在的資料夾下的 `static` 目錄
- `BASE_DIR` 在 `settings.py` 前面有定義過,它的內容就是這個專案所在的資料夾的路徑接著在專案資料夾下建立 `static` 資料夾,建立完後,整個專案的目錄結構如下:
poll/ ├── default/ │ ├── migrations/ │ └── templates/ │ └── default/ ├── poll/ └── static/
將 Bootstrap 相關檔案放到靜態檔案存放路徑之下
1. 到 Bootstrap 官網:https://getbootstrap.com/
點選「`Download`」連結進入下載頁面。
2. 點按下載頁面的 __Compiled CSS and JS__ 下方的「`Download`」按鈕
3. 將下載的壓縮檔解開後,把得到的 `css` 與 `js` 兩個資料夾複製到專案的靜態檔案存放路徑(`static`)下,完成後整個專案的目錄結構如下:poll/ ├── default/ │ ├── migrations/ │ └── templates/ │ └── default/ ├── poll/ └── static/ ├── css/ └── js/
修改網站基底頁面範本
將網站基底頁面範本 `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"> <!-- 引用 Bootstrap CSS --> <link rel="stylesheet" href="/static/css/bootstrap.min.css"> <title>線上投票</title> </head> <body> <div id="main-content" class="container"> {% block content %}{% endblock %} </div> <!-- Bootstrap library --> <script src="/static/js/bootstrap.min.js"></script> </body> </html>
- ++第 7 行++,引用專案靜態檔案資料夾內的 Bootstrap CSS 框架
- ++第 11 行++,將 div 套用 `container` 類別,讓其內容在顯示的時候在周圍留些間隙,視覺上會比較舒適一些,不會太過擁擠
- ++第 17 - 18 行++,載入專案內 Bootstrap 框架的 Javascript 函式庫,在本例中沒使用特殊的互動效果,這 2 行亦可省略
上面兩張擷圖顯示了引用 Bootstrap 框架前後的差異:- `<H1></H1>` 的字體大小
- 連結文字的顏色不一樣了,另外,連結的底線也消失了
- 顯示的內容左邊多了留白的空間,讓版面看起來比較不那麼擁擠美化頁面上的元件
使用 CSS 框架的好處是,這些框架已將事先定義好一整套的樣式規則,我們僅需要將網頁上的元件套用欲使用的 CSS 類別,就能輕鬆地增進網頁的美觀,並維持網站頁面外觀的一致性。
- 按鈕(Button)
若想把問題列表頁面上的「新增投票主題」、「修改」、「刪除」等連結改得像是按鈕的外觀的話,可以將這些連結套用 `btn` 以及與其語意相符的按鈕樣式類別即可。預先定義的按鈕樣式的類別如下表:
|按鈕樣式|外觀範例|按鈕樣式|外觀範例|
|-|-|-|-|
|btn-primary|![](https://i.imgur.com/w1CVKys.png)|btn-secondary|![](https://i.imgur.com/KScbWj8.png)|
|btn-success|![](https://i.imgur.com/50z2ly6.png)|btn-danger|![](https://i.imgur.com/weMNnfN.png)|
|btn-warning|![](https://i.imgur.com/pZqC9Ji.png)|btn-info|![](https://i.imgur.com/CRMt119.png)|
|btn-light|![](https://i.imgur.com/hAEwfUh.png)|btn-dark|![](https://i.imgur.com/fB8ZXuk.png)|
|btn-link|![](https://i.imgur.com/idfnCFy.png)|另外,除了預設尺寸外,也可以額外套用 `btn-lg` 或 `btn-sm` 來指定按鈕的尺寸為「大」或「小」。
依上述說明修改 `default/templates/default/poll_list.html`,將「新增投票主題」、「修改」、「刪除」等連結套用 CSS 類別:
{% extends "base.html" %} {% block content %} <h1>投票主題</h1> <p><a href="create/" class="btn btn-primary">新增投票主題</a></p> <ul> {% for poll in poll_list %} <li> {{ poll.date_created }} <a href="{{ poll.id }}/">{{ poll.subject }}</a> | <a href="{{ poll.id }}/update/" class="btn btn-sm btn-secondary">修改</a> | <a href="{{ poll.id }}/delete/" class="btn btn-sm btn-danger">刪除</a> | </li> {% endfor %} </ul> {% endblock %}
- 修改++第 5, 11, 12 行++,為 `<a>` 標籤新增 `class` 屬性
- ++第 5 行++,為「新增投票主題」這個連結同時套用 `btn` 以及 `btn-primary` 這兩種 CSS 類別。
- ++第 11, 12 行++額外指定要套用 `btn-sm` 類別,因此每個問題列表項目的「修改」與「刪除」連結的按鈕尺寸會比上方的「新增投票主題」來得小。修改後頁面外觀如下圖:列表、清單(List group)
接下來美化一下問題列表的外觀。將 `<ul>` 套用 `list-group` 類別,並將其內的 `<li>` 元素皆套用 `list-group-item` 類別:
{% extends "base.html" %} {% block content %} <h1>投票主題</h1> <p><a href="create/" class="btn btn-primary">新增投票主題</a></p> <ul class="list-group"> {% for poll in poll_list %} <li class="list-group-item"> {{ poll.date_created }} <a href="{{ poll.id }}/">{{ poll.subject }}</a> <a href="{{ poll.id }}/update/" class="btn btn-sm btn-secondary">修改</a> <a href="{{ poll.id }}/delete/" class="btn btn-sm btn-danger">刪除</a> </li> {% endfor %} </ul> {% endblock %}
- 在++第 6, 8 行++分別套用 `list-group` 與 `list-group-item` 類別後,每個 `<li>` 前方的黑點消失了,每個投票主題項目都加上了外框,並增加了內邊界(padding)。
資料卡片(Card)
接下來試著美化檢視問題的頁面,在此我們選用另一種方式-資料卡片(Card)-來呈現問題的詳細內容。
資料卡片的結構大致如下:
<div class="card"> <div class="card-header">卡片標題</div> <div class="card-body"> 卡片內容 </div> <div class="card-footer">卡片頁腳</div> </div>
每張卡片可包含 `card-header` 、 `card-body` 以及 `card-footer` 3 個部份,前面兩個比較容易理解, `card-footer` 通常是用來放說明或註解用的。
修改 `default/templates/default/poll_detail.html`,將投票主題當成資料卡片的標題,而附屬於這個主題的投票選項就當成資料卡片的內容,而新增選項的連結則當成頁腳。
{% extends "base.html" %} {% block content %} <p><a href="/" class="btn btn-primary">回首頁</a></p> <div class="card"> <div class="card-header"> <h1>{{ poll.subject }}</h1> </div> <div class="card-body"> <div>小提示!直接按選項文字就可以投票囉!</div> <ul class="list-group"> {% for option in options %} <li class="list-group-item"> <a href="{% url 'option_edit' option.id %}" class="btn btn-sm btn-secondary">修改</a> <a href="{% url 'option_delete' option.id %}" class="btn btn-sm btn-danger">刪除</a> <a href="{% url 'poll_vote' option.id %}">{{ option.title }}</a> : {{ option.count }} 票 </li> {% endfor %} </ul> </div> <div class="card-footer"> <a href="{% url 'option_create' poll.id %}" class="btn btn-sm btn-primary">新增選項</a> </div> </div> {% endblock %}
修改其它頁面範本
接著美化 `default/templates/default/poll_form.html`:
{% extends "base.html" %} {% block content %} {% if backpath %} <a href="{{ backpath }}" class="btn btn-primary">返回前一頁</a><BR> {% endif %} <h1>{{ title }}</h1> <form action="" method="post"> {% csrf_token %} <table> {{ form.as_table }} </table> <input type="submit" value="送出" class="btn btn-sm btn-primary"> </form> {% endblock %}
- 因為在「新增投票主題」、「修改投票主題」、「新增投票選項」以及「修改投票選項」等 4 個頁面都會共用這個範本,為了讓使用者更容易識別目前正在進行哪一種操作,在這裡增加了第 7 行的程式碼:
<h1>{{ title }}</h1>
- 另外,為了方便使用者返回前一個頁面,範本中也增加了第 4 - 6 行的內容:
{% if backpath %} <a href="{{ backpath }}" class="btn btn-primary">返回前一頁</a><BR> {% endif %}
由於上述的需求,需要修改 `default/views.py` 中用來處理「新增投票主題」、「修改投票主題」、「新增投票選項」以及「修改投票選項」的頁面視圖,以下僅顯示相對應的 `PollCreate` 、 `PollUpdate` 、 `OptionCreate` 與 `OptionUpdate` 等 4 個處理類別,主要是為這 4 個類別將 `title` 與 `backpath` 加入要傳給頁面範本的資料清單內。
# 新增投票主題 class PollCreate(CreateView): model = Poll fields = ['subject'] # 指定要顯示的欄位 success_url = '/poll/' # 成功新增後要導向的路徑 extra_context = {'title': '新增投票主題', 'backpath': '/'} # 修改投票主題 class PollUpdate(UpdateView): model = Poll fields = ['subject'] # 指定要顯示的欄位 success_url = '/poll/' # 成功新增後要導向的路徑 extra_context = {'title': '修改投票主題', 'backpath': '/'}
- 新增++第 34, 41 行++,如果要額外傳遞給頁面範本的資料都是固定的內容,可以透過 `extra_context` 這個屬性,以字典的方式指派要傳遞給頁面範本的額外資料,以此例來說,我們要額外傳遞 `title` 跟 `backpath` 這 2 項資料給頁面範本
## 新增投票選項 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) # 新增 title 與 backpath,以便在 default/poll_form.html 中使用 def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['title'] = '新增投票選項' ctx['backpath'] = reverse('poll_view', kwargs={'pk': self.kwargs['pid']}) return ctx ## 修改投票選項 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]) # 新增 title 與 backpath,以便在 default/poll_form.html 中使用 def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['title'] = '修改投票選項' ctx['backpath'] = reverse('poll_view', kwargs={'pk': self.object.poll_id}) return ctx
- 新增++第 63 - 68, 80 - 85行++,為 `OptionCreate` 以及 `OptionUpdate` 類別新增 `get_context_data()` 成員函式,將 `title` 與 `backpath` 傳遞給頁面範本。因為這兩個功能返回前一頁的路徑是會變動的,因此無法單純透過指定 `extra_context` 屬性來達到目的,必須以定義 `get_context_data()` 成員函式的方式,在函式內取得 `backpath` 的內容,再傳遞給頁面範本。
- ++第 65, 82 行++,先取得原本要傳給頁面範本的內容,將其暫存在 `ctx` 變數中,其型態為「字典」
- ++第 66, 67, 83, 84 行++,在 `ctx` 字典中,再加入 `title` 與 `backpath` 這兩組資訊
- ++第 68, 85 行++,回傳加工後的字典 `ctx` 當做要傳給頁面範本的資料頁面範本剩下 `default/templates/default/poll_confirm_delete.html` 還沒處理,修改如下:
{% extends "base.html" %} {% block content %} {% if backpath %} <a href="{{ backpath }}" class="btn btn-primary">返回前一頁</a> {% endif %} <h1>{{ title }}</h1> <form action="" method="post"> <p class="list-group-item">確定要刪除 {{ object }} 這筆紀錄嗎?</p> {% csrf_token %} <input type="submit" value="是的,我要刪除" class="btn btn-sm btn-danger"> </form> {% endblock %}
- 新增++第 4-6 行++,若 views 有傳遞 `backpath` 變數的話,顯示「返回前一頁」按鈕,以方便使用者返回前一頁。
- 修改++第 7 行++,將「刪除紀錄」改為顯示由 views 傳來的 `title` 變數的內容。因為上述的修改,需要 views 額外傳遞 `backpath` 以及 `title` 這 2 個參數,所以需要修改 `default/views.py` 裡的 `PollDelete` 以及 `OptionDelete` 這 2 個處理類別以傳遞 `title` 與 `backpath` 給頁面範本,以下僅顯示 `PollDelete` 及 `OptionDelete`:
# 刪除投票主題 class PollDelete(DeleteView): model = Poll success_url = '/poll/' extra_context = {'title': '刪除投票主題', 'backpath': '/'}
- 新增++第 47 行++,加入 `extra_context` 屬性來指定要傳給頁面範本的額外資訊
## 刪除投票選項 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}) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['title'] = '刪除投票選項' ctx['backpath'] = reverse('poll_view', kwargs={'pk': self.object.poll_id}) return ctx
- 新增++第 97 - 101 行++,新增 `get_context_data()` 成員函式來加入額外傳遞的內容。