🚀 Day 9:使用者互動與表單提交(Form & POST)

5 天營隊第二週 (Day 9 / 總時數 6 Hours) | 告別靜態網頁!掌握 HTML 表單送出、CSRF 防範與資料庫動態 CRUD 互動!

📖 為什麼學 these?——「理財咖啡館開啟顧客點餐與撤單互動」關卡冒險故事線

💡 故事情境:讓使用者可以在網頁表單直接填寫記帳、一鍵刪除與搜尋!

過去兩天我們只能在網頁上「看」資料庫的明細。今天我們要讓使用者可以手動在網頁上輸入「品項、金額、分類」並按下【新增】!我們要學習 **HTTP POST 表單提交**、蓋上 **CSRF 防偽安全印章**、在網頁表格旁放上 **一鍵刪除垃圾桶**,並在新增成功後彈出綠色的提醒框,讓你的記帳網站達到真正商業級的互動流!

單元名稱 記帳程式關卡角色 為什麼這個單元不可或缺?(技術關聯)
單元 1:HTTP GET vs POST 📝 觀看菜單 vs 遞交點餐單 (<form>) 理解 GET(取得資料)與 POST(傳送機密修改)的差異,撰寫 HTML <form action="" method="POST">,對應 Day 1 的 input()
單元 2:CSRF 防護與接收資料 🛡️ 防偽安全印章 ({% csrf_token %}) 在表單加上 {% csrf_token %} 防止駭客跨站偽造點餐,並在 View 使用 request.POST.get() 擷取輸入內容。
單元 3:實作「新增消費」功能 🍳 寫入廚房帳本與自動回到首頁 將接收到的 POST 資料透過 ORM 寫入 SQLite(Expense.objects.create()),並用 redirect('home') 轉向首頁,對應 Day 5 的 handle_add()
單元 4:動態 URL 與刪除功能 🗑️ 撤銷點餐垃圾桶按鈕 (<int:id>) 設定動態 URL 參數(如 delete/<int:pk>/),在網頁表格旁實作垃圾桶按鈕,點擊立刻移除該筆資料,對應 Day 5 的 handle_delete()
單元 5:URL 查詢參數與篩選 🔎 分類快速檢索條 (?category=餐飲) 實作帶有 GET 查詢參數的下拉選單,結合 Django ORM .filter() 實現按分類即時篩選,對應 Day 5 的 handle_filter()
單元 6:表單防呆與 Flash 訊息 🔔 新增成功彈出式提醒框 (Messages) 防止空白表單提交,引入 django.contrib.messages,在新增/刪除成功後彈出綠色 Alert 提示,對應 Day 3 的 try-except 警示。

📌 課程前導與簡介說明

🎓 教師心法與教學策略 (Teacher Only)
  • 營隊定位:實現全套網頁 CRUD 操作。從傳統「唯讀 (Read-only)」網站跨越至「可互動 (Interactive)」動態 Web 應用。
  • 高中生心理亮點:當學生在網頁按下【新增消費】,網頁自動刷新並彈出「✅ 新增成功」綠色提示框,接著資料真的出現在下方表格時,會感到無比震撼!
  • 教學銜接點:GET vs POST (GET 像看菜單,POST 像下訂單);`{% csrf_token %}` 是 Django 預設的安全性門神,沒加必報 403 Forbidden 錯誤。
🌟 學生自主學習指引 (Student Self-Study)

歡迎來到 Day 9!今天我們要讓網站具備真正的「互動寫入與刪除能力」!請閱讀每個單元的 **故事單元說明**,點擊 **延伸閱讀** 展開深層觀念與 Google 搜尋,並善用每區塊的 **AI 協作除錯提示詞** 來練習撰寫表單與控制邏輯!

💻 Day 9 專案總成果:具備表單新增、垃圾桶刪除與分類搜尋的 Django 記帳網站

【教師參考解答】學生於 Day 9 結束時將完成的 URL、View 與 HTML 表單程式碼:

1. 路由對映 (`expenses/urls.py`):

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='home'),
    path('add/', views.add_expense, name='add_expense'),         # 新增消費路由
    path('delete/<int:pk>/', views.delete_expense, name='delete_expense'), # 動態刪除路由
]

