實戰:記帳

接續上一個 assistant 專案與 journal 應用程式。現在想在數位助理專案額外加上記帳的功能,這功能跟原本的日誌是可以相互獨立運作。

在 Django 專案中,其實可以包含多個應用程式,接下來的記帳功能,就採用在同一個專案下新增另一個應用程式的方式來示範。

在專案中新增記帳應用程式

新增應用程式

先切換到專案資料夾下,再以管理腳本 manage.pystartapp 命令來新增應用程式:

python manage.py startapp expenses

expenses 是欲新增的應用程式名稱

將應用程式加入專案

新增完應用程式之後,還得修改專案的設定檔,將應用程式列入 INSTALLED_APPS 列表,才算將應用程式加入專案。

修改 assistant/assistant/setting.py,新增第 41 行,在 INSTALLED_APPS 列表中加入方才新增的 expenses 應用程式:

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'journal', 'expenses', ]

將應用程式定義的路徑規則加入專案

雖然目前還尚未定義記帳應用程式的路徑規則,不過要將專案加入應用程式的話,這個步驟也不能省。

開啟專案的路徑規則定義 assistant/assistant/urls.py,新增第 25 行,將 expenses 應用程式中定義的路徑規則加入專案的路徑規則清單:

urlpatterns = [ path('admin/', admin.site.urls), path('', RedirectView.as_view(url='journal/')), path('journal/', include('journal.urls')), path('accounts/', include('django.contrib.auth.urls')), path('expenses/', include('expenses.urls')), ]

資料庫

定義資料模型

開啟 assistant/expense/models.py,新增以下程式碼:

# 支出紀錄 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

套用到資料庫

對資料模型進行新增、刪除、修改之後,還需要回到命令提示字元的視窗下輸入兩個指令,才能套用這些變更:

python manage.py makemigrations
python manage.py migrate

網址、視圖、範本、表單

定義路徑規則(urls.py)

在這個例子中,我們會實作支出紀錄列表、新增支出紀錄、修改支出紀錄以及刪除支出紀錄等 4 個功能,首先來定義這個應用程式要處理的路徑規則。

新增應用程式的路徑規則定義檔 assistant/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'), ]

定義視圖(views.py)

開啟 expenses 應用程式的視圖檔 assistant/expenses/views.py,填入以下程式碼:

from django.views.generic import * from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse 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 這個頁面範本 def get_success_url(self): return reverse('expense_list') # 新增成功返回支出紀錄列表頁面 # 修改支出紀錄 class ExpenseUpdate(LoginRequiredMixin, UpdateView): model = Expense fields = '__all__' template_name = 'form.html' def get_success_url(self): return reverse('expense_list') # 修改成功返回支出紀錄列表頁面 # 刪除支出紀錄 class ExpenseDelete(LoginRequiredMixin, DeleteView): model = Expense template_name = 'confirm_delete.html' def get_success_url(self): return reverse('expense_list') # 刪除成功返回支出紀錄列表頁面

支出紀錄的列表、新增、修改、刪除等 4 個功能,同樣沿用通用視圖類別來實作,另外因為也限定這 4 個功能都需要登入後才能操作,因此在定義視圖類別時,也都使用了 LoginRequiredMixin 這個混成類別來加入是否已經登入的權限檢查。

這邊刻意用比較麻煩的方式,在定義路徑時為規則命名,在通用視圖內定義 get_success_url() 到底有什麼用?想像一下如果日後因為某種原因,需要修改支出紀錄相關功能的存取路徑。如果使用現在示範的這種做法的話,只需要在 urls.py 修改路徑即可,不需要修改 views.py,甚至頁面範本也不需要修改;反之,若採用先前的做法,直接指定 success_url 屬性,當路徑規則相對應的路徑變更的時候,views.py 裡的 success_url 對應到的目標也要跟著做修正,當然,頁面範本內使用到相關路徑的地方也需要修改。

定義頁面範本(templates)

支出紀錄的新增、修改、刪除,直接共用前一個日誌應用程式 journal 已定義的表單範本(form.html)以及確認刪除範本(confirm_delete.html)就好,只需要新增支出紀錄列表的頁面範本。

新增支出列表頁面範本

新增 assistant/templates/expenses 資料夾,並於該資料夾下新增 expense_list.html

開啟頁面範本檔案 assistant/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" }}) {{ expense.time }}</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 %}

修改網站頁面基底範本

因為新增了支出紀錄的相關功能,所以需要修改網站基底範本,將「支出列表」以及「新增消費」連結加上,以方便使用相關功能。

開啟檔案 assistant/templates/base.html,新增第 14, 15 行,新增「支出列表」及「新增消費」兩個連結:

<!DOCTYPE html> <html lang="zh-hant"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>數位助理</title> </head> <body> <h1>數位助理</h1> <div> <a href="/journal/">日誌列表</a> <a href="/journal/create/">寫日誌</a> <a href="{% url 'expense_list' %}">支出列表</a> <a href="{% url 'expense_create' %}">新增消費</a> {% if user.is_authenticated %} {{ user.username }} <a href="{% url 'logout' %}">登出</a> {% else %} <a href="{% url 'login' %}">登入</a> {% endif %} </div> <div>{% block content %}{% endblock %}</div> </body> </html>

這裡使用了 Django 頁面範本的內建標籤 url 來傳回指定路徑規則的存取路徑,使用方式如下:

{% url '路徑規則名稱' %}

這邊可使用的 路徑規則名稱 必須是在 urls.py 裡定義路徑規則時,有額外透過 name 屬性命名過的為路徑規則。

:bulb: 關於頁面範本中內建的 {% url %} 標籤

如果有額外參數的話,可以將參數依序列在路徑規則名稱之後,多個參數之間以空格隔開,例:

{% url '路徑規則名稱' 參數1 參數2 參數3 %}

例如:範例中有一條路徑規則如下:

path('<int:pk>/update/', ExpenseUpdate.as_view(), name='expense_update'),

在頁面範本中的 expense 變數裡存放了某一筆支出紀錄,可以這樣產生修改該筆紀錄的路徑:

{% url 'expense_update' expense.id %}