案例:數位助理
-
前言
前面已經分別實作了數位助理專案的日誌以及記帳的功能,接著來進行版面美化的工作。在這個案例中,同樣直接透過 CDN 來引用 Bootstrap 框架以及 Font Awesome 來美化網站頁面。
修改網站基底範本
建立導覽列
新增 `templates/navbar.html`
<!-- Navbar begin //--> <nav class="navbar navbar-expand-sm bg-dark mb-3" data-bs-theme="dark"> <div class="container-fluid"> <!-- 網站標誌 --> <div class="navbar-brand"><i class="far fa-address-card"></i> 數位助理</div> <!-- 在小螢幕的設備上顯示可展開/收合導覽選單的按鈕 --> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-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 ms-auto"> {% if user.is_authenticated %} <li class="nav-item"><a href="/" class="nav-link"><i class="fas fa-book"></i> 日記</a></li> <li class="nav-item"><a href="/journal/create" class="nav-link"><i class="fas fa-edit"></i> 寫日誌</a></li> <li class="nav-item"><a href="/expenses/" class="nav-link"><i class="fas fa-money-check"></i> 帳本</a></li> <li class="nav-item"><a href="/expenses/create" class="nav-link"><i class="fas fa-money-check-alt"></i> 記帳</a></li> <li class="nav-item"> <form action="{% url 'logout' %}" method="post"> {% csrf_token %} <input type="submit" value="{{ user.username }} 登出" class="btn btn-sm btn-primary" /> </form> </li> {% else %} <li class="nav-item"><a href="{% url 'login' %}" class="btn btn-sm btn-primary"><i class="fas fa-sign-in-alt"></i> 登入</a></li> {% endif %} </ul> </div> </div> </nav> <!-- Navbar end //-->
引用 Bootstrap、FontAwesome 以及導覽列
將網站基底頁面範本 `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"> <!-- FontAwesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <title>數位助理</title> </head> <body> <div class="container"> {% include 'navbar.html' %} <div>{% block content %}{% endblock %}</div> </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>
- ++第 7 行++,引用 Bootstrap 框架
- ++第 9 行++,引用 FontAwesome 字型
- ++第 13, 16 行++,新增一組 `<div></div>` 標籤,套用 `container` 類別,用來放置頁面要呈現的內容
- ++第 14 行++,透過 Django 的 `{% include %}` 標符,將方才新增的導覽列加進頁面範本中修改後,導覽列的顯示效果如下:
美化日誌功能
使用者登入
先前在表單所使用的頁面範本中,要產生每個欄位的輸入元件時,都是使用 `{{ form.as_p }}` 或 `{{ form.as_table }}` 來將頁面範本接收到的 `form` 變數中關於欄位的定義自動展開。這種方式的好處是方便,但是較難針對欄位的外觀做較細部的處理。
另一種方式則是自行撰寫所需欄位的 HTML 碼,只要輸入元件的 `name` 屬性與其所屬的資料模型或表單定義內的欄位名稱相同,就可以用來在頁面上接收使用者的輸入,在送出表單後,Django 也能正常處理。以下示範不透過 `form` 變數來自動產生輸入元件的 HTML 碼,而改採使用者自行撰寫的方式來修改使用者登入表單的頁面範本。
修改 `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="row row-cols-lg-auto align-items-center"> <div class="col-12"> <input name="username" autofocus="" required="" id="id_username" maxlength="254" type="text" class="form-control mr-2" placeholder="帳號"> </div> <div class="col-12"> <input name="password" required="" id="id_password" type="password" class="form-control mr-2" placeholder="密碼"> </div> <div class="col-12"> <input type="submit" value="login" class="btn btn-primary"/> </div> </div> </form> {% endblock %}
- ++第 4 - 6 行++,如果表單有錯誤的話,就顯示訊息。
- 當送出表單資料後,Django 在驗證使用者填寫的資料時發現有錯誤,就會返回原本的頁面,並在 `form` 變數的 `errors` 屬性填入相關錯誤訊息。
- 對登入表單來說,驗證失敗最主要的狀況就是帳號與密碼錯誤,所以可以直接將訊息寫死在頁面範本中。當然,也可以使用 `{{ form.errors }}` 將 Django 填入的錯誤訊息輸出,但是它的輸出樣式可能不符合需求。
- ++第 10, 20 行++,用來包覆欲行內輸入元件的容器,需套用 `row`、`row-cols-lg-auto` 類別讓其下的輸入元件可以接在同一行。
- ++第 11 - 19 行++,輸入元素的 HTML 原始碼
- 為了一致性的外觀,每個輸入元素的標籤(`<input>`)都套用 `form-control` 類別
- 帳號與密碼輸入標籤中的 `required` 屬性用來指示這兩個輸入欄位必須要輸入內容
- 帳號輸入標籤中的 `autofocus` 屬性是指頁面載入時,使用者輸入的焦點要自動移到這個元素上。使用者不需要事先以滑鼠點按這個輸入元件,按鍵盤登打的內容會直接填入這個入元件中。使用者登出
修改 `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 %}
- ++第 4 行++,修改訊息外觀。Bootstrap 框架的 `alert` 類別是套用在包覆警告、通知訊息用的容器上,需額外搭配相對應的類別,以產生不同的前背景配色組合:
搭配類別 外觀示例 `alert-primary` `alert-secondary` `alert-success` `alert-danger` `alert-warning` `alert-info` `alert-light` `alert-dark` - ++第 5 行++,將連結外觀變更為按鈕樣式(套用 `btn btn-primary` 類別),並加入圖示
日誌列表
修改 `templates/journal/journal_list.html` :
{% extends "base.html" %} {% block content %} <h2><i class="fas fa-book"></i> 我的日誌:</h2> <table class="table table-sm"> <tr><td>時間</td><td>項目</td><td>功能</td></tr> {% for journal in journal_list %} <tr> <td>({{ journal.created|date:"l" }}){{ journal.created }}</td> <td><a href="{% url 'journal_edit' journal.id %}">{{journal.content}}</a></td> <td><a href="{% url 'journal_delete' journal.id %}" class="btn btn-sm btn-danger py-0 px-1"><i class="fas fa-trash-alt"></i> 刪除</a></td> </tr> {% endfor %} </table> {% include 'pagination.html' %} {% endblock %}
- ++第 4 行++,加上圖示
- ++第 5 行++,表格套用 `table table-sm` 類別
- ++第 11 行++,修改「刪除」連結外觀
- 套用 `btn btn-sm btn-danger py-0 px-1` 類別
`py-0` 表示不留上下內邊界(padding),`px-1` 表示使用第 1 級左右內邊界
- 加上垃圾桶圖示修正時間格式
日誌列表的時間格式目前顯示的語言是英文,若要修正為中文的話,需要修改專案設定值。
修改 `assistant/settings.py` ,將 `LANGUAGE_CODE` 以及 `TIME_ZONE` 的值修正如下:
# Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ LANGUAGE_CODE = 'zh-hant' TIME_ZONE = 'Asia/Taipei'
* `LANGUAGE_CODE` 設為 `zh-hant` 表示要使用正體中文來顯示時間字串
* `TIME_ZONE` 設為 `Asia/Taipei` 表示要使用臺北所在的時區來處理時間欄位美化分頁連結
修改 `templates/pagination.html`
{% if is_paginated %} <div> {% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}" class="btn btn-sm btn-primary"><i class="fas fa-chevron-circle-left"></i>上一頁</a> {% endif %} {% for page in paginator.page_range %} {% if page == page_obj.number %} <button class="btn btn-sm btn-primary" disabled>{{ page }}</button> {% else %} <a href="?page={{ page }}" class="btn btn-sm btn-primary">{{ page }}</a> {% endif %} {% endfor %} {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}" class="btn btn-sm btn-primary">下一頁<i class="fas fa-chevron-circle-right"></i></a> {% endif %} </div> {% endif %}
修改項目主要有 2 個:
- 為「上一頁」與「下一頁」加上適合的圖示
- 為所有的連結套用 `btn btn-sm btn-primary` 類別我的帳本
修改 `templates/expenses/expense_list.html`
{% extends "base.html" %} {% block content %} <h2><i class="fas fa-money-check"></i> 我的帳本:</h2> <table class="table table-sm"> <tr> <th>時間</th> <th>項目</th> <th>類別</th> <th>金額</th> <th>功能</th> </tr> {% for expense in expense_list %} <tr> <td>({{ expense.time|date:"l" }}) {{ expense.time }}</td> <td><a href="{{ expense.id }}/update/">{{ expense.item }}</a></td> <td>{{ expense.get_category_display }}</td> <td>{{ expense.amount }}</td> <td><a href="{{ expense.id }}/delete/" class="btn btn-sm btn-danger py-0 px-1"><i class="fas fa-trash-alt"></i> 刪除</a></td> </tr> {% endfor %} </table> {% include 'pagination.html' %} {% endblock content %}
主要修改以下項目:
- ++第 5 行++,表格套用 `table` 以及 `table-sm` 類別
- ++第 19 行++,修改「刪除」連結外觀
- 套用 `btn btn-sm btn-danger py-0 px-1` 類別
`py-0` 表示不留上下內邊界(padding),`px-1` 表示使用第 1 級左右內邊界
- 加上垃圾桶圖示寫日誌與記帳
修改頁面範本
修改 `templates/form.html`
{% 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 %}
美化表單
頁面範本 `templates/form.html` 中採用了 `{{ form.as_table }}` 的方式來產生輸入元件的 HTML 碼,所以才能讓新增、修改日誌以及記帳等功能共用同一個頁面範本。但是這樣一來,也無法直接在頁面範本中指定 `<input>` 標籤來套用 `form-control` 類別來修改它們的外觀。
如果想要在頁面範本中保持採用 `{{ form.as_table }}` 讓多項輸要輸入資料的功能可以共用同一個頁面範本的話,該如何針對自動產生的輸入元件的標籤做相關的調整呢?官方的標準做法是自行撰寫表單類別,在類別中指定每個輸入欄位的相關設定。示範如下:
新增 `journal/forms.py`,在其中自行定義表單類別:
from django.forms import ModelForm, Textarea from .models import Journal class JournalForm(ModelForm): # 自訂表單定義 class Meta: # 表單元類別 model = Journal # 指定參考的資料模型 fields = ['content'] # 在表單中要顯示哪些輸入欄位 widgets = { 'content': Textarea(attrs={'class': 'form-control'}), }
- ++第 4 - 10 行,自訂一個表單類別 `JournalForm` 繼承自 `ModelForm` 類別
`django.forms` 套件中定義了一個 `ModelForm` 的類別,可以由指定的資料模型(`Model`)定義產生相對應的表單定義
- ++第 5 - 10 行++,定義 `JournalForm` 類別的元類別(`class Meta`),注意,`Meta` 不可以改成其他名稱
- ++第 6 行++,`model` 屬性用來指定參考的資料模型
- ++第 7 行++,`fields` 屬性用來指定在表單中要包含參考的資料模型中的哪些欄位
- ++第 8 - 10 行++,`widgets` 屬性用來指定輸入欄位要使用的小工具的定義,以字典的方式來指定欄位與其使用的小工具
- ++第 9 行++,`content` 欄位以多行文字輸入元件 `Textarea` 的型式(也就是 HTML 中的 `<textarea></textarea>` 標籤)呈現在頁面上。
要修改輸入元件產生的 HTML 標籤的屬性,可以透過關鍵字參數 `attrs` 來指定,這個參數接受字典形式的內容,鍵值的部份欲修改的屬性名稱,對應值的部份則是該屬性欲指定的設定值。以此例來說,我們想要讓 `<textarea></textarea>` 套用 `form-control` 類別,若是自行撰寫 HTML 碼的話,就需要在 `<textarea>` 標籤中指定 `class` 屬性為 `form-control`,例:<textarea class="form-control"> </textarea>
在自訂表單定義類別中,就要以 `{'class': 'form-control'}` 的方式來指定。若想修改多個屬性,則在 `attrs` 字典中加入其它鍵值與對應值的配對即可,例:
attrs={'class': 'form-control', 'autofocus': true}
自訂了表單定義後,接著修改通用視圖,指定使用自訂的表單定義。
請修改 `journal/views.py`,指定 `JournalCreate` 、 `JournalUpdate` 等 2 個 class 的 `form_class` 屬性:
from .models import Journal from django.contrib.auth.mixins import LoginRequiredMixin from .forms import JournalForm # 引用自訂表單定義
## 新增日誌 class JournalCreate(LoginRequiredMixin, CreateView): form_class = JournalForm success_url = reverse_lazy('journal_list') # 操作成功後重新導向日誌列表頁面 template_name = 'form.html' ## 修改日誌 class JournalEdit(LoginRequiredMixin, UpdateView): model = Journal form_class = JournalForm success_url = reverse_lazy('journal_list') # 操作成功後重新導向日誌列表頁面 template_name = 'form.html'
修改處如下:
- 新增++第 6 行++,引用方才定義的自訂表單 `JournalForm`
- 新增++第 17, 24 行++,透過 `form_class` 屬性指定使用自訂表單定義 `JournalForm`
- 刪除 `JournalCreate` 以及 `JournalUpdate` 下的 `fields` 屬性設定
- 由於 `fields` 屬性在自訂表單 `JournalForm` 中已經指定過了,不需要重覆指定,可以移除。
- 刪除 `JournalCreate` 類別下的 `model` 屬性設定
- 理由同上
- `JournalEdit` 需要先載入欲修改的紀錄,再放到表單上,所以 `JournalEdit` 裡的 `model` 屬性設定不可以移除
同理,要修改記帳功能表單的外觀,也要修改 `expenses` 應用程式的 `forms.py` 以及 `views.py`。新增自訂表單定義 `expenses/forms.py`:
from django.forms import ModelForm, TextInput, Select, NumberInput from .models import Expense class ExpenseForm(ModelForm): class Meta: model = Expense # 參考資料模型 Expense fields = '__all__' # 顯示*所有*欄位 widgets = { 'item': TextInput(attrs={'class': 'form-control'}), 'category': Select(attrs={'class': 'form-control'}), 'amount': NumberInput(attrs={'class': 'form-control'}), }
|小工具(Widget)|說明|
|-|-|
|`TextInput`|單行文字框(`<input type='text'>`)|
|`Select`|下拉選單(`<select></select>`)|
|`NumberInput`|單行數字框(`<input type='number'>`)|修改 `expenses/view.py`,指定 `ExpenseCreate` 與 `ExpenseUpdate` 這兩個通用視圖使用方才自定義的 `ExpenseForm`:
from django.urls import reverse_lazy from .models import Expense from .forms import ExpenseForm ``` ``` python=14 # 新增支出紀錄 class ExpenseCreate(LoginRequiredMixin, CreateView): form_class = ExpenseForm template_name = 'form.html' # 指定使用 form.html 這個頁面範本 success_url = reverse_lazy('expense_list') # 新增成功返回支出紀錄列表頁面 # 修改支出紀錄 class ExpenseUpdate(LoginRequiredMixin, UpdateView): model = Expense form_class = ExpenseForm template_name = 'form.html' success_url = reverse_lazy('expense_list') # 新增成功返回支出紀錄列表頁面
修改方式請參考前一段關於 `JournalCreate` 與 `JournalEdit` 的修改說明。