實戰:留言板
-
<style> .markdown-body img:not(.emoji) { border-radius: 5px; box-shadow: 1px 1px 5px dimgrey; } </style>
接下來這個範例不大,但是可以讓我們再重頭複習一遍用 Django 建立網站專案的過程。這個簡單的留言板網站,允許任何人留言,但僅允許網站管理員登入後刪除留言。所以這個範例中也會示範如何讓登入及登出使用者。
建立專案與應用程式
(1)在終端機中下達指令建立一個新的專案
django-admin startproject guestbook
打完指令後,會產生一個 `guestbook` 的資料夾
guestbook/ manage.py guestbook/ __init__.py settings.py urls.py wsgi.py
(2)建立一個應用程式
cd guestbook python manage.py startapp web
修改 `guestbook/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', 'web', ]
- 修改++第 28 行++:允許以任何主機名稱來存取專案網站。
- 新增++第 40 行++:將方才新增的應用程式 `web` 加入專案中。LANGUAGE_CODE = 'zh-hant' TIME_ZONE = 'Asia/Taipei'
- 修改++第 107 行++,將預設的管理介面語系改為正體中文。
- 修改++第 109 行++,將時區改為臺北時間。定義資料模型
修改 `web/models.py`
from django.db import models # Create your models here. class Message(models.Model): user = models.CharField("姓名", max_length=50) subject = models.CharField("主旨", max_length=200) content = models.TextField("內容") created = models.DateTimeField("留言時間", auto_now_add=True) def __str__(self): return self.subject
- 新增++第 5 - 12 行++,定義留言資料模型
- ++第 11, 12 行++,定義 `__str__()` 方法,回傳資料代表字串
執行以下指令更新資料庫
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser
- ++第 1 行++,建立資料庫異動腳本
- ++第 2 行++,將異動腳本實際套用到資料庫
- ++第 3 行++,建立網站管理帳號完成後,啟用專案網站服務
python manage.py runserver 0.0.0.0:80
視圖、網址、範本、表單
我們要建立相對應的網頁,首先定義專案的路徑規則。開啟 `guestbook/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('message/', include('web.urls')), path('', RedirectView.as_view(url='message/')), ]
- 修改++第 18 行++,從 `django.urls` 多引用一個函式 `include`
- 新增第 23 行,將自訂應用程式 `web` 的路徑規則納入專案中,並在每條規則的路徑前插入 `message/` 字串
- 新增第 24 行,將空路徑重向導向 `message/`。在未指定路徑的情況下,自動轉向存取 `message/` 頁面。接著定義自訂應用程式 `web` 的路徑規則。新增檔案 `web/urls.py`
from django.urls import path from .views import * urlpatterns = [ path('', MessageList.as_view(), name='msg_list'), path('<int:pk>/', MessageView.as_view(), name='msg_view'), path('create/', MessageCreate.as_view(), name='msg_create'), ]
- ++第 2 行++,由目前應用程式的 `views` 模組中引用所有的項目
- ++第 5 - 7 行++,加入 3 條規則,分別表示留言列表、留言檢視以及新增留言這 3 個功能的路徑存取規則,同時在 `path()` 函式中加入 `name` 參數來幫每一條路徑規則命名。命名的目的是為了之後在程式碼中可以指定使用某條路徑規則。開啟 `web/views.py`,修改為以下程式碼。
from django.shortcuts import render from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from .models import * # 留言列表 class MessageList(ListView): model = Message ordering = ['-id'] # 以 id 欄位值由大至小反向排序 # 留言檢視 class MessageView(DetailView): model = Message # 新增留言 class MessageCreate(CreateView): model = Message fields = ['user', 'subject', 'content'] # 僅顯示 user, subject, content 這 3 個欄位 success_url = reverse_lazy('msg_list') # 新增成功後,導向留言列表
- ++第 9 行++,`ListView` 的衍生類別中,指定 `ordering` 屬性可調整資料的排序方式,此屬性的值為清單,清單內容為排序的依據,將欄位名稱填入即可。若要反向排序,則在欄位名稱前加上 `-`。以此例,我們希望留言列表時,由較新的留言先列出,再列出較舊的留言。
- ++第 19 行++,`reverse_lazy('msg_list')` 的意思是,拿 `msg_list` 這條路徑規則來反向推導它對應的存取路徑建立目錄 `web/templates/web`
新增專案網站基底範本 `guestbook/web/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="/">首頁</a> <a href="{% url 'msg_create' %}">新增留言</a> </div> <div>{% block content %}{% endblock %}</div> </body> </html>
新增留言列表頁面範本 `web/templates/web/message_list.html`
{% extends "base.html" %} {% block content %} <ul> {% for msg in message_list %} <li> {{ msg.created }} <a href="{% url 'msg_view' msg.id %}">{{ msg.subject }}</a> </li> {% endfor %} </ul> {% endblock %}
- ++第 8 行++,`{% url %}` 標籤用來指定要使用哪條路徑規則來反推存取路徑。
名稱為 `msg_view` 的路徑規則的定義如下:
```python
path('<int:pk>/', MessageDetail.as_view(), name='msg_view'),
```
因為其存取路徑包含了一個可變參數 `<int:pk>`,所以需要指定要拿什麼值來填入這個位置新增留言檢視頁面範本 `web/templates/web/message_detail.html`
{% extends "base.html" %} {% block content %} <h2>姓名</h2> {{ message.user }} <h2>主旨</h2> {{ message.subject }} <h2>內容</h2> {{ message.content|linebreaks }} {% endblock %}
- ++第 9 行++,讓 `message.content` 的內容讓過濾器 `linebreaks` 處理過之後再輸出。
- `linebreaks` 是 Django 的範本內建一個過濾器(filter),它的作用是將字串裡的換行符號轉成 HTML 的 `<br/>` 標籤,但若是換行符號後面跟著空行,則換行符號前這一整段非空行會被轉換成一對 `<p>`、`</p>` 包夾的段落。過濾器(filter)是什麼?
Django 頁面範本裡的過濾器是用來對欲輸出的資料先進行簡單的加工處理後再輸出,比方說將換行符號以 `<br/>` 取代(`linebreaks`),將英文字母全轉換為大寫(`upper`),修改日期時間資料的輸出格式(`date`),從清單中挑出第一個元素(`first`),...等。
使用的方式為 `{{ 欲被處理的資料|套用的過濾器 }}`
完整的頁面範本標籤及過濾器請參見官方網站:
https://docs.djangoproject.com/en/5.0/ref/templates/builtins/
新增表單頁面範本 `web/templates/message_form.html`
{% extends 'base.html' %} {% block content %} <form action="" method="post"> {% csrf_token %} <table> {{ form.as_table }} </table> <input type="submit" value="送出" /> </form> {% endblock %}
使用者登入與登出
前面我們已經完成了留言板的基本功能:留言列表、檢視留言內容、新增留言等。假設現在想要實作讓管理員刪除留言的功能,為了確認管理員身分,刪除留言需要登入系統後才可以操作。
接下來我們要示範如何透過 Django 內建的機制來進行網站登入。
修改專案設定檔 `guestbook/settings.py`,新增以下幾行:
# 登入後重新導向首頁 (預設會導向 /accounts/profile/) LOGIN_REDIRECT_URL = '/' # 登出後重新導向首頁 LOGOUT_REDIRECT_URL = '/'
- 若沒有在專案設定檔中指定 `LOGIN_REDIRECT_URL` 的話,使用者登入後會被導向 `/accounts/profile/` 這個不存在的路徑。
- 如果沒有指定 `LOGOUT_REDIRECT_URL` 的話,按下登出後,可能會被導向到管理後臺的登出畫面。修改 `guestbook/urls.py`:
urlpatterns = [ path('admin/', admin.site.urls), path('message/', include('web.urls')), path('', RedirectView.as_view(url='message/')), path('accounts/', include('django.contrib.auth.urls')), ]
- 新增++第 25 行++,引用 Django 框架內建的 `django.contrib.auth` 應用程式定義的路徑規則,將其前方加上 `accounts/` 後納入專案的路徑規則。
【說明】以下待排版
引入 `django.contrib.auth.urls` 之後,不需撰寫處理視圖,專案就多了以下的路徑:
|路徑|規則名稱(`name=`)|作用|
|-|-|-|
|`accounts/login/`|`login`|登入|
|`accounts/logout/`|`logout`|登出|
|`accounts/password_change/`|`password_change`|變更密碼|
|`accounts/password_change/done/`|`password_change_done`|已完成密碼變更|
|`accounts/password_reset/`|`password_reset`|重設密碼|
|`accounts/password_reset/done/`|`password_reset_done`|已送出密碼重設|
|`accounts/reset/<uidb64>/<token>/`|`password_reset_confirm`|密碼重設確認|
|`accounts/reset/done/`|`password_reset_complete`|已完成密碼重設|除了路徑之外,相對應的請求處理也都包含在 `django.contrib.auth` 應用程式中,現在只剩相關的頁面範本需要我們自己處理。
新增資料夾 `web/templates/registration`
新增登入頁面範本 `web/templates/registration/login.html`
{% extends "base.html" %} {% block content %} <form action="" method="post"> {% csrf_token %} {{ form.as_p }} <div> <input type="submit" value="登入"> </div> </form> {% endblock %}
新增登出頁面範本 `web/templates/registration/logged_out.html`
{% extends "base.html" %} {% block content %} <p>您已登出!!</p> <a href="{% url 'login' %}">請按此處重新登入</a> {% endblock %}
修改專案基底頁面範本 `guestbook/web/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="/">首頁</a> <a href="{% url 'msg_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 行++,如果使用者已登入,顯示其帳號名稱及登出連結,若為未登入狀態,則顯示登入連結
- 使用通用視圖時,Django會自動將使用者資訊傳遞給頁面範本,若需取用,可透過頁面範本中的 `user` 變數。該變數的 `is_authenticated` 欄位代表使用者是否已通過登入身份認證,而其 `username` 欄位則代表登入時所使用的帳號名稱。
- 這裡示範了 Django 頁面範本中的 `{% if %}`, `{% else %}`, `{% endif %}` 的完整結構,為二者擇其一的情況。
- ++第 15 - 18 行++,透過表單以 HTTP POST 方式存取登出的功能
- ++第 16 行++,表單裡記得一定要加入 `{% csrf_token %}`Django 5.0 的安全性更新
在 Django 5.0 版以前,可以簡單放置一個連結來操作登出功能
<a href="{% url 'logout' %}">登出</a>
但 Django 為了增進安全性,從 4.1 版起,支援以 HTTP POST 方式來執行登出功能,並自 5.0 版起,棄用以 HTTP GET 方式來存取內建的登出功能,僅能透過 HTTP POST 來執行。若使用舊的寫法,按下登出連結,會看到「HTTP ERROR 405」的錯誤畫面:同時我們查看命令提示字元也會看到「Method Not Allowed (GET)」的錯誤訊息:
因此在新版 Django 5.0 以後,需要改以表單透過 HTTP POST 的方式來執行登出的操作:
<form action="{% url 'logout' %}" method="post"> {% csrf_token %} <button type="submit">登出</button> </form>
限制登入後才能刪除留言
開啟 `web/urls.py`,修改為以下程式碼:
from django.urls import path from .views import * urlpatterns = [ path('', MessageList.as_view(), name='msg_list'), path('<int:pk>/', MessageView.as_view(), name='msg_view'), path('create/', MessageCreate.as_view(), name='msg_create'), path('<int:pk>/delete/', MessageDelete.as_view(), name='msg_delete'), ]
- 新增++第 8 行++,定義「刪除留言」的存取路徑
開啟 `web/views.py`,新增以下程式碼:
# 刪除留言 class MessageDelete(DeleteView): model = Message success_url = reverse_lazy('msg_list') # 刪除成功後,導向留言列表
新增刪除留言頁面範本 `web/templates/message_confirm_delete.html`:
{% extends "base.html" %} {% block content %} <h2>刪除記錄</h2> <p>您確定要刪除「{{ object }}」這筆記錄嗎?</p> <form action="" method="POST"> {% csrf_token %} <input type="submit" action="" value="是的,我要刪除" /> </form> {% endblock %}
- ++第 5 行++,通用視圖 `DeleteView` 會取得符合指定 `id` 的該筆紀錄,放在傳遞給頁面範本的變數 `object` 中。在這邊將這筆紀錄印出來,讓使用者確認。若要修改紀錄的代表字串,則需修改 `models.py` 相對應資料模型的 `__str__(self)` 方法。
開啟 `web/templates/web/message_list.html`,修改為以下程式碼:
{% extends "base.html" %} {% block content %} <ul> {% for msg in message_list %} <li> {{ msg.created }} <a href="{% url 'msg_view' msg.id %}">{{ msg.subject }}</a> {% if user.is_authenticated %} | <a href="{% url 'msg_delete' msg.id %}">刪除</a> {% endif %} </li> {% endfor %} </ul> {% endblock %}
- 新增++第 9 - 11 行++,若使用者已登入,則顯示刪除留言的連結
不過目前的寫法屬於「掩耳盜鈴」的保護法,只是在使用者未登入的狀態下,在頁面上不顯示刪除的連結。但是,若使用者知道刪除某篇留言的連結的路徑,直接在瀏覽器的網址列手動輸入網址,也是可以操作刪除留言的功能的。
正確的做法,除了不顯示連結之外,應該在視圖的處理類別檢查使用者是否已經登入了。
修改 `web/views.py`:
from django.shortcuts import render from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from .models import * from django.contrib.auth.mixins import LoginRequiredMixin
- 新增++第 5 行++,引用 `LoginRequiredMixin` 混成(Mixin)類別,這個類別的作用是自動檢查使用者是否已登入,如果還沒登入,會自動導向到登入頁面要求使用者登入。
# 刪除留言 class MessageDelete(LoginRequiredMixin, DeleteView): model = Message success_url = reverse_lazy('msg_list') # 刪除成功後,導向留言列表
- 修改++第 23 行++,在 `MessageDelete` 類別的繼承來源加上 `LoginRequiredMixin` 類別,並將其加在原本的 `DeleteView` 之前。