<style>
.markdown-body img:not(.emoji) {
border-radius: 5px;
box-shadow: 1px 1px 5px dimgrey;
}
</style>
實戰:記帳
===
接續上一個 `assistant` 專案與 `journal` 應用程式。現在想在數位助理專案額外加上記帳的功能,這功能跟原本的日誌是可以相互獨立運作。
在 Django 專案中,其實可以包含多個應用程式,接下來的記帳功能,就採用在同一個專案下新增另一個應用程式的方式來示範。
## 在專案中新增記帳應用程式
### 新增應用程式
先切換到專案資料夾下,再以管理腳本 `manage.py` 的 `startapp` 命令來新增應用程式:
``` bash
python manage.py startapp expenses
```
`expenses` 是欲新增的應用程式名稱
### 將應用程式加入專案
新增完應用程式之後,還得修改專案的設定檔,將應用程式列入 `INSTALLED_APPS` 列表,才算將應用程式加入專案。
修改 `assistant/assistant/setting.py`:
``` python=33
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'journal',
'expenses',
]
```
- 新增++第 41 行++,在 `INSTALLED_APPS` 列表中加入方才新增的應用程式。
## 資料庫
### 定義資料模型
開啟 `expense/models.py`,新增++第 4 - 23 行++程式碼:
``` python=
from django.db import models
# Create your models here.
# 支出紀錄
class Expense(models.Model):
# 支出類別選項
CATE_CHOICES = (
(0, "未分類"),
(1, "飲食"),
(2, "衣服"),
(3, "交通"),
(4, "教育"),
(5, "娛樂"),
(99, "其它"),
)
# 欄位定義
item = models.CharField('項目', max_length=30)
category = models.IntegerField('支出類別', default=0, choices=CATE_CHOICES)
amount = models.IntegerField('支出金額', default=0)
time = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.item
```
- ++第 7-15 行++,定義支出類別選項的值與其對應的標籤文字,`CATE_CHOICES` 可自行命名。
- 每組選項值與標籤文字的對應關係以 **值組(tuple)** 的形式呈現
- 所有選項對應關係的列表可被放在 **列表(list)** 或 **值組(tuple)** 中,以此例來說,這裡是以值組的方式來存放所有選項,改用列表的方式來存放也可以:
``` python
CATE_CHOICES = [
(0, "未分類"),
(1, "飲食"),
(2, "衣服"),
(3, "交通"),
(4, "教育"),
(5, "娛樂"),
(99, "其它"),
]
```
- ++第 17-20 行++,定義支出紀錄所需要的欄位
- ++第 18 行++,支出類別是一個整數欄位,預設在表單中是以單行文字框的形式呈現,讓使用者自行輸入資料。如果希望在表單中讓 Django 自動產生下拉選單提供選項讓使用者選填的話,可以在定義欄位時,透過欄位的 `choices` 屬性指定要套用哪個選項列表

:arrow_up: 未指定 `choices` 屬性時,預設以單行文字框供使用者自行輸入

