Day 13 已經讓首頁開始讀取 MySQL 裡真正的任務。
目前已經可以:
新增任務
↓
存進 MySQL
↓
首頁讀取
↓
顯示真正資料
但如果任務內容打錯,還沒有辦法修改。
像是:
任務名稱打錯
說明想補內容
狀態要改
分類選錯
截止日期要調整
所以 Day 14 要補上 CRUD 裡的:
Update
也就是「編輯任務」。
Day 14 只做:
首頁點「編輯」
↓
進入編輯頁
↓
帶入原本資料
↓
修改內容
↓
送出表單
↓
更新 MySQL
↓
回首頁
今天先不做:
刪除任務
篩選
快速切換狀態
分類 CRUD
services/
routes/
Claude API
我先把需求交給 Claude Code:
目前狀態:
- Flask 已成功連上 MySQL
- Task / Category Model 已建立
- /tasks/new 可以新增任務
- 首頁已經從 MySQL 讀取真正的 tasks
- index.html 使用 Jinja2 顯示任務列表
今天只做「編輯既有任務」。
需求:
1. 首頁每張任務卡片增加「編輯」入口
2. 新增編輯任務頁面
3. 編輯頁面要先顯示原本資料
4. 可以修改:
- 任務名稱
- 說明
- 狀態
- 分類
- 截止日期
5. GET 負責顯示原本資料
6. POST 負責更新 Task
7. 更新成功後 redirect 回首頁
8. 找不到 task_id 時回傳 404
今天不要:
- 刪除任務
- 篩選
- 快速切換狀態
- 分類 CRUD
- services/
- routes/
- Claude API
請先規劃會新增或修改哪些檔案,
不要直接修改程式。
最後規劃是:
新增:
templates/edit_task.html
修改:
app.py
templates/index.html
static/css/style.css
沒有動資料庫結構。
Day 14 新增:
@app.route("/tasks/<int:task_id>/edit", methods=["GET", "POST"])
def edit_task(task_id):
task = Task.query.get_or_404(task_id)
if request.method == "POST":
category = Category.query.filter_by(
name=request.form.get("category")
).first()
due_date_text = request.form.get("due_date", "")
due_date = (
datetime.strptime(due_date_text, "%Y-%m-%d").date()
if due_date_text
else None
)
task.title = request.form.get("title", "")
task.description = request.form.get("description", "")
task.status = request.form.get("status", "todo")
task.category_id = category.id if category else None
task.due_date = due_date
db.session.commit()
return redirect(url_for("index"))
return render_template("edit_task.html", task=task)
這次的網址會像:
/tasks/3/edit
其中:
3
就是任務的 id。
task_id 是怎麼進來的?Route:
@app.route("/tasks/<int:task_id>/edit")
裡面的:
<int:task_id>
代表 Flask 會把網址中的數字取出來。
例如:
/tasks/3/edit
會變成:
task_id = 3
接著:
task = Task.query.get_or_404(task_id)
就會去資料庫找:
id = 3
的那筆 Task。
如果不存在,就直接回:
404 Not Found
進入:
/tasks/3/edit
第一次是 GET。
這時候不是更新資料,而是把原本的 Task 傳到:
edit_task.html
例如原本:
任務名稱:測試1
狀態:進行中
分類:生活
截止日期:2026/10/01
編輯頁打開後就會先看到這些內容。
<input
type="text"
id="title"
name="title"
value="{{ task.title }}"
required
>
所以使用者不用重新輸入整份內容,只需要改要修改的部分。
原本一開始是:
<textarea>{{ task.description }}</textarea>
但如果資料庫裡:
description = NULL
頁面可能會顯示:
None
所以後來修成:
<textarea id="description" name="description" rows="4">{{ task.description or "" }}</textarea>
意思是:
有 description
→ 顯示原內容
沒有 description
→ 顯示空白
這樣比較合理。
例如原本:
status = in_progress
編輯頁打開時,「進行中」要直接被選中。
<option value="todo"
{% if task.status == 'todo' %}selected{% endif %}>
待處理
</option>
<option value="in_progress"
{% if task.status == 'in_progress' %}selected{% endif %}>
進行中
</option>
<option value="done"
{% if task.status == 'done' %}selected{% endif %}>
已完成
</option>
一開始編輯頁只有:
學習
專案
生活
但資料庫允許:
category_id = NULL
如果一筆任務原本沒有分類,瀏覽器會自動選第一個:
學習
這樣即使使用者完全沒改分類,送出後也可能被意外改成「學習」。
所以後來補上:
<option value=""
{% if not task.category %}selected{% endif %}>
未分類
</option>
完整概念:
<select id="category" name="category">
<option value="" {% if not task.category %}selected{% endif %}>
未分類
</option>
<option value="學習"
{% if task.category and task.category.name == '學習' %}selected{% endif %}>
學習
</option>
<option value="專案"
{% if task.category and task.category.name == '專案' %}selected{% endif %}>
專案
</option>
<option value="生活"
{% if task.category and task.category.name == '生活' %}selected{% endif %}>
生活
</option>
</select>
如果送出:
category = ""
這段:
Category.query.filter_by(
name=request.form.get("category")
).first()
查不到分類,就會得到:
None
最後:
task.category_id = category.id if category else None
就會正確存成:
NULL
HTML:
<input type="date">
需要:
YYYY-MM-DD
所以使用:
<input
type="date"
id="due_date"
name="due_date"
value="{{ task.due_date.strftime('%Y-%m-%d') if task.due_date else '' }}"
>
如果原本沒有日期,就顯示空白。
這次跟 Day 12 最大的不同,是沒有重新:
Task(...)
Day 12 是:
建立新的 Task
→ INSERT
Day 14 則是直接修改原本查到的:
task
例如:
task.title = request.form.get("title", "")
task.description = request.form.get("description", "")
task.status = request.form.get("status", "todo")
task.category_id = category.id if category else None
task.due_date = due_date
最後:
db.session.commit()
SQLAlchemy 就會把同一筆資料更新回 MySQL。
因為:
task = Task.query.get_or_404(task_id)
查出來的 task 本來就是 SQLAlchemy 正在管理的資料。
所以只要修改欄位後:
db.session.commit()
就可以。
不需要:
db.session.add(task)
Day 13 的首頁已經是 Jinja2 迴圈。
Day 14 在每張任務卡片加:
<a
class="edit-link"
href="{{ url_for('edit_task', task_id=task.id) }}">
編輯
</a>

