案例:留言板

前言

第 7 課已經大致說明了留言板的程式架構,接著來進行版面美化的工作。在這個案例中,同樣直接透過 CDN 來引用 Bootstrap 框架,同時為了讓頁面看起來更活潑,也會引用 Font Awesome[1] 在頁面加上一些圖示。

修正範本路徑

在原本的實作中,頁面範本被分散在 guestbook/templates/ 以及 guestbook/web/templates/,為了方便管理,請將 guestbook/web/templates/ 裡所有的檔案及資料夾移入 guestbook/templates/,將這兩個資料夾合併在一起,完成後專案目錄的結構大致如下:

guestbook/
├── db.sqlite3
├── guestbook/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py*
├── templates/
│   ├── base.html
│   ├── confirm_delete.html
│   ├── form.html
│   ├── navbar.html
│   ├── registration/
│   │   ├── logged_out.html
│   │   └── login.html
│   └── web/
│       ├── message_detail.html
│       └── message_list.html
└── web/
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    ├── urls.py
    └── views.py

引用 Bootstrap 與 FontAwesome 框架

如同前一個案例,先修改 guestbook/templates/base.html

<!doctype html> <html lang="en"> <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"> <!-- FontAwesome --> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" 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>

原本的「首頁」、「新增留言」、「登入」/「登出」等連結暫且移除,稍候製作導覽列時再將其移入。

加入頁面導覽列

新增 guestbook/templates/navbar.html 檔案,並鍵入以下內容:

<nav class="navbar navbar-expand-sm navbar-dark bg-dark"> <!-- 網站標誌 --> <div class="navbar-brand">留言板</div> <!-- 展開/收合切換鈕(畫面小時才會顯示) --> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <!-- 導覽列選單內容(可收合) --> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ml-auto"> <li class="nav-item"><a href="/" class="nav-link">首頁</a></li> <li class="nav-item"><a href="/message/create" class="nav-link">新增留言</a></li> {% if user.is_authenticated %} <li class="nav-item"><a href="/accounts/logout" class="nav-link btn btn-sm btn-primary">{{ user.username }} 登出</a></li> {% else %} <li class="nav-item"><a href="{% url 'login' %}" class="nav-link btn btn-sm btn-primary">登入</a></li> {% endif %} </ul> </div> </nav>

然後記得把這個範本加入 guestbook/templates/base.html ,這樣才會在每個頁面顯示。增加第 15 行{% include "navbar.html" %}

<div class="container"> {% include "navbar.html" %} {% block content %}{% endblock %} </div>

加入 Font Awesome 圖示

