實戰:記帳
-
接續上一個 `assistant` 專案與 `journal` 應用程式。現在想在數位助理專案額外加上記帳的功能,這功能跟原本的日誌是可以相互獨立運作。
在 Django 專案中,其實可以包含多個應用程式,接下來的記帳功能,就採用在同一個專案下新增另一個應用程式的方式來示範。
在專案中新增記帳應用程式
新增應用程式
先切換到專案資料夾下,再以管理腳本 `manage.py` 的 `startapp` 命令來新增應用程式:
python manage.py startapp expenses
`expenses` 是欲新增的應用程式名稱
將應用程式加入專案
新增完應用程式之後,還得修改專案的設定檔,將應用程式列入 `INSTALLED_APPS` 列表,才算將應用程式加入專案。
修改 `assistant/assistant/setting.py`:
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 行++程式碼:
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)** 中,以此例來說,這裡是以值組的方式來存放所有選項,改用列表的方式來存放也可以:CATE_CHOICES = [ (0, "未分類"), (1, "飲食"), (2, "衣服"), (3, "交通"), (4, "教育"), (5, "娛樂"), (99, "其它"), ]
- ++第 17-20 行++,定義支出紀錄所需要的欄位
- ++第 18 行++,支出類別是一個整數欄位,預設在表單中是以單行文字框的形式呈現,讓使用者自行輸入資料。如果希望在表單中讓 Django 自動產生下拉選單提供選項讓使用者選填的話,可以在定義欄位時,透過欄位的 `choices` 屬性指定要套用哪個選項列表:arrow_up: 未指定 `choices` 屬性時,預設以單行文字框供使用者自行輸入
:arrow_up: 指定 `choices` 屬性時,預設會以下拉選單呈現選項
- ++第 22-23 行++,定義 `__str__()` 方法來回傳代表紀錄的字串
套用到資料庫對資料模型進行新增、刪除、修改之後,還需要回到命令提示字元的視窗下輸入兩個指令,才能套用這些變更:
- 建立資料異動腳本
python manage.py makemigrations
- 執行資料庫遷移
python manage.py migrate
網址、視圖、範本、表單
定義路徑規則(`urls.py`)
在這個例子中,我們會實作支出紀錄列表、新增支出紀錄、修改支出紀錄以及刪除支出紀錄等 4 個功能,首先來定義這個應用程式要處理的路徑規則。
新增應用程式的路徑規則定義檔 `expenses/urls.py`,並在檔案內填入以下程式碼:
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` 應用程式的路徑規則納入整個專案中:
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`,進行以下修改
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`,填入以下程式碼:
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`,並填入以下程式碼:
{% 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`,修改為以下程式碼:
<!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 行++,新增「支出列表」及「新增消費」兩個連結
- 關於頁面範本中內建的 `{% url %}` 標籤
如果有額外參數的話,可以將參數依序列在路徑規則名稱之後,多個參數之間以空格隔開,例:
{% url '路徑規則名稱' 參數1 參數2 參數3 %}
例如:範例中有一條路徑規則如下:
path('<int:pk>/update/', ExpenseUpdate.as_view(), name='expense_update'),
在頁面範本中的 `expense` 變數裡存放了某一筆支出紀錄,可以這樣產生修改該筆紀錄的路徑:
{% url 'expense_update' expense.id %}