2. 互動 View 視圖 (`expenses/views.py`):

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from .models import Expense
from django.db.models import Sum

def home(request):
    """ 首頁:支援分類過濾與總額統計 """
    category_query = request.GET.get('category', '').strip()
    
    if category_query:
        records = Expense.objects.filter(category=category_query).order_by('-date')
    else:
        records = Expense.objects.all().order_by('-date')
        
    total_cost = records.aggregate(Sum('price'))['price__sum'] or 0

    context = {
        'records': records,
        'total_cost': total_cost,
        'category_query': category_query,
    }
    return render(request, 'expenses/home.html', context)

def add_expense(request):
    """ 處理 POST 表單提交與新增消費 """
    if request.method == 'POST':
        item = request.POST.get('item', '').strip()
        price = request.POST.get('price', '').strip()
        category = request.POST.get('category', '').strip() or '一般'

        # 表單防呆驗證
        if not item or not price:
            messages.error(request, '❌ 請填寫完整的品項名稱與金額!')
            return redirect('home')

        try:
            Expense.objects.create(
                item=item,
                price=int(price),
                category=category
            )
            messages.success(request, f'✅ 成功新增消費:{item} ${price} 元!')
        except ValueError:
            messages.error(request, '❌ 金額必須為純數字!')

        return redirect('home')
    return redirect('home')

def delete_expense(request, pk):
    """ 依 ID 刪除特定消費紀錄 """
    expense = get_object_or_404(Expense, pk=pk)
    item_name = expense.item
    expense.delete()
    messages.warning(request, f'🗑️ 已刪除紀錄:{item_name}')
    return redirect('home')

3. 首頁 UI 與表單模板 (`templates/expenses/home.html`):

{% extends 'base.html' %}

{% block content %}
<!-- Flash 提醒訊息彈窗 -->
{% if messages %}
    {% for message in messages %}
        <div class="alert alert-{{ message.tags }} alert-dismissible fade show shadow-sm" role="alert">
            {{ message }}
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
        </div>
    {% endfor %}
{% endif %}

<!-- 新增消費表單卡片 -->
<div class="card shadow-sm border-0 mb-4">
    <div class="card-header bg-primary text-white fw-bold">➕ 新增消費紀錄</div>
    <div class="card-body">
        <form action="{% url 'add_expense' %}" method="POST" class="row g-3">
            {% csrf_token %} <!-- 🛡️ CSRF 安全防偽印章 -->
            <div class="col-md-4">
                <input type="text" name="item" class="form-control" placeholder="品項名稱 (例如: 珍珠奶茶)" required>
            </div>
            <div class="col-md-3">
                <input type="number" name="price" class="form-control" placeholder="金額 (TWD)" required>
            </div>
            <div class="col-md-3">
                <input type="text" name="category" class="form-control" placeholder="分類 (例如: 餐飲)">
            </div>
            <div class="col-md-2">
                <button type="submit" class="btn class-btn btn-success w-100 fw-bold">新增記帳</button>
            </div>
        </form>
    </div>
</div>

<!-- 分類搜尋過濾列與明細表格 -->
<div class="card shadow-sm border-0">
    <div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
        <span class="fw-bold">📊 記帳明細列表</span>
        <form method="GET" action="{% url 'home' %}" class="d-flex gap-2">
            <input type="text" name="category" class="form-control form-control-sm" placeholder="搜尋分類..." value="{{ category_query }}">
            <button type="submit" class="btn btn-sm btn-outline-primary">搜尋</button>
            {% if category_query %}
                <a href="{% url 'home' %}" class="btn btn-sm btn-outline-secondary">清除</a>
            {% endif %}
        </form>
    </div>
    <div class="card-body p-0">
        <table class="table table-hover align-middle mb-0">
            <thead class="table-light">
                <tr>
                    <th>#</th>
                    <th>日期</th>
                    <th>品項名稱</th>
                    <th>分類</th>
                    <th>金額</th>
                    <th class="text-center">操作</th>
                </tr>
            </thead>
            <tbody>
                {% for r in records %}
                <tr>
                    <td>{{ forloop.counter }}</td>
                    <td>{{ r.date|date:"Y-m-d" }}</td>
                    <td class="fw-bold">{{ r.item }}</td>
                    <td><span class="badge bg-secondary">{{ r.category }}</span></td>
                    <td class="text-success fw-bold">${{ r.price }}</td>
                    <td class="text-center">
                        <!-- 動態 URL 垃圾桶刪除按鈕 -->
                        <a href="{% url 'delete_expense' r.pk %}" class="btn btn-sm btn-outline-danger" onclick="return confirm('確定要刪除 [{{ r.item }}] 嗎?');">
                            🗑️ 刪除
                        </a>
                    </td>
                </tr>
                {% empty %}
                <tr>
                    <td colspan="6" class="text-center py-4 text-muted">📭 目前尚無任何記帳紀錄!</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