先到 Font Awesome 的圖示展示頁面(https://fontawesome.com/icons)搜尋需要的圖示,可以在頁面上的搜尋框輸入關鍵字來篩選圖示。列表中以深色呈現的圖示是可以免費使用的,淺色的圖示則是需要另外付費購買。

假設現在要在「留言板」前面加上一個代表「訊息」的圖示,可以在搜尋框輸入「message」來篩選圖示。

若想使用 comment-alt 這個圖示,可點選它查看細節。

重點在頁面左下方,有列出使用這個圖示的 HTML 碼 <i class="far fa-comment-alt"></i> ,把這段 HTML 碼放在導覽列的「留言板」之前,修改 guestbook/templates/navbar.html 的第 3 行:

<div class="navbar-brand"><i class="far fa-comment-alt"></i>留言板</div>

修改後的導覽列會變成這樣:

持續修改 guestbook/templates/navbar.html ,為「首頁」、「新增留言」、「登入」、「登出」等加上合適的圖示:

<nav class="navbar navbar-expand-sm navbar-dark bg-dark"> <!-- 網站標誌 --> <div class="navbar-brand"><i class="far fa-comment-alt"></i> 留言板</div> <!-- 在小螢幕的設備上顯示可展開/收合導覽選單的按鈕 --> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <!-- 導覽列選單內容(可收合) --> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ml-auto"> <li class="nav-item"><a href="/" class="nav-link"><i class="fas fa-home"></i> 首頁</a></li> <li class="nav-item"><a href="/message/create" class="nav-link"><i class="fas fa-edit"></i> 新增留言</a></li> {% if user.is_authenticated %} <li class="nav-item"><a href="/accounts/logout" class="nav-link btn btn-sm btn-primary">{{ user.username }} <i class="fas fa-sign-out-alt"></i> 登出</a></li> {% else %} <li class="nav-item"><a href="{% url 'login' %}" class="nav-link btn btn-sm btn-primary"><i class="fas fa-sign-in-alt"></i> 登入</a></li> {% endif %} </ul> </div> </nav>

整個修改完的導覽列外觀如下,與原本純文字的導覽列相比,看起來稍微活潑了些:

修改頁面路徑規則

修改 guestbook/web/urls.py

from django.urls import path from . import views urlpatterns = [ path('', views.MessageListView.as_view()), path('create/', views.MessageCreate.as_view()), path('<int:pk>', views.MessageDetailView.as_view()), path('<int:pk>/delete/', views.MessageDelete.as_view()), ]

另外也需要修改 guestbook/guestbook/urls.py

from django.contrib import admin from django.urls import path from django.conf.urls import include from django.views.generic import RedirectView urlpatterns = [ path('admin/', admin.site.urls), path('', RedirectView.as_view(url='/message/')), path('message/', include('web.urls')), path('accounts/', include('django.contrib.auth.urls')), ]

主要修改第 8 行將首頁導向至留言列表頁面以及與第 9 行修正 web 模組的路徑。

美化其他頁面範本

留言列表

修改 guestbook/templates/web/message_list.html ,使用列表、清單(List Group)來顯示留言列表:

{% extends "base.html" %} {% block content %} <ul class="list-group mt-2"> {% for message in message_list %} <li class="list-group-item list-group-item-action"> <small class="text-muted"><i class="fas fa-clock"></i> {{message.publication_date|date:"Y/m/d H:i"}}</small> {% if user.is_authenticated %} <a href="{{message.id}}/delete" class="text-danger" title="刪除"><i class="fas fa-minus-circle"></i></a> {% endif %} <a href="{{message.id}}">{{ message.subject }}</a> </li> {% endfor %} </ul> {% endblock %}

留言檢視

修改 guestbook/templates/web/message_detail.html ,同樣以資訊卡片(Card)來顯示留言的詳細內容:

{% extends "base.html" %} {% block content %} <div class="card mt-2"> <div class="card-header"> <div class="font-weight-bold"><i class="fas fa-file-alt"></i> {{ message.subject }}</div> </div> <div class="card-body"> <div class="card-text">{{ message.content|linebreaks }}</div> </div> <div class="card-footer d-flex justify-content-between"> <small class="text-muted"><i class="fas fa-user"></i> {{ message.user }}</small> <small class="text-muted"><i class="fas fa-clock"></i> {{ message.publication_date|date:"Y/m/d H:i" }}</small> </div> </div> {% endblock %}

新增留言

修改 guestbook/templates/form.html ,讓頁面上的表格也套用 Bootstrap 框架的表格外觀:

{% extends "base.html" %} {% block content %} <form action="" method="post"> {% csrf_token %} <table class="table table-sm"> {{ form.as_table }} </table> <input type="submit" value="送出" class="btn btn-sm btn-primary"/> </form> {% endblock %}

修改的地方只有第 6 行,為 <table> 標籤加上 class="table table-sm" 屬性,額外套用 table-sm 的原因是想讓表格的列可以排列得稍微緊緻一些。

接下來再來修正一下頁面上輸入元件的外觀。Bootstrap 框架也有輸入元件的相關定義,最基本的方式就是為這些輸入元件套用 form-control 類別,例:

<form>
  <!-- ...略... -->
  <input type="text" class="form-control">
  <!-- ...略... -->
  <textarea class="form-control"></textarea>
  <!-- ...略... -->
</form>

新增留言這個頁面範本產生輸入元件的方式是透過第 8 行的 {{ form.as_table }} 將接收到的 form 變數的內容轉成相對應的 HTML 碼,沒有辦法直接以新增屬性的方式來套用 CSS 類別。

在不安裝額外的 python 套件的前提下,有 2 種方式來解決這個問題:

  1. 在視圖(View)產生 form 的時候,就指定每個輸入元件要套用的 CSS 類別
  2. 不使用 {{ form.as_table }} 來自動產生 HTML 碼,而是自行撰寫這些輸入元件的 HTML 碼

方案1:預先指定要套用的 CSS 類別

看一下 guestbook/web/views.py ,新增留言是由 MessageCreate 這個 class 負責的,它繼承了 django 內建的 CreateView 類別,由我們指定的 Message 這個 model 自動生成 form 的定義。

class MessageCreate(CreateView): model = Message fields = ['user', 'subject', 'content'] success_url = "/" template_name = 'form.html'

在這邊無法調整 form 的內容,所幸 CreateView 除了指定 modelfields 的方式自動生成 form 之外,也接受使用者以指定 form_class 的方式,轉而參考該 class 來產生 form。

新增 guestbook/web/forms.py 自行定義 form class:

from django import forms from .models import Message class MessageForm(forms.ModelForm): class Meta: model = Message fields = ['user', 'subject', 'content'] widgets = { 'user': forms.TextInput(attrs={'class': 'form-control'}), 'subject': forms.TextInput(attrs={'class': 'form-control'}), 'content': forms.Textarea(attrs={'class': 'form-control'}), }

在這邊透過繼承 forms.ModelForm 的方式來定義 MessageForm。定義好表單之後,再修正 guestbook/web/views.py,修改 MessageCreate 的內容:

# ... 略,新增底下這行 from .forms import MessageForm # ... 略 class MessageCreate(CreateView): form_class = MessageForm success_url = "/" template_name = 'form.html'

方案2:自行撰寫 HTML 碼

採用這個方案的話,表示要完全忽略傳遞給頁面範本的 form 變數的內容,在範本中就不以 {{ form.as_table }} 來產生相關輸入元件的 HTML 碼。不需要新增 forms.py,不需要修改 views.py ,僅修改頁面範本即可。

修改 guestbook/templates/form.html

{% extends "base.html" %} {% block content %} <form action="" method="post"> {% csrf_token %} <table class="table"> <tr> <th><label for="id_user">姓名:</label></th> <td><input id="id_user" name="user" class="form-control"></td> </tr> <tr> <th><label for="id_subject">主旨:</label></th> <td><input id="id_subject" name="subject" class="form-control"></td> </tr> <tr> <th><label for="id_content">內容:</label></th> <td><textarea name="content" id="id_content" cols="30" rows="10" class="form-control"></textarea></td> </tr> </table> <input type="submit" value="送出" class="btn btn-sm btn-primary"/> </form> {% endblock %}

將原本的 {{ form.as_table }} 刪除,改以第 7-18 行的 HTML 碼取代。

採用這個方案要特別注意自行撰寫的 HTML 碼,每個輸入欄位的 name 屬性與視圖裡所參照的 model 是否有對應到,如果無法完全對應的話,可能有些欄位的值就無法被寫人資料庫。

建議盡量採用方案1,比較不會發生不慎打錯欄位名稱的狀況。

刪除留言

修改 guestbook/templates/confirm_delete.html

{% extends "base.html" %} {% block content %} <h1>刪除記錄</h1> <p>您確定要刪除這筆記錄嗎?</p> <form action="" method="POST"> {% csrf_token %} <input type="submit" value="是的,我要刪除" class="btn btn-sm btn-danger" /> </form> {% endblock %}

主要是第 8 行,為按鈕套用 btn btn-sm btn-danger 等類別。

使用者登入

修改 guestbook/templates/registration/login.html

{% extends "base.html" %} {% block content %} {% if form.errors %} <p class="alert alert-danger">帳號或密碼不符合,請再試一次。</p> {% endif %} <form method="post" action="{% url 'login' %}"> {% csrf_token %} <div class="h3 mb5">請輸入您的帳號密碼</div> <div class="form-inline"> <input name="username" autofocus="" required="" id="id_username" maxlength="254" type="text" class="form-control mr-2" placeholder="帳號"> <input name="password" required="" id="id_password" type="password" class="form-control mr-2" placeholder="密碼"> <input type="submit" value="login" class="form-control btn-primary"/> </div> </form> {% endblock %}

使用者登出

修改 guestbook/templates/registration/logged_out.html

{% extends "base.html" %} {% block content %} <p class="alert alert-info">您已登出!!</p> <a href="{% url 'login'%}" class="btn btn-primary">請按此處重新<i class="fas fa-sign-in-alt"></i> 登入</a> {% endblock %}


  1. Font Awesome 也是一個 CSS 框架,比較特別的是它提供了一個風格統一的圖示庫,讓網頁設計師可以方便地在頁面上擺放圖示。
    官網:https://fontawesome.com/ ↩︎