:arrow_up: 指定 `choices` 屬性時,預設會以下拉選單呈現選項
- ++第 22-23 行++,定義 `__str__()` 方法來回傳代表紀錄的字串
### 套用到資料庫
對資料模型進行新增、刪除、修改之後,還需要回到命令提示字元的視窗下輸入兩個指令,才能套用這些變更:
- 建立資料異動腳本
``` bash
python manage.py makemigrations
```
- 執行資料庫遷移
``` bash
python manage.py migrate
```
## 網址、視圖、範本、表單
### 定義路徑規則(`urls.py`)
在這個例子中,我們會實作支出紀錄列表、新增支出紀錄、修改支出紀錄以及刪除支出紀錄等 4 個功能,首先來定義這個應用程式要處理的路徑規則。
新增應用程式的路徑規則定義檔 `expenses/urls.py`,並在檔案內填入以下程式碼:
``` python=
from django.urls import path
from .views import *
urlpatterns = [
path('', ExpenseList.as_view(), name='expense_list'),
path('create/', ExpenseCreate.as_view(), name='expense_create'),
path('<int:pk>/update/', ExpenseUpdate.as_view(), name='expense_update'),
path('<int:pk>/delete/', ExpenseDelete.as_view(), name='expense_delete'),
]
```
如程式碼所示,在 `expenses` 應用程式中,我們定義了 4 條路徑規則,並分別為其命名。記得前面在修改專案的路徑規則時,有新增一條規則來將 `expenses` 應用程式的路徑規則納入整個專案中:
``` python
path('expenses/', include('expenses.urls')),
```
所以實際上,這支應用程式所定義的 4 個存取路徑,與其實際被加入專案時的存取路徑如下表:
|應用程式定義路徑|實際在專案中的存取路徑|功能|
|-|-|-|
|`''`|`'expenses/'`|支出紀錄列表|
|`'create/'`|`'expenses/create/'`|新增支出紀錄|
|`'<int:pk>/update/'`|`'expenses/<int:pk>/update/'`|修改支出紀錄|
|`'<int:pk>/delete/'`|`'expenses/<int:pk>/delete/'`|刪除支出紀錄|
### 將應用程式定義的路徑規則加入專案
開啟專案的路徑規則定義 `assistant/assistant/urls.py`,進行以下修改
``` python=21
urlpatterns = [
path('admin/', admin.site.urls),
path('journal/', include('journal.urls')),
path('', RedirectView.as_view(url='journal/')),
path('accounts/', include('django.contrib.auth.urls')),
path('expenses/', include('expenses.urls')),
]
```
- 新增++第 26 行++,將 `expenses` 應用程式中定義的路徑規則加入專案的路徑規則清單,並在加入時將 `expenses` 定義的規則的路徑前加上 `expenses/`
### 定義視圖(`views.py`)
開啟 `expenses` 應用程式的視圖檔 `expenses/views.py`,填入以下程式碼:
```python=
from django.views.generic import *
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from .models import Expense
# Create your views here.
# 支出紀錄列表
class ExpenseList(LoginRequiredMixin, ListView):
model = Expense
ordering = ['-id'] # 反向排序
paginate_by = 10 # 每頁顯示幾筆
# 新增支出紀錄
class ExpenseCreate(LoginRequiredMixin, CreateView):
model = Expense
fields = '__all__' # 在表單上顯示*所有*欄位
template_name = 'form.html' # 指定使用 form.html 這個頁面範本
success_url = reverse_lazy('expense_list') # 新增成功返回支出紀錄列表頁面
# 修改支出紀錄
class ExpenseUpdate(LoginRequiredMixin, UpdateView):
model = Expense
fields = '__all__'
template_name = 'form.html'
success_url = reverse_lazy('expense_list') # 新增成功返回支出紀錄列表頁面
# 刪除支出紀錄
class ExpenseDelete(LoginRequiredMixin, DeleteView):
model = Expense
template_name = 'confirm_delete.html'
success_url = reverse_lazy('expense_list') # 新增成功返回支出紀錄列表頁面
```
支出紀錄的列表、新增、修改、刪除等 4 個功能,同樣沿用通用視圖類別來實作,另外因為也限定這 4 個功能都需要登入後才能操作,因此在定義視圖類別時,也都使用了 `LoginRequiredMixin` 這個混成類別來加入是否已經登入的權限檢查。
### 定義頁面範本(templates)
支出紀錄的新增、修改、刪除,直接共用前一個日誌應用程式 `journal` 已定義的表單範本(`form.html`)以及確認刪除範本(`confirm_delete.html`)就好,只需要新增支出紀錄列表的頁面範本。
#### 新增支出列表頁面範本
新增 `templates/expenses` 資料夾,並於該資料夾下新增 `expense_list.html`。
開啟頁面範本檔案 `templates/expenses/expense_list.html`,並填入以下程式碼:
``` html=
{% extends "base.html" %}
{% block content %}
<h2>我的支出紀錄</h2>
<table>
<tr>
<th>時間</th>
<th>項目</th>
<th>類別</th>
<th>金額</th>
<th>功能</th>
</tr>
{% for expense in expense_list %}
<tr>
<td>{{ expense.time|date:"(l)Y-m-d" }}</td>
<td><a href="{% url 'expense_update' expense.id %}">{{ expense.item }}</a></td>
<td>{{ expense.get_category_display }}</td>
<td>{{ expense.amount }}</td>
<td><a href="{% url 'expense_delete' expense.id %}">刪除</a></td>
</tr>
{% endfor %}
</table>
{% include 'pagination.html' %}
{% endblock content %}
```
- ++第 13-21 行++,以迴圈標籤將取得的支出紀錄一筆一筆列出
- ++第 17 行++,印出支出紀錄的 `category` 欄位值對應的標籤文字
- 在定義資料模型時,若在定義個欄位時同時指定該欄位的 `choices` 屬性,資料模型會為自動為該欄位產生 `.get_欄位名稱_display()` 這個方法來回傳欄位儲存值所對應的標籤文字
- 承上,因為支出紀錄的 `category` 欄位有指定其 `choices` 屬性,所以 `Expense` 資料模型自動產生了 `.get_category_display()` 方法
- 在頁面範本中以 `{{ expense.get_category_display }}` 呼叫 `Expense` 類別的 `.get_category_display()` 方法將取得的支出紀錄 `expense` 的支出類別欄位儲存值所對應標籤文字。舉例來說,若某筆紀錄的 `category` 欄位值為 `1`,透過 `{{ expense.get_category_display}}` 則會傳回 `飲食`。
- ++第 23 行++,引用先前已經做好的分頁顯示的頁面範本來產生分頁連結

#### 修改網站頁面基底範本
因為新增了支出紀錄的相關功能,所以需要修改網站基底範本,將「支出列表」以及「新增消費」連結加上,以方便使用相關功能。
開啟檔案 `assistant/templates/base.html`,修改為以下程式碼:
``` html=
<!DOCTYPE html>
<html lang="zh-hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>數位助理</title>
</head>
<body>
<h1>數位助理</h1>
<div>
<a href="{% url 'journal_list' %}">日誌列表</a>
<a href="{% url 'journal_create' %}">寫日誌</a>
<a href="{% url 'expense_list' %}">支出列表</a>
<a href="{% url 'expense_create' %}">新增消費</a>
{% if user.is_authenticated %}
{{ user.username }}
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button type="submit">登出</button>
</form>
{% else %}
<a href="{% url 'login' %}">登入</a>
{% endif %}
</div>
<div>{% block content %}{% endblock %}</div>
</body>
</html>
```
修改處如下:
- 新增++第 13, 14 行++,新增「支出列表」及「新增消費」兩個連結
::: info
:bulb: **關於頁面範本中內建的 `{% url %}` 標籤**
如果有額外參數的話,可以將參數依序列在路徑規則名稱之後,多個參數之間以空格隔開,例:
``` html
{% url '路徑規則名稱' 參數1 參數2 參數3 %}
```
例如:範例中有一條路徑規則如下:
``` python
path('<int:pk>/update/', ExpenseUpdate.as_view(), name='expense_update'),
```
在頁面範本中的 `expense` 變數裡存放了某一筆支出紀錄,可以這樣產生修改該筆紀錄的路徑:
``` python
{% url 'expense_update' expense.id %}
```
:::