</div>
{% endblock %}

【學生自主實作框架】請根據今日各單元所學,將空缺的 `____` 填入正確的 Django Form & POST 關鍵字:

<!-- 🎯 Day 9 自主挑戰:請填入 HTML 表單與 View 邏輯空缺的 ____ 關鍵字! -->

<!-- --- [檔案: templates/expenses/home.html] --- -->
<form action="{% url 'add_expense' %}" method="____"> # 提示:提交機密修改表單的 HTTP 方法 POST
    {% ____ %} # 提示:必填的 CSRF 安全防偽印章標籤 csrf_token
    
    <input type="text" name="item" placeholder="品項名稱">
    <input type="number" name="price" placeholder="金額">
    <button type="submit">送出</button>
</form>

# --- [檔案: expenses/views.py] ---
def add_expense(request):
    if request.method == '____': # 提示:檢查請求是否為 POST
        item_input = request.POST.____('item') # 提示:從 request.POST 字典取出欄位值的方法 get
        price_input = request.POST.get('price')
        
        Expense.objects.____(item=item_input, price=price_input) # 提示:ORM 創建資料的方法 create
        return ____('home') # 提示:新增成功後重導向回首頁的函式 redirect

⏰ 單元 1:HTTP GET vs POST(09:00 - 10:00)

📝 故事單元說明:觀看菜單 vs 遞交點餐單 (<form>)

在網路世界上,讀取資料與寫入資料有著嚴格的分工:
• **HTTP GET**:像是「看菜單」,只用來讀取網頁(參數會裸露在網址列中,如 ?category=餐飲)。
• **HTTP POST**:像是「把點餐單遞給廚房」,用來傳送修改或新增機密資料(資料封裝在 Request Body 中不外露)。
我們會使用 HTML <form method="POST"> 來讓使用者填寫記帳資料!對應的就是 Day 1 的 input()

💡 深度觀念:GET 與 POST 的安全性與冪等性 (Idempotency)

網頁通訊協定對 HTTP 動詞的規範:

  • GET 請求:安全 (Safe) 且冪等 (Idempotent),多次請求不影響伺服器狀態。資料長度受限於 URL 長度限制。
  • POST 請求:非冪等 (Non-idempotent),會改變伺服器狀態(如新增資料庫紀錄)。資料放在 Body 中,長度無限制。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請向初學者詳細說明 HTTP GET 與 POST 的本質差異,並舉例說明為什麼查詢網頁要用 GET,而提交表單新增資料一定要用 POST。

🎓 教師備課手冊 (Teacher Only)
  • 講解 GET (獲取/無副作用) 與 POST (提交/改變伺服器狀態)。
  • 撰寫 HTML <form action="..." method="POST"> 標籤結構。
  • 概念連結:對應 Day 1 的 input() 鍵盤輸入與資料收集。
💻 HTML 程式碼段落 (表單結構)
<!-- HTML POST 表單基本結構 -->
<form action="{% url 'add_expense' %}" method="POST">
    <input type="text" name="item" placeholder="品項名稱">
    <input type="number" name="price" placeholder="金額">
    <button type="submit">新增</button>
</form>
⚠️ 常見陷阱與問題案例
  • input 漏寫 name 屬性:表單寫了 <input type="text"> 但漏寫 name="item",導致 View 端 request.POST.get('item') 完全抓不到資料!
🤖 單元 1 常見問題 AI 協作除錯提示詞

我在 View 端使用 request.POST.get('item') 時總是抓到 None。請幫我檢查 HTML 標籤中是否漏寫了 name="item" 屬性。

