實戰:日誌
-
接下來我們要建立個人數位助理的網站專案,在這個專案中有 2 個獨立的功能:日誌以及記帳。
建立專案與應用程式
建立新專案
(1)在終端機中下達指令建立一個新的專案
django-admin startproject assistant
打完指令後,會產生一個 `assistant` 的資料夾
assistant/ manage.py assistant/ __init__.py settings.py urls.py wsgi.py
在專案下建立應用程式
cd assistant python manage.py startapp journal
- ++第 1 行++,變更工作資料夾至 `assistant` 專案
- ++第 2 行++,執行專案管理腳本檔 `manage.py` 在專案下建立 `journal` 應用程式修改 `assistant/settings.py`:
ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'journal', ]
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
# Internationalization # https://docs.djangoproject.com/en/5.0/topics/i18n/ LANGUAGE_CODE = 'zh-hant' TIME_ZONE = 'Asia/Taipei'
- 修改++第 28 行++,允許使用者以任何主機名稱來存取專案網站。
- 新增++第 40 行++,將剛才建立的應用程式 `journal` 加入網站專案中。
- 修改++第 58 行++,指定頁面範本的搜尋(儲存)資料夾,這樣可以將所有的頁面範本檔案放在同一個資料夾來統一管理。
- 修改++第 107 行++,指定內建界面使用正體中文語系的訊息。
- 修改++第 109 行++,將時區變更為臺北所在時區。
資料庫修改 `journal/models.py`:
from django.db import models # Create your models here. # 日誌 class Journal(models.Model): content = models.TextField("內容") created = models.DateField('建立日期', auto_now_add=True) def __str__(self): return self.content
- 新增++第 4 - 10 行++,定義日誌資料表
執行以下指令建立資料庫:
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser
- ++第 1 行++,建立資料庫異動腳本
- ++第 2 行++,將異動腳本實際套用至資料庫
- ++第 3 行++,建立網站管理員帳號開啟網站服務
python manage.py runserver 0.0.0.0:80
網址、視圖、範本、表單
網址
我們要建立相對應的網頁,開啟 `assistant/urls.py`,修改為以下程式碼。
from django.contrib import admin from django.urls import path, include from django.views.generic import RedirectView urlpatterns = [ path('admin/', admin.site.urls), path('journal/', include('journal.urls')), path('', RedirectView.as_view(url='journal/')), ]
- 修改++第 18 行++,從 `django.urls` 額外引用 `include`
- 新增++第 23, 24 行++,加入 2 條路徑規則
- ++第 23 行++,將自行建立的應用程式 `journal` 裡的所有路徑規則的路徑加上 `journal/` 前導字串後併入網站路徑規則
- ++第 24 行++,將空路徑重向導向至 `journal/`
新增日誌的路徑規則檔案 `journal/urls.py`:from django.urls import path from .views import * urlpatterns = [ path('', JournalList.as_view(), name='journal_list'), path('create/', JournalCreate.as_view(), name='journal_create'), path('<int:pk>/edit/', JournalEdit.as_view(), name='journal_edit'), path('<int:pk>/delete/', JournalDelete.as_view(), name='journal_delete'), ]
- ++第 2 行++,由目前的應用程式的 `views.py` 中引用所有相關資源
- ++第 5 - 8 行++,定義 4 條路徑規則分別對應日誌列表、新增日誌、修改日誌、刪除日誌`urls.py` 裡的 `include()` 的作用是什麼?
如上所示,我在們 `assistant/journal/urls.py` 中定義了應用程式 `journal` 的 4 條路徑規則:
路徑 對應功能 :- :- ` ` 日誌列表 `create/` 新增日誌 `<int:pk>/update/` 修改日誌 `<int:pk>/delete/` 刪除日誌 又在 `assistant/urls.py` 定義網站專案的路徑規則中,以++第 24 行++ 程式碼:
path('journal/', include('journal.urls')),
將應用程式 `journal` 定義的路徑規則以前置 `journal/` 字串的方式加入網站專案中,等同於在專案的路徑規則中加入以下 4 條規則:
路徑 對應功能 :- :- `journal/` 日誌列表 `journal/create/` 新增日誌 `journal/<int:pk>/update/` 修改日誌 `journal/<int:pk>/delete/` 刪除日誌 視圖
開啟 `assistant/journal/views.py`,修改為以下程式碼:
from django.shortcuts import render from django.urls import reverse_lazy from django.views.generic import ListView, CreateView, UpdateView, DeleteView from .models import Journal ## Create your views here. ## 日誌列表 class JournalList(ListView): model = Journal ordering = ['-id'] # 依 id 欄位反向排序(新的在前面) ## 新增日誌 class JournalCreate(CreateView): model = Journal fields = ['content'] # 自動產生表單時僅顯示 content 欄位 success_url = reverse_lazy('journal_list') # 操作成功後重新導向日誌列表頁面 template_name = 'form.html' ## 修改日誌 class JournalEdit(UpdateView): model = Journal fields = ['content'] # 自動產生表單時僅顯示 content 欄位 success_url = reverse_lazy('journal_list') # 操作成功後重新導向日誌列表頁面 template_name = 'form.html' ## 刪除日誌 class JournalDelete(DeleteView): model = Journal success_url = reverse_lazy('journal_list') # 操作成功後重新導向日誌列表頁面 template_name = 'confirm_delete.html'
- 新增++第 2 行++,從 `django.views.generic` 引用 4 個通用視圖類別:`ListView`、`CreateView`、`UpdateView` 以及 `DeleteView`
- 新增++第 3 行++,從目前應用程式的 `models.py` 中引用 `Journal` 資料表定義
- 新增++第 7 - 30 行++,繼承 4 個通用視圖類別,分別實作日誌列表、新增日誌、修改日誌以及刪除日誌等 4 個視圖
- ++第 9 行++,修改排序依據,改為依 `id` 欄位由大到小排序
- ++第 15, 22 行++,在新增日誌以及修改日誌自動產生的表單上,僅顯示 `content` 欄位
- ++第 16, 23, 29 行++,新增、修改、刪除日誌後,導向日誌列表的頁面,這邊我們使用 `success_url` 屬性來指定重新導向的頁面路徑,透過 `reverse_lazy()` 函式,以路徑規則名稱來反推其路徑
- ++第 17, 24, 30 行++,自行指定欲使用的頁面範本範本
為了將所有的頁面範本集中管理,前面在專案設定檔中已經指定了頁面範本的搜尋路徑,接下來我們就將所有的頁面範本都放在這個資料夾下。
建立頁面範本資料夾
新增頁面範本資料夾 `templates`
建立 `journal` 應用程式的頁面範本資料夾 `templates/journal`
新增頁面範本
新增網站基底頁面範本 `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> </div> <div>{% block content %}{% endblock %}</div> </body> </html>
新增**日誌列表**頁面範本 `templates/journal/journal_list.html`:
{% extends 'base.html' %} {% block content %} <h2>我的日誌:</h2> <table> <tr> <th>時間</th> <th>項目</th> <th>操作</th> </tr> {% for journal in journal_list %} <tr> <td>{{ journal.created|date:"Y-m-d" }}</td> <td><a href="{% url 'journal_edit' journal.id %}">{{ journal.content }}</a></td> <td><a href="{% url 'journal_delete' journal.id %}">刪除</a></td> </tr> {% endfor %} </table> {% endblock %}
- ++第 11 - 17 行++,將 `journal_list` 中的每筆紀錄拿出來處理,每次拿到一筆紀錄就放在 `journal` 這個變數中
- ++第 13 行++
``` python
{{ journal.created|date:"Y-m-d" }}
```
的意思是將取得的 `journal` 紀錄的 `created` 欄位,經由過濾器 `date` 處理後再輸出。而在使用 `date` 過濾器時,額外給定了參數來指定處理後的輸出格式。這邊給定的 `"Y-m-d"` (小寫英文字母) 表示要以「西元年-月-日」的格式輸出。關於 date 過濾器
它是用來處理日期/時間資料的過濾器,依據使用者指定的格式輸出。以下摘列 date 過濾器接受的部份常用格式字元及其代表意義:
|格式字元|說明|範例輸出|
|:-:|:-|:-|
|`d`|月份的第幾天,以前置 0 補滿 2 位| '01' 到 '31'|
|`j`|月份的第幾天,無前置 0| '1' 到 '31' |
|`D`|短文字型式的星期幾| `Fri` |
|`l`|長文字型式的星期幾| `Friday` |
|`w`|數字型式的星期幾| '0'(星期天) 到 '6'(星期六)|
|`z`|年份中的第幾天,由 0 起算| '0' 到 '365' |
|`W`|ISO-8601標準中的週次,以星期一為一週起始| '1' 到 '53' |
|`m`|月份,以前置 0 補滿 2 位| '01' 到 '12' |
|`n`|月份,無前置 0 | '1' 到 '12' |
|`M`|短文字型式的月份名| 'Jan' |
|`b`|短文字型式的月份名,全小寫| 'feb' |
|`F`|長文字型式的月份名| 'September' |
|`t`|當月份的總日數| '28' 到 '31' |
|`y`|2位型式的西元年份| '19' |
|`Y`|4位型式的西元年份| '2019' |
|`L`|當年是否為閏年| 'True' 或 'False' |
|`g`|12小時制的小時,無前置 0| '1' 到 '12' |
|`G`|24小時制的小時,無前置 0| '0' 到 '23' |
|`h`|12小時制的小時,以前置 0 補滿 2 位| '01' 到 '12' |
|`H`|24小時制的小時,以前置 0 補滿 2 位| '00' 到 '23' |
|`i`|分鐘,以前置 0 補滿 2 位| '00' 到 '59' |
|`s`|秒數,以前置 0 補滿 2 位| '00' 到 '59' |**注意,在正體中文語系下:**
- 'D' 與 'l' 沒有差別,都會以 '星期一' 這樣的格式顯示星期幾
- 月份的 'M', 'b', 'F' 也無差別,都會以 '一月' 這樣的方式呈現完整列表請參見官方網站說明文件:
[https://docs.djangoproject.com/en/5.0/ref/templates/builtins/#date](https://docs.djangoproject.com/en/5.0/ref/templates/builtins/#date)
新增**共用編輯表單**頁面範本 `templates/form.html`
{% extends 'base.html' %} {% block content %} <form action="" method="post"> {% csrf_token %} <table> {{ form.as_table }} </table> <input type="submit" value="送出" /> </form> {% endblock %}
- ++第 5 行++,產生 CSRF 驗證資料
- ++第 7 行++,將 `form` 變數中的表單欄位以表格的型式輸出新增**共用刪除表單**頁面範本`templates/confirm_delete.html`:
{% extends 'base.html' %} {% block content %} <h2>刪除紀錄</h2> <p>{{ object }}</p> <p>確定要刪除這筆記錄嗎?</p> <form action="" method="post"> {% csrf_token %} <input type="submit" value="是的,我要刪除" /> </form> {% endblock %}
使用者登入與登出
新增登入登出路徑規則
開啟 `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')), ]
- 新增++第 25 行++,引用 Django 框架內建的 `django.contrib.auth` 應用程式定義的路徑規則,將其前方加上 `accounts/` 後納入專案的路徑規則。
- 直接利用內建的應用程式 `django.contrib.auth` 來處理使用者登入與登出。
開啟專案設定檔 `assistant/assistant/settings.py`,增加以下程式碼:
# Redirect to home URL after login (Default redirects to /accounts/profile/) LOGIN_REDIRECT_URL = '/' # 設定登人後導向的頁面
登入登出頁面範本
新增資料夾 `templates/registration`
新增**登入**頁面範本 `templates/registration/login.html`:
{% extends "base.html" %} {% block content %} {% if form.errors %} <p>帳號或密碼不符合,請再試一次。</p> {% endif %} <form action="" method="post"> {% csrf_token %} <div> <td>帳號:</td> <td>{{ form.username }}</td> </div> <div> <td>密碼:</td> <td>{{ form.password }}</td> </div> <div> <input type="submit" value="登入" /> </div> </form> {% endblock %}
- ++第 4 - 6 行++,若 `form.errors` 不是空的,表示登入有誤,因此顯示錯誤訊息。
- ++第 10 - 17 行++,除了像前一個留言板的範例用 `{{ form.as_p }}` 或 `{{ form.as_table }}` 來自動將表單元件轉成 HTML 碼之外,也可以像這個例子,自己編寫表單元素的 HTML 碼。新增**登出**頁面範本 `templates/registration/logged_out.html`:
{% extends "base.html" %} {% block content %} <p>您已登出!!</p> <a href="{% url 'login'%}">請按此處重新登入</a> {% endblock %}
修改**網站基底**頁面範本 `templates/base.htm`,加上登出/登入連結:
<!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> {% 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 - 21 行++,若使用者已登入,則顯示登入帳號以及登出連結,否則顯示登人連結
- ++第 13 行++,使用者登人系統後,`user` 變數的 `is_authenticated` 欄位會被設為 `True`。因此可以透過檢查這個欄位的值來得知使用者是否在已登入系統的狀態。
- ++第 15, 20 行++,`{% url 'logout' %}` 的作用是將被命名為 `logout` 的那條路徑規則對應的路徑印出來。同理,`{% url 'login' %}` 則是將被命名為 `login` 的那條路徑規則所對應的路徑印出來。- 這 2 條名為 `login` 與 `logout` 的路徑規則有被定義在 `django.contrib.auth` 應用程式的路徑規則中,還記得剛才我們有將它們引入專案的路徑規則組嗎?
限制登入後才能執行日記功能
到目前為止,雖然提供了使用者登人與登出的功能,但實際上使用者登入與否並不影響他在這個網站上所能進行的操作,即使在沒登入的狀態也能寫日誌、修改日誌、甚至刪除日誌。
你可能會想到在可以頁面範本中檢查使用者的登入狀況,再決定是否要顯示相關的連結。但是,看不到連結,不代表不能手動在網址列打上這些功能的存取路徑,一旦使用者「猜」到他想執行的操作所對應到的路徑,他可以自己打在網址列上,再讓瀏覽器送出存取請求來執行他想進行的動作。
為了做好網站的權限管控,不能只把不讓使用者操作的連結藏起來就好,而是應該在處理使用者請求執行某項功能的時候,要同時檢核他的權限。
這個範例的需求比較簡單,因為它是個人用的數位助理,所以只要檢查使用者是否已登入就好,不需要更複雜的權限管理。
開啟 `journal/views.py`,修改為以下程式碼:
from django.shortcuts import render from django.urls import reverse_lazy from django.views.generic import ListView, CreateView, UpdateView, DeleteView from .models import Journal from django.contrib.auth.mixins import LoginRequiredMixin ## Create your views here. ## 日誌列表 class JournalList(LoginRequiredMixin, ListView): model = Journal ordering = ['-id'] # 依 id 欄位反向排序(新的在前面) ## 新增日誌 class JournalCreate(LoginRequiredMixin, CreateView): model = Journal fields = ['content'] # 自動產生表單時僅顯示 content 欄位 success_url = reverse_lazy('journal_list') # 操作成功後重新導向日誌列表頁面 template_name = 'form.html' ## 修改日誌 class JournalEdit(LoginRequiredMixin, UpdateView): model = Journal fields = ['content'] # 自動產生表單時僅顯示 content 欄位 success_url = reverse_lazy('journal_list') # 操作成功後重新導向日誌列表頁面 template_name = 'form.html' ## 刪除日誌 class JournalDelete(LoginRequiredMixin, DeleteView): model = Journal success_url = reverse_lazy('journal_list') # 操作成功後重新導向日誌列表頁面 template_name = 'confirm_delete.html'
- 新增++第 5 行++,引用 Django 內建的 `LoginRequiredMixin`
- `LoginRequiredMixin` 是一個混成的類別,用來限定被混成的類別一定要在已登入的狀態才允許操作,否則會將導向登入頁面要求使用者登入。
- 新增++第 9, 14, 21, 28 行++,將 `LoginRequiredMixin` 加進 `JournalList`、`JournalCreate`、`JournalUpdate`、`JournalDelete` 的繼承列表
【注意】
要將混成的類別寫在原始繼承類別的前面。
修改完成後,若在未登入的情況下,不論是點按日誌列表,或是寫日誌,亦或是直接在網址列登打修改某篇日誌或刪除某篇日誌的路徑,都會直接被轉址到要求使用者登入的頁面,待登入成功後才能繼續原先欲操作的功能:
Mixin 混成是什麼?
Mixin 是在物件導向程式設計語言裡一種特殊的類別(Class),它有自身定義的方法(Method),但是無法單獨被實體化為物件,必須依附其他類別才可以運作。混成類別通常是用來擴充被依附的類別的功能。
你可以把混成類別想像有特殊功能的附屬套件,需安裝到主要類別來增強主要類別的功能。舉個例子來說,電影變型金鋼裡的領袖柯博文,它的原始設定是一部大貨車,它可以在地上奔馳,可以變形為機器人,但是它不能在天空飛行。但是在後續的電影續集中,它的貨櫃被賦予了飛行套件的意義,因此當柯博文裝上了飛行套件之後,除了它本身具有的能力之外,透過飛行套件,它也取得了飛行的能力。
分頁顯示在日誌列表的頁面上,會將所有的日誌記錄全部列出來,但當日誌經過一段時間的累積之後,在同一個頁面上要顯示所有的日誌,有可能會因為數量太大,而導致頁面產生的速度過慢。因此一般的網站在處理清單式的頁面的時候,通常都會採用分頁的手法,將所有紀錄拆分成多頁顯示,每一項最多僅顯示固定筆數的紀錄。
修改視圖,加入分頁設定
開啟 `assistant/journal/views.py`,修改 `JournalList` 類別:
# 日誌列表 class JournalList(LoginRequiredMixin, ListView): model = Journal ordering = ['-id'] # 依 id 欄位反向排序(新的在前面) paginate_by = 3 # 設定每頁最多顯示的資料筆數
- 新增++第 12 行++,通用視圖 `ListView` 的衍生類別可以透過指定 `paginate_by` 屬性來指定每頁顯示的最多資料筆數
_註:為了便於示範,在不產生過多測試資料的情況下,這裡設定每頁最多只顯示 3 筆日誌。實務上每頁顯示的資料量應該會更多。
處理頁面範本,加入分頁連結
在視圖中指定 `paginate_by` 屬性值,`ListView` 會自動依據這個設定取出目前頁面的資料量。因此頁面範本中只能取得最多 `paginate_by` 筆數的紀錄,但在頁面上顯示這些紀錄後,我們目前的頁面範本並不會自動顯示上一頁、下一頁、或其他分頁頁碼的連結。這部份不是 `ListView` 的工作,頁面如何呈現,該呈現什麼,這是頁面範本的任務。因此需要修改頁面範本,來規範這些分頁的連結該出現在何處,以及如何呈現這些分頁連結。
建立分頁連結範本
首先新增分頁連結的範本 `templates/pagination.html`:
{% if is_paginated %} <div> {% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}">上一頁</a> {% endif %} {% for page in paginator.page_range %} {% if page == page_obj.number %} <a href="#"><font color="red">{{ page }}</font></a> {% else %} <a href="?page={{ page }}">{{ page }}</a> {% endif %} {% endfor %} {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}">下一頁</a> {% endif %} </div> {% endif %}
- `is_paginated`、`page_obj`、`paginator` 這是 `ListView` 會傳遞給頁面範本的變數
- `is_paginated` 用來表示目前頁面是否需要顯示分頁。當總資料量沒超過 `paginate_by` 指定的數值的時候,是不需要顯示分頁的。
- `page_obj` 裡記錄了當前分頁的資訊,比方說現在顯示的是第幾頁,當前分頁是否有上一頁或下一頁,而它們的頁碼又分別是多少?
- `paginator` 則包含了完整的分頁資訊,例如:全部的資料筆數有多少,總共被分為幾頁,分頁的頁碼範圍等。
- ++第 1 - 17 行++,當需要顯示分頁時才產生相關 HTML 碼
- ++第 3 - 5 行++,當前的頁面如果有上一頁的話才顯示上一頁的連結
- `page_obj` 的 `has_previous` 屬性若為 `True` 的話代表目前分頁有上一頁
- `page_obj` 的 `previous_page_number` 則是上一頁的頁碼
- ++第 6 - 12 行++,顯示所有頁碼的連結
- ++第 6 行++,`paginator.page_range` 紀錄了完整的頁碼範圍,透過 `for` 迴圈取出每一個頁碼,每取出一個頁碼,就先記在 `page` 變數中
- ++第 7 - 11 行++,`page_obj.number` 指的是當前分頁的頁碼。若現在取出的頁碼 `page` 剛好就是當前分頁的頁碼的話,顯示成普通的文字;若非當前頁碼的話,才顯示為連結
- ++第 13 - 15 行++,當前的頁面如果有下一頁的話才顯示下一頁的連結
- `page_obj` 的 `has_next` 屬性若為 `True` 的話代表目前分頁有下一頁
- `page_obj` 的 `next_page_number` 則是下一頁的頁碼這個分頁連結的範本所產生的 HTML 碼並不是一個完整的頁面,僅產生分頁連結的部份而已。若有其他的頁面範本需要產生分頁連結的時候,可以在頁面範本中透過 `{% include %}` 標籤來引用分頁連結範本:
{% include 'pagination.html' %}
將分頁連結加入日誌列表
開啟日誌列表頁面範本 `templates/journal/journal_list.html`,修改為以下程式碼:
{% extends 'base.html' %} {% block content %} <h2>我的日誌:</h2> <table> <tr> <th>時間</th> <th>項目</th> <th>操作</th> </tr> {% for journal in journal_list %} <tr> <td>{{ journal.created|date:"Y-m-d" }}</td> <td><a href="{% url 'journal_edit' journal.id %}">{{ journal.content }}</a></td> <td><a href="{% url 'journal_delete' journal.id %}">刪除</a></td> </tr> {% endfor %} </table> {% include 'pagination.html' %} {% endblock %}
- 新增++第 19 行++,引入 `pagination.html` 分頁連結範本
- 直接利用內建的應用程式 `django.contrib.auth` 來處理使用者登入與登出。