本文為 Django 會員系統建立教學,第 5 篇:
- 設計登入與註冊功能的基礎路由與頁面配置
- Django 使用者密碼加密方式詳解
- 使用 Django 內建功能實現使用者註冊與登入
- 使用 Django 實現安全登出功能:完整指南
- Django 使用者登入與資料操作的最佳實踐 👈 所在位置
- Django: 添加公開留言與履歷列表功能
- 使用 Alpine.js 和 Django 動態管理留言按鈕啟用狀態
- Django 收藏功能的實現:使用 ManyToMany 關係與自定義中介模型
建議閱讀本文前,先閱讀完 Django 與前端框架教學 系列文
本篇教學將詳細介紹如何在 Django 專案中實現與使用者關聯的資料操作,包括:
- 加入登入限制的裝飾器。
- 透過表單處理資料的新增與修改。
- 解決使用者 ID 無法傳遞的問題。
- 安全地限制用戶僅能操作自己的資料。
登入限制與裝飾器的應用
為確保只有登入的使用者可以訪問相關頁面,我們需要使用 @login_required 裝飾器。
新增 @login_required 到視圖函數
以下為一個新增履歷的視圖函數範例:
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from django.contrib import messages
@login_required
def index(request):
if request.POST:
form = ResumeForm(request.POST)
form.save()
messages.success(request, "新增成功")
return redirect("resumes:index")
resumes = Resume.objects.all()
return render(request, "resumes/index.html", {"resumes": resumes})
@login_required 的功能
- 確保只有已登入的使用者才能訪問某些特定的視圖(View)。
- 如果未登入的使用者嘗試訪問這些受保護的視圖,Django 會自動將他們重定向到指定的登入頁面。
- 這是一個裝飾器,通常用在需要保護的視圖函數上,例如:
from django.contrib.auth.decorators import login_required
@login_required
def profile(request):
return render(request, "users/profile.html")當使用者訪問 /profile 頁面時,@login_required 會檢查該使用者是否已登入:
- 若已登入:執行視圖函數,返回正常的頁面響應。
- 若未登入:重定向至登入頁面,並附帶查詢參數
?next=/profile,告訴系統完成登入後應跳轉回/profile。
LOGIN_URL 的設定
在 settings.py 中,LOGIN_URL 指定未登入使用者被引導到的登入頁面路徑:
LOGIN_URL = "/users/login"作用
- 設置全局重定向路徑:未登入的使用者訪問任何被
@login_required保護的視圖時,Django 會將他們重定向到該路徑。 - 提高可維護性:集中管理登入頁面路徑,若登入頁面改名或遷移,只需更新這一處設置。
對應的 settings.py 示例:
# settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"users", # 假設有用戶相關的應用
"resumes", # 假設有履歷相關的應用
]
# 指定未登入使用者重定向到的登入頁面
LOGIN_URL = "/users/login"
# 靜態檔案的路徑配置(如需其他設置)
STATIC_URL = "/static/"資料操作與 request.user 的結合
問題背景
在許多多用戶應用中,不同的使用者會有各自的數據。例如:
- 使用者 A 和使用者 B 可以分別管理各自的履歷,數據之間互不干擾。
- 若沒有正確綁定履歷與當前使用者,所有履歷都會出現在同一列表中,導致數據混亂,甚至出現權限洩漏的風險。
當新增履歷時,應將履歷與當前使用者進行綁定,自動將當前使用者的 ID 傳入資料庫,可以改用以下方式:
更新模型結構
新增 Resume 模型欄位
為了支援使用者與履歷的關聯,我們需要在模型中加入 user 外鍵:
檔案位置:models.py
from django.db import models
from django.contrib.auth.models import User
class Resume(models.Model):
title = models.CharField(max_length=100)
skill = models.CharField(max_length=300, null=True)
location = models.CharField(max_length=200, null=True)
content = models.TextField(null=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
def __str__(self):
return f"{self.title} ({self.skill})"代碼解析
user外鍵:關聯到 Django 內建的User模型,表示每份簡歷屬於某個用戶。on_delete=models.CASCADE:當用戶被刪除時,相關的簡歷資料將一併刪除。null=True:允許初始資料的user欄位為空,避免遷移錯誤。
執行資料遷移
在模型修改完成後,執行以下命令生成並應用遷移:
python manage.py makemigrations
python manage.py migrate注意
若執行遷移時出現初始資料有空值的警告,可調整 user 欄位設為 null=True,如上所示,然後重新執行遷移。
建立關聯:手動設置(方法一)
透過 commit=False,我們可以暫存表單資料並手動為 user 字段賦值:
@login_required
def index(request):
if request.POST:
form = ResumeForm(request.POST)
resume = form.save(commit=False) # 保存數據,但不立即提交到資料庫
resume.user = request.user # 綁定當前使用者
resume.save() # 提交數據到資料庫
messages.success(request, "新增成功")
return redirect("resumes:index")
resumes = Resume.objects.all()
return render(request, "resumes/index.html", {"resumes": resumes})
流程解釋:
- 登入檢查:
@login_required裝飾器確保只有已登入的使用者可以進入這個視圖。如果未登入,會自動重定向到設定的登入頁面。 - 表單提交(POST 請求):
if request.POST:判斷是否有表單數據提交。form = ResumeForm(request.POST)將提交的數據綁定到ResumeForm表單。resume = form.save(commit=False)使用commit=False表示先不將數據寫入資料庫,這樣我們可以進一步操作數據(如關聯當前使用者)。
- 綁定當前使用者:
resume.user = request.user綁定resume的user字段,將當前登入的使用者(request.user)設為該履歷的擁有者。這是將履歷與使用者綁定的關鍵步驟。 - 保存數據:
resume.save()這會將履歷數據保存到資料庫,包括綁定的使用者信息。 - 成功訊息與重定向: 使用
messages.success(request, "新增成功")顯示成功訊息,並將使用者重定向到履歷列表頁面。 - 顯示履歷列表: 如果沒有提交表單,則展示所有履歷數據。
這段代碼的主要目的是將當前登入的使用者與新增的履歷關聯,然後保存數據。
建立關聯:使用 instance 預設值(方法二)
我們可以在初始化表單時使用 instance 設定 user:
@login_required
def index(request):
if request.POST:
form = ResumeForm(
request.POST,
instance=Resume(user=request.user) # 預設將 user 設為當前使用者
)
form.save()
messages.success(request, "新增成功")
return redirect("resumes:index")
resumes = Resume.objects.all()
return render(request, "resumes/index.html", {"resumes": resumes})
登入檢查: 同樣使用 @login_required 裝飾器來確保該視圖只有在使用者登入後才能訪問。
表單提交(POST 請求):
if request.POST:判斷是否有表單數據提交。form = ResumeForm(request.POST, instance=Resume(user=request.user))這裡的instance=Resume(user=request.user)表示在初始化表單時,將user字段預設為當前登入的使用者。這樣即使表單沒有明確填寫user字段,它也會自動從當前使用者賦值。
保存數據: 使用 form.save() 保存表單數據,這會自動將所有字段(包括 user)保存到資料庫中。
成功訊息與重定向: 顯示成功訊息 messages.success(request, "新增成功") 並重定向到履歷列表頁面。
顯示履歷列表: 如果沒有提交表單,則展示所有履歷數據。
補充討論:
在以下表單定義中,fields 只包含四個字段(title、skill、location、content),但我們在保存時卻能插入一個額外的 user 字段,這是為什麼?
from django.forms import ModelForm
from .models import Resume
class ResumeForm(ModelForm):
class Meta:
model = Resume
fields = ["title", "skill", "location", "content"]
解答與詳解:
1. fields 的作用
- 定義表單字段:
fields是為了告訴 Django 在生成的表單中,哪些模型字段應該被顯示並供用戶填寫。 - 不影響模型的完整性:
fields不會限制模型字段的最終值,只是決定表單需要處理的字段。- 在上述例子中,
fields只包括title、skill、location、content,但模型仍然完整地包含所有字段(如user)。 - 未列入
fields的字段仍可以通過程式邏輯手動設置。
- 在上述例子中,
2. 如何插入未包含在 fields 的字段
- 手動設置:
- 即使
user不在fields中,我們仍然可以在保存表單時手動設置它:
- 即使
resume = form.save(commit=False) # 暫不保存到數據庫
resume.user = request.user # 手動設置 user 字段
resume.save() # 最後保存到數據庫- 自動設置(使用
instance預設值):- 我們可以在初始化表單時,通過
instance預設字段值,減少手動設置:
- 我們可以在初始化表單時,通過
form = ResumeForm(
request.POST,
instance=Resume(user=request.user) # 預先設置 user 字段
)
form.save() # 表單保存時會自動包括 user3. 使用 instance 預設值的詳解
- 作用:
instance參數允許我們在初始化表單時提供一個基礎對象。- 在這裡,
instance=Resume(user=request.user)會創建一個Resume對象,並預設user字段為當前用戶。 - 表單只處理
fields中列出的字段,但保存時會保留user字段的值。
- 在這裡,
- 好處:
- 簡潔性:避免手動設置字段(如
resume.user = request.user),邏輯集中處理。 - 自動化:預設值會自動被表單保存,減少額外的操作。
- 簡潔性:避免手動設置字段(如
- 注意事項:
- 模型約束:
- 如果模型的其他字段設有約束條件(如
blank=False或null=False),未設置值時會導致錯誤。 - 在初始化時,應確保這些字段能正確被賦值。
- 如果模型的其他字段設有約束條件(如
- 數據庫唯一性:
- 如果模型中的字段設置了
unique=True(如user),需注意避免初始化值違反唯一性約束。
- 如果模型中的字段設置了
- 模型約束:
4. 手動設置 vs 自動設置
- 手動設置:
- 適合需要額外控制保存邏輯的場景。
form = ResumeForm(request.POST)
resume = form.save(commit=False)
resume.user = request.user # 手動設置 user 字段
resume.save()- 自動設置(使用
instance):- 適合邏輯簡單、字段初始化時能確保正確的場景。
form = ResumeForm(
request.POST,
instance=Resume(user=request.user) # 初始化時設置 user
)
form.save()關於「四個欄位先預帶,不一定只有四個」
- 解釋:
- 表單中的
fields只決定用戶在前端可以填寫的字段,但保存的對象是完整的模型。 - 因此,除了
fields中的字段,其他字段(如user)可以通過程式邏輯或預設值進行設置。
- 表單中的
小結
fields是控制表單顯示的字段,不是對模型字段的限制。- 未列入
fields的字段可以在後端手動設置或通過instance預設值處理。 - 使用
instance可以讓代碼更簡潔,但需注意模型的約束條件。 - 選擇手動設置或自動設置需根據具體業務邏輯來決定。
安全限制:僅操作自己的資料
限制列表顯示
在視圖函數中,篩選出僅屬於當前使用者的資料:
@login_required
def index(request):
resumes = Resume.objects.filter(user=request.user).order_by("-id") # 僅顯示當前用戶的資料
return render(request, "resumes/index.html", {"resumes": resumes})
修改、刪除僅針對自己的資料
修改和刪除操作同樣需要確保僅影響到當前使用者的資料:
修改範例
@login_required
def edit(request, id):
resume = get_object_or_404(Resume, id=id, user=request.user) # 限制只能操作自己的資料
form = ResumeForm(instance=resume)
return render(
request,
"resumes/edit.html",
{"resume": resume, "form": form},
)
刪除範例
@login_required
def delete(request, id):
resume = get_object_or_404(Resume, id=id, user=request.user) # 限制只能刪除自己的資料
if request.POST:
resume.delete()
messages.success(request, "刪除成功")
return redirect("resumes:index")
return render(request, "resumes/delete.html", {"resume": resume})
小結
@login_required:保護資料操作,僅允許登入用戶訪問。- 表單數據處理:使用
commit=False或instance靈活設置額外欄位。 - 安全性:在視圖中篩選資料,確保用戶僅能操作自己的內容。
希望本篇教學能幫助新手更清楚地理解 Django 中與用戶資料相關的最佳實踐!