案例:線上投票

第 4 至 6 課已經建立了一個簡單的線上投票網站,看起來有點陽春,接下來試著將輸出的頁面稍加美化一下。

如果對於 CSS 已經很熟悉了,可以直接自己撰寫樣式表。在這個案例中,我們直接引入市面上很多人使用的 CSS 框架 – Bootstrap 來美化網站的外觀。

Bootstrap 官網:https://getbootstrap.com/

引用 Bootstrap 框架

在專案中加入 Bootstrap 框架有好幾種方式,可以將至官網下載後,將其當成專案靜態檔案的一部份。最簡便的方式則是透過網路上的 CDN[1] 服務,直接引用所需要的檔案。以下兩種方式請選擇一種套用即可。

1. 透過 CDN 服務引用

參考 Bootstrap 官網上的說明,將 poll/default/templates/base.html 修改如下:

<!doctype html> <html lang="zh-hant"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- 引用 Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous"> <title>線上投票</title> </head> <body> <div class="container"> {% block content %}{% endblock %} </div> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script> </body> </html>

2. 下載為專案的靜態檔案

指定靜態檔案存放路徑

先修改專案設定檔 poll/poll/settings.py,指定靜態檔案存放的資料夾,新增第 123 - 125 行

# Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ]

接著在專案資料夾下建立 static 資料夾,建立完後,整個專案的目錄結構如下:

poll/
├── default/
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── templates/
│   │   ├── default/
│   │   │   ├── poll_detail.html
│   │   │   └── poll_list.html
│   │   ├── base.html
│   │   ├── confirm_delete.html
│   │   └── general_form.html
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── poll/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── static/
├── db.sqlite3
├── manage.py*
└── README.md

將 Bootstrap 相關檔案放到靜態檔案存放路徑之下

  1. 到 Bootstrap 官網:https://getbootstrap.com/

    點選「Download」按鈕進入下載頁面。

  2. 點按下載頁面的 Compiled CSS and JS 下方的「Download」按鈕

  3. 將下載的壓縮檔解開後,把得到的 cssjs 兩個資料夾複製到專案的靜態檔案存放路徑(static)下,完成後整個專案的目錄結構如下:

    poll/
    ├── default/
    │   ├── migrations/
    │   │   ├── 0001_initial.py
    │   │   └── __init__.py
    │   ├── templates/
    │   │   ├── default/
    │   │   │   ├── poll_detail.html
    │   │   │   └── poll_list.html
    │   │   ├── base.html
    │   │   ├── confirm_delete.html
    │   │   └── general_form.html
    │   ├── admin.py
    │   ├── apps.py
    │   ├── __init__.py
    │   ├── models.py
    │   ├── tests.py
    │   ├── urls.py
    │   └── views.py
    ├── poll/
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── static/
    │   ├── css/
    │   │   ├── bootstrap.css
    │   │   ├── bootstrap.css.map
    │   │   ├── bootstrap-grid.css
    │   │   ├── bootstrap-grid.css.map
    │   │   ├── bootstrap-grid.min.css
    │   │   ├── bootstrap-grid.min.css.map
    │   │   ├── bootstrap.min.css
    │   │   ├── bootstrap.min.css.map
    │   │   ├── bootstrap-reboot.css
    │   │   ├── bootstrap-reboot.css.map
    │   │   ├── bootstrap-reboot.min.css
    │   │   └── bootstrap-reboot.min.css.map
    │   └── js/
    │       ├── bootstrap.bundle.js
    │       ├── bootstrap.bundle.js.map
    │       ├── bootstrap.bundle.min.js
    │       ├── bootstrap.bundle.min.js.map
    │       ├── bootstrap.js
    │       ├── bootstrap.js.map
    │       ├── bootstrap.min.js
    │       └── bootstrap.min.js.map
    ├── db.sqlite3
    ├── manage.py*
    └── README.md
    

修改網站基底頁面範本

將網站基底頁面範本 poll/default/templates/base.html 修改如下:

<!doctype html> <html lang="zh-hant"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- 引用 Bootstrap CSS --> <link rel="stylesheet" href="/static/css/bootstrap.min.css"> <title>線上投票</title> </head> <body> <div class="container"> {% block content %}{% endblock %} </div> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="/static/js/bootstrap.bundle.min.js"></script> </body> </html>
引用 Bootstrap 框架前
引用 Bootstrap 框架後

上面兩張擷圖顯示了引用 Bootstrap 框架前後的差異:

美化頁面上的元件

使用 CSS 框架的好處是,這些框架已將事先定義好一整套的樣式規則,我們僅需要將網頁上的元件套用欲使用的 CSS 類別,就能輕鬆地增進網頁的美觀,並維持網站頁面外觀的一致性。

按鈕(Button)

若想把問題列表頁面上的「新增投票主題」、「修改」、「刪除」等連結改得像是按鈕的外觀的話,可以將這些連結套用 btn 以及與其語意相符的按鈕樣式類別即可。預先定義的按鈕樣式的類別如下表:

按鈕樣式 外觀範例 按鈕樣式 外觀範例 按鈕樣式 外觀範例
btn-primary btn-secondary btn-success
btn-danger btn-warning btn-info
btn-light btn-dark btn-link

另外,除了預設尺寸外,也可以額外套用 btn-lgbtn-sm 來指定按鈕的尺寸為「大」或「小」。

依上述說明修改 poll/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 %}

修改後頁面外觀如下圖:

列表、清單(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 %}

資料卡片(Card)

