Dash-math_Database / firebase_progress.py
Mochi0622's picture
update
fc76b7f
Raw
History Blame Contribute Delete
7.32 kB
"""
firebase_progress.py
--------------------
學生學習進度的讀寫邏輯。
放在專案根目錄,由 dt_callbacks.py 和頁面呼叫。
Firestore 資料結構:
students/{student_id}/
profile: { name, student_id, last_seen }
progress: { quiz_correct, solutions_viewed, ai_hints_used, ... }
events: [ { type, unit, qid, ts, ... }, ... ] ← 子集合
"""
from __future__ import annotations
import datetime
from firebase_init import get_db
# ─── 時間戳 ────────────────────────────────────────────────────────────────
def _now() -> str:
return datetime.datetime.now(datetime.timezone.utc).isoformat()
# ─── 學生個人資料 ──────────────────────────────────────────────────────────
def upsert_student(student_id: str, name: str = "") -> None:
"""建立或更新學生個人資料。"""
db = get_db()
ref = db.collection("students").document(student_id)
ref.set({
"student_id": student_id,
"name": name or student_id,
"last_seen": _now(),
}, merge=True)
# ─── 記錄事件 ─────────────────────────────────────────────────────────────
def log_event(student_id: str, event_type: str, **kwargs) -> None:
"""
記錄一筆學習事件到 students/{student_id}/events 子集合。
event_type 範例:
"quiz_correct" - 填空/選擇題答對
"quiz_wrong" - 答錯
"solution_viewed" - 展開詳細解法
"hint_used" - 按下 AI 提示
"quiz_ai_done" - AI 批改完成
"""
db = get_db()
payload = {
"type": event_type,
"ts": _now(),
**kwargs, # unit, qid, answer, ...
}
db.collection("students").document(student_id)\
.collection("events").add(payload)
def log_quiz_result(
student_id: str,
unit: str,
qid: str,
correct: bool,
answer: str = "",
) -> None:
"""記錄一題答題結果,同時更新 progress 計數。"""
event_type = "quiz_correct" if correct else "quiz_wrong"
log_event(student_id, event_type, unit=unit, qid=qid, answer=answer)
# 更新統計計數
db = get_db()
ref = db.collection("students").document(student_id)
field = "quiz_correct_count" if correct else "quiz_wrong_count"
ref.set({field: _increment(1), "last_seen": _now()}, merge=True)
def log_solution_viewed(student_id: str, unit: str, solution_id: str) -> None:
"""記錄學生展開了某個解法區塊。"""
log_event(student_id, "solution_viewed", unit=unit, solution_id=solution_id)
db = get_db()
db.collection("students").document(student_id)\
.set({"solutions_viewed_count": _increment(1), "last_seen": _now()}, merge=True)
def log_choice_option(unit: str, qid: str, option_key: str) -> None:
"""記錄某題某選項被選擇的次數(全班統計,存在 question_stats 集合)。"""
db = get_db()
ref = (
db.collection("question_stats")
.document(f"{unit}__{qid}")
)
ref.set({
"unit": unit,
"qid": qid,
f"option_{option_key}": _increment(1),
}, merge=True)
def log_quiz_question_stat(unit: str, qid: str, correct: bool) -> None:
"""記錄某題全班答對/答錯次數(存在 question_stats 集合)。"""
db = get_db()
ref = db.collection("question_stats").document(f"{unit}__{qid}")
field = "correct_count" if correct else "wrong_count"
ref.set({"unit": unit, "qid": qid, field: _increment(1)}, merge=True)
def get_all_question_stats() -> list[dict]:
"""讀取所有題目的答題統計(老師後台用)。"""
db = get_db()
docs = db.collection("question_stats").stream()
return [d.to_dict() for d in docs]
def register_question(
unit: str,
qid: str,
qtype: str,
prompt_runs: list | None = None,
prompt_md: str = "",
options: list | None = None,
correct_answer: str | list | None = None,
) -> None:
"""
在學生作答時,記錄題目原始內容到 question_stats。
每次都更新(允許覆蓋),確保每道題目各自正確記錄。
prompt_runs: 題目的 run 清單(含文字與 LaTeX)
prompt_md: 題目的 markdown 純文字(備援)
options: 選項清單 [{"key": "a", "text": "..."}, ...]
correct_answer: 正確答案 key(單選為 str,多選為 list)
"""
db = get_db()
ref = db.collection("question_stats").document(f"{unit}__{qid}")
update: dict = {"unit": unit, "qid": qid, "qtype": qtype}
if prompt_runs is not None:
# 只存 text/latex 欄位,避免多餘 key 佔空間
update["prompt_runs"] = [
{k: v for k, v in r.items() if k in ("text", "latex", "kind", "style", "newline")}
for r in (prompt_runs or [])
]
if prompt_md:
update["prompt_md"] = prompt_md[:300]
if options:
update["options"] = [{"key": o.get("key", ""), "text": o.get("text", "")} for o in options]
if correct_answer is not None:
if isinstance(correct_answer, list):
update["correct_answers"] = [str(k).lower() for k in correct_answer]
else:
update["correct_answers"] = [str(correct_answer).lower()]
ref.set(update, merge=True)
def log_hint_used(student_id: str, unit: str, qid: str) -> None:
"""記錄學生使用了 AI 提示。"""
log_event(student_id, "hint_used", unit=unit, qid=qid)
db = get_db()
db.collection("students").document(student_id)\
.set({"ai_hints_used_count": _increment(1), "last_seen": _now()}, merge=True)
# ─── 讀取 ────────────────────────────────────────────────────────────────
def get_student_progress(student_id: str) -> dict:
"""讀取單一學生的 progress 計數(老師後台用)。"""
db = get_db()
doc = db.collection("students").document(student_id).get()
if doc.exists:
return doc.to_dict()
return {}
def get_all_students() -> list[dict]:
"""讀取所有學生的 profile(老師後台列表用)。"""
db = get_db()
docs = db.collection("students").stream()
return [d.to_dict() for d in docs]
def get_student_events(student_id: str, limit: int = 100) -> list[dict]:
"""讀取某學生最近的事件記錄。"""
db = get_db()
docs = (
db.collection("students").document(student_id)
.collection("events")
.order_by("ts", direction="DESCENDING")
.limit(limit)
.stream()
)
return [d.to_dict() for d in docs]
# ─── Firestore 遞增輔助 ───────────────────────────────────────────────────
def _increment(n: int):
from google.cloud.firestore import Increment
return Increment(n)