⏰ 單元 2:CSRF 防護與接收資料(10:00 - 11:00)

🛡️ 故事單元說明:防偽安全印章 ({% csrf_token %})

如果沒有防護,駭客可以偷偷複製你的點餐單,假冒你的名義偷偷幫你下單(這稱為 **跨站請求偽造 CSRF 攻擊**)。Django 極度重視網路安全,強制規定所有 POST 表單內部必須加上魔法印章 {% csrf_token %}!沒有這枚印章,Django 會毫不留情地拋出 **403 Forbidden** 阻擋攻擊!而在 View 端,我們用 request.POST.get('item') 就能安全解開顧客填寫的內容。

💡 深度觀念:什麼是 CSRF (Cross-Site Request Forgery)?

CSRF 是一種常見的 Web 網路攻擊手法:

  • 攻擊原理:使用者登入 A 網站後,誘導點擊惡意 B 網站,B 網站利用瀏覽器的 Cookie 憑證發送偽造請求至 A 網站。
  • {% csrf_token %} 防禦:Django 會為每個使用者會話生成一個的一次性加密令牌 (Token) 塞在表單中,驗證成功才允許寫入。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請向初學者解釋什麼是 CSRF (跨站請求偽造) 攻擊,以及 Django 表單中為什麼必須寫 {% csrf_token %},沒寫時出現的 403 Forbidden 代表什麼意思。

🎓 教師備課手冊 (Teacher Only)
  • 在表單中加入 {% csrf_token %} 標籤。
  • 在 View 寫 item = request.POST.get('item') 擷取提交資料。
  • 故意帶學生不寫 {% csrf_token %},觀察 Django 彈出的 403 Forbidden 畫面。
💻 HTML 碼與 Python View 段落
<!-- HTML 表單內必加安全印章 -->
<form method="POST">
    {% csrf_token %}
    <input type="text" name="item">
</form>

# View 擷取資料邏輯
def add_expense(request):
    if request.method == 'POST':
        item = request.POST.get('item') # 抓取 name="item" 的輸入值
        price = request.POST.get('price')
⚠️ 常見陷阱與問題案例
  • 403 Forbidden 錯誤:送出 POST 表單時跳出 `CSRF verification failed. Request aborted.` $\rightarrow$ 忘記在 `
    ` 內部加上 `{% csrf_token %}`!
🤖 單元 2 常見問題 AI 協作除錯提示詞

我在送出 POST 表單時跳出了 403 Forbidden (CSRF verification failed)。請提醒我如何在 HTML 內部補上 {% csrf_token %}。

⏰ 單元 3:實作「新增消費」功能(11:00 - 12:00)

🍳 故事單元說明:寫入廚房帳本與自動回到首頁

當我們安全拿到表單資料後,主廚 View 會呼叫 ORM 的 Expense.objects.create(item=item, price=price) 將資料寫入 SQLite 保險箱!寫入成功後,我們使用 return redirect('home') 讓使用者的瀏覽器自動跳轉回首頁,看到最新加進去的那筆開銷!這完全對應 Day 5 的 handle_add() 邏輯!

💡 深度觀念:為什麼新增完資料必須 `redirect()`?(PRG 模式)

在 Web 開發中,處理 POST 請求後最佳實踐是重導向 (Redirect):

  • Post/Redirect/Get (PRG) 模式:若 POST 成功後直接 render() 回傳網頁,使用者按重新整理 (F5) 會重複觸發表單提交(重複扣款/重複新增)。
  • redirect('home'):回傳 302 重導向,讓瀏覽器發起新的 GET 請求載入首頁,解決重複提交問題。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請向初學者解釋什麼是 PRG (Post/Redirect/Get) 模式,以及為什麼 Django 表單新增成功後必須用 redirect('home') 而不能直接 render()。

🎓 教師備課手冊 (Teacher Only)
  • 使用 Expense.objects.create(...) 寫入 SQLite。
  • 匯入 redirect 模組,傳回 return redirect('home')
  • 概念連結:對應 Day 5 handle_add() 的新增與存檔邏輯。
💻 Python View 段落 (`expenses/views.py`)
from django.shortcuts import render, redirect
from .models import Expense