接下來試著美化檢視問題的頁面,在此我們選用另一種方式-資料卡片(Card)-來呈現問題的詳細內容。

資料卡片的結構大致如下:

<div class="card"> <div class="card-header">卡片標題</div> <div class="card-body"> 卡片內容 </div> <div class="card-footer">卡片頁腳</div> </div>

每張卡片可包含 card-headercard-body 以及 card-footer 3 個部份,前面兩個比較容易理解, card-footer 通常是用來放說明或註解用的。

修改 poll/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"> <p>小提示!直接按選項文字就可以投票囉!</p> <ul class="list-group"> {% for option in option_list %} <li class="list-group-item"> <a href="/option/{{ option.id }}/update/" class="btn btn-sm btn-secondary">修改</a> <a href="/option/{{ option.id }}/delete/" class="btn btn-sm btn-danger">刪除</a> <a href="/option/{{ option.id }}/">{{ option.title }}</a> : {{ option.count }}</li> {% endfor %} </table> </div> <div class="card-footer"> <a href="/option/create/{{ poll.id }}" class="btn btn-sm btn-primary">新增選項</a> </div> </div> {% endblock %}

修改其它頁面範本

接著美化 poll/default/templates/general_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 %}

由於上述的需求,需要修改 poll/default/views.py 中用來處理「新增投票主題」、「修改投票主題」、「新增投票選項」以及「修改投票選項」的頁面視圖,以下僅顯示相對應的 QuestionCreateQuestionUpdateAnswerCreateAnswerUpdate 等 4 個 class,主要是為這 4 個類別分別加上 get_context_data 成員函式,將 titlebackpath 加入要傳給頁面範本的資料清單內。

# 新增投票主題 class PollCreate(CreateView): model = Poll fields = ['subject'] # 指定要顯示的欄位 success_url = '/poll/' # 成功新增後要導向的路徑 template_name = 'general_form.html' # 要使用的頁面範本 # 新增 title 與 backpath,以便在 general_form.html 中使用 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['title'] = '新增投票主題' context['backpath'] = '/' return context # 修改投票主題 class PollUpdate(UpdateView): model = Poll fields = ['subject'] # 指定要顯示的欄位 success_url = '/poll/' # 成功新增後要導向的路徑 template_name = 'general_form.html' # 要使用的頁面範本 # 新增 title 與 backpath,以便在 general_form.html 中使用 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['title'] = '修改投票主題' context['backpath'] = '/' return context
# 新增投票選項 class OptionCreate(CreateView): model = Option fields = ['title'] template_name = 'general_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,以便在 general_form.html 中使用 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['title'] = '新增投票選項' context['backpath'] = reverse('poll_view', kwargs={'pk': self.kwargs['pid']}) return context # 修改投票選項 class OptionUpdate(UpdateView): model = Option fields = ['title'] template_name = 'general_form.html' # 修改成功後返回其所屬投票主題檢視頁面 def get_success_url(self): return '/poll/'+str(self.object.poll_id)+'/' # 新增 title 與 backpath,以便在 general_form.html 中使用 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['title'] = '修改投票選項' context['backpath'] = reverse('poll_view', kwargs={'pk': self.object.poll_id}) return context

要特別注意的是,「新增答案」或「修改答案」後,應該要導向到答案所屬的問題檢視頁面,所以 OptionCreateOptionUpdate 這兩個 view 還需要再分別新增 get_success_url 成員函式來回傳操作完成後的導向頁面路徑。

頁面範本剩下 poll/default/templates/confirm_delete.html 還沒處理,修改如下:

{% extends "base.html" %} {% block content %} {% if backpath %} <a href="{{ backpath }}" class="btn btn-primary">返回前一頁</a><BR> {% endif %} <h1>{{ title }}</h1> <div class="list-group-item">{{object}}</div> <p>您確定要刪除這筆記錄嗎?</p> <form action="" method="POST"> {% csrf_token %} <input type="submit" action="" value="是的,我要刪除" class="btn btn-sm btn-danger"> </form> {% endblock %}

因為上述的修改,需要 views 額外傳遞 backpath 以及 title 這 2 個參數,所以需要修改 poll/default/views.py 裡的 PollDelete 以及 OptionDelete 這 2 個 View,分別為其加上 get_context_data 成員函式,以傳遞 titlebackpath 給頁面範本,以下僅顯示 PollDeleteOptionDelete

# 刪除投票主題 class PollDelete(DeleteView): model = Poll success_url = '/poll/' template_name = "confirm_delete.html" # 新增 title 與 backpath,以便在 confirm_delete.html 中使用 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['title'] = '刪除投票主題' context['backpath'] = '/poll/' return context
# 刪除投票選項 class OptionDelete(DeleteView): model = Option template_name = 'confirm_delete.html' # 刪除成功後返回其所屬投票主題檢視頁面 def get_success_url(self): return reverse('poll_view', kwargs={'pk': self.object.poll_id}) # 新增 title 與 backpath,以便在 confirm_delete.html 中使用 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['title'] = '刪除投票選項' context['backpath'] = reverse('poll_view', kwargs={'pk': self.object.poll_id}) return context

  1. 內容傳遞網路(英語:Content delivery network或Content distribution network,縮寫:CDN)是指一種透過網際網路互相連接的電腦網路系統,利用最靠近每位使用者的伺服器,更快、更可靠地將音樂、圖片、影片、應用程式及其他檔案傳送給使用者,來提供高效能、可擴展性及低成本的網路內容傳遞給使用者。
    來源:https://zh.wikipedia.org/wiki/內容傳遞網路 ↩︎