例如:
task.id = 3
連結就會前往:
/tasks/3/edit
這次只加一個很小的樣式:
.edit-link {
display: inline-block;
margin-top: 6px;
color: var(--text-secondary);
font-size: 0.85rem;
font-weight: 600;
text-decoration: none;
}
.edit-link:hover {
color: var(--primary);
text-decoration: underline;
}
沒有重新改首頁版面,只是讓「編輯」比較容易辨識。
我直接進入:
127.0.0.1:5000/tasks/3/edit
編輯 id = 3 的任務。
修改任務內容後按:
儲存變更
Flask 收到 POST,更新原本那筆 Task。
接著:
return redirect(url_for("index"))
回到首頁。
回到 MySQL Workbench 查看:
SELECT * FROM tasks;
可以看到原本:
id = 3
的那一筆內容已經跟著修改。
重點是:
id 沒有變
代表這次不是新增另一筆 Task,而是真的更新原本資料。
原本:
id = 3
修改後:
id = 3
只是其他欄位被更新。
目前 CRUD 已經完成三個:
Create
→ 新增任務
Read
→ 首頁顯示任務
Update
→ 編輯任務
現在任務如果:
標題寫錯
說明要補
狀態改變
分類改變
截止日期調整
都不需要重新新增,可以直接修改原本那筆資料。
剩下最後一個基本 CRUD:
Delete
所以下一步就是把刪除功能補上。