def add_expense(request):
    if request.method == 'POST':
        item = request.POST.get('item')
        price = request.POST.get('price')
        
        # 寫入資料庫
        Expense.objects.create(item=item, price=int(price))
        
        # 重導向回首頁 (PRG 模式)
        return redirect('home')
    return redirect('home')
⚠️ 常見陷阱與問題案例
  • 重刷頁面重複新增:沒有寫 `redirect()` 而是寫 `render()`,導致使用者在瀏覽器按 F5 重刷時一直跳出「是否重複送出表單」。
🤖 單元 3 常見問題 AI 協作除錯提示詞

我在表單新增成功後重新整理網頁會重複寫入相同的資料。請幫我把 return render(...) 改為 return redirect('home')。

⏰ 單元 4:動態 URL 與刪除功能(13:00 - 14:00)

🗑️ 故事單元說明:撤銷點餐垃圾桶按鈕 (<int:id>)

要刪除特定的一筆資料,我們必須知道它的身份 ID!我們在 urls.py 使用動態網址參數 path('delete/<int:pk>/', ...)。當使用者點擊網頁表格旁邊的【🗑️ 刪除】按鈕時,網址會帶著這筆資料的 Primary Key (如 /delete/5/)。View 收到 pk=5 後,執行 Expense.objects.get(pk=5).delete() 就能精準刪除它!對應 Day 5 的 handle_delete(index)

💡 深度觀念:網址動態參數與 `get_object_or_404`

Django 動態路由與安全的物件擷取:

  • Path Converter <int:pk>:匹配整數並自動轉換為 Python `int` 變數傳入 View 的參數中。
  • get_object_or_404(Model, pk=pk):若指定的 ID 在資料庫中找不到,自動傳回漂亮的 404 Not Found 頁面,避免拋出 500 內部崩潰。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請說明 Django 動態路由 path('delete/<int:pk>/', ...) 的運作原理,並示範如何在 HTML 中寫出 {% url 'delete_expense' r.pk %} 連結以及在 View 使用 get_object_or_404() 進行安全刪除。

🎓 教師備課手冊 (Teacher Only)
  • urls.py 定義 path('delete/<int:pk>/', ...)
  • 在 View 使用 get_object_or_404(Expense, pk=pk) 並呼叫 .delete()
  • 在 HTML 表格旁加入帶參數的 URL 標籤 {% url 'delete_expense' r.pk %}
  • 概念連結:對應 Day 5 handle_delete(index) 依 ID 刪除。
💻 刪除 View 與 HTML 刪除按鈕段落
# 1. View 端刪除邏輯
def delete_expense(request, pk):
    expense = get_object_or_404(Expense, pk=pk)
    expense.delete() # 執行 ORM 刪除
    return redirect('home')

<!-- 2. HTML 端超連結帶入 pk -->
<a href="{% url 'delete_expense' r.pk %}" class="btn btn-danger btn-sm">🗑️ 刪除</a>
⚠️ 常見陷阱與問題案例
  • NoReverseMatch 錯誤:在 HTML 使用 `{% url 'delete_expense' %}` 時忘記帶入變數參數 `r.pk` $\rightarrow$ 報錯 `NoReverseMatch at / : Reverse for 'delete_expense' with no arguments not found.`!
🤖 單元 4 常見問題 AI 協作除錯提示詞

我在 HTML 寫 {% url 'delete_expense' %} 時跳出了 NoReverseMatch 錯誤。請提醒我如何在 URL 標籤末端補上 ID 變數 (例如 r.pk)。

⏰ 單元 5:URL 查詢參數與篩選(14:00 - 15:00)

🔎 故事單元說明:分類快速檢索條 (?category=餐飲)

當顧客想查「這個月花多少錢吃餐飲?」時,我們在網頁上方放一個 GET 搜尋表單。當點擊【搜尋】時,網址會帶有 Query Parameter(如 http://127.0.0.1:8000/?category=餐飲)。View 主廚用 request.GET.get('category') 抓出這個字串,並呼叫 Expense.objects.filter(category=cat) 進行精準過濾!對應 Day 5 的 handle_filter()

💡 深度觀念:URL 查詢字串 (Query String) 與 ORM Filter

GET 查詢參數常用於搜尋、過濾與分頁功能:

  • request.GET.get('key'):取出網址 ?key=value 的參數值。
  • 進階 ORM 模糊查詢:使用 item__icontains=keyword 可實現不分大小寫的關鍵字模糊搜尋。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請說明如何使用 HTML <form method="GET"> 傳送網址參數,並示範在 Django View 中用 request.GET.get() 搭配 Expense.objects.filter() 完成搜尋過濾。

🎓 教師備課手冊 (Teacher Only)
  • 在 View 寫 cat = request.GET.get('category')
  • 若有值則執行 Expense.objects.filter(category=cat)
  • 概念連結:對應 Day 5 handle_filter() 的分類搜尋。
💻 Python View 與 GET 表單段落
# View 端處理 GET 查詢參數
def home(request):
    cat_query = request.GET.get('category', '').strip()
    if cat_query:
        records = Expense.objects.filter(category=cat_query)
    else:
        records = Expense.objects.all()
    return render(request, 'expenses/home.html', {'records': records})
⚠️ 常見陷阱與問題案例
  • 搜尋表單寫成 method="POST":搜尋過濾屬於讀取操作,必須使用 `method="GET"`,參數才會附在 URL 後方。
🤖 單元 5 常見問題 AI 協作除錯提示詞

我的搜尋表單按送出後網址沒有出現 ?category=餐飲 的參數。請幫我檢查 <form> 的 method 屬性是否誤寫成了 POST 而不是 GET。

⏰ 單元 6:表單防呆與 Flash 訊息(15:00 - 16:00)

🔔 故事單元說明:新增成功提醒快訊 (Messages)

當使用者在表單不小心把金額空著、或是輸入英文數字時,程式不應該白屏當機!我們可以在 View 加入防呆判斷,並引入 Django 內建的 **Flash Messages 框架**!當新增或刪除成功時,呼叫 messages.success(request, '新增成功!'),網頁畫面上就會彈出超漂亮的綠色 Alert 提醒視窗!對應 Day 3 的 try-except 防呆提示!

💡 深度觀念:Django 訊息框架 (Messages Framework)

Django 利用 Session 在跨重導向頁面間傳遞一次性訊息 (Flash Message):

  • 常用訊息等級messages.success() (綠色成功)、messages.error() (紅色錯誤)、messages.warning() (黃色警告)。
  • HTML 渲染 {% if messages %}:訊息顯示一次後會自動從 Session 中清除,不會重複彈出。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請說明如何使用 django.contrib.messages 在 View 中傳送一次性成功或錯誤訊息,並示範如何在 HTML 中用 Bootstrap 5 Alert 彈窗印出 messages。

🎓 教師備課手冊 (Teacher Only)
  • 匯入 from django.contrib import messages
  • 在 View 防呆判斷時呼叫 messages.error()messages.success()
  • 在 HTML 頂部加上 {% if messages %} 走訪印出 Bootstrap Alert 視窗。
  • 概念連結:對應 Day 3 的 try-except 與防呆警示提示。
💻 View 訊息發送與 HTML Alert 彈窗
# 1. View 端發送一次性快訊
from django.contrib import messages

def add_expense(request):
    if request.method == 'POST':
        item = request.POST.get('item')
        if not item:
            messages.error(request, '❌ 品項名稱不能為空!')
            return redirect('home')
        
        messages.success(request, '✅ 成功新增一筆消費!')
        return redirect('home')

<!-- 2. HTML 頂部顯示 Messages -->
{% if messages %}
    {% for message in messages %}
        <div class="alert alert-{{ message.tags }}">
            {{ message }}
        </div>
    {% endfor %}
{% endif %}
⚠️ 常見陷阱與問題案例
  • HTML 未走訪 messages:在 View 寫了 `messages.success()`,但 HTML 忘記寫 `{% if messages %}` 迴圈,導致訊息完全沒彈出來!
🤖 單元 6 常見問題 AI 協作除錯提示詞

我在 View 呼叫了 messages.success() 但網頁上沒有跳出提示框。請幫我檢查 HTML 模板中是否漏寫了 {% if messages %} ... {% endif %} 區塊。