fix(templates): replace all TemplateResponse with _render to bypass Jinja2 unhashable dict cache bug
This commit is contained in:
+44
-54
@@ -94,6 +94,18 @@ from app.i18n import get_languages as _get_languages
|
|||||||
templates.env.globals["get_languages"] = _get_languages
|
templates.env.globals["get_languages"] = _get_languages
|
||||||
|
|
||||||
|
|
||||||
|
def _render(request: Request, name: str, ctx: dict) -> HTMLResponse:
|
||||||
|
"""Render a Jinja2 template and return HTMLResponse.
|
||||||
|
|
||||||
|
Bypasses Starlette TemplateResponse to avoid a Jinja2 cache bug where
|
||||||
|
the context dict is passed as ``globals`` and used as an unhashable
|
||||||
|
cache key (``TypeError: unhashable type: 'dict'``).
|
||||||
|
"""
|
||||||
|
ctx["request"] = request
|
||||||
|
html = templates.get_template(name).render(ctx)
|
||||||
|
return HTMLResponse(html)
|
||||||
|
|
||||||
|
|
||||||
def _get_copyright() -> str:
|
def _get_copyright() -> str:
|
||||||
from app.models import SystemConfig as _SC
|
from app.models import SystemConfig as _SC
|
||||||
from app.database import SessionLocal
|
from app.database import SessionLocal
|
||||||
@@ -151,8 +163,7 @@ def index_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
if not lang or lang not in avail:
|
if not lang or lang not in avail:
|
||||||
lang = "zh"
|
lang = "zh"
|
||||||
|
|
||||||
return templates.TemplateResponse("index.html", {
|
return _render(request, "index.html", {"postcards": all_postcards[:20], "lang": lang,
|
||||||
"request": request, "postcards": all_postcards[:20], "lang": lang,
|
|
||||||
"has_more": len(all_postcards) > 20,
|
"has_more": len(all_postcards) > 20,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -248,7 +259,7 @@ def login_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
user = redirect_if_not_logged_in(request, db)
|
user = redirect_if_not_logged_in(request, db)
|
||||||
if user:
|
if user:
|
||||||
return _redirect("/dashboard")
|
return _redirect("/dashboard")
|
||||||
return templates.TemplateResponse("login.html", {"request": request})
|
return _render(request, "login.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login")
|
@router.post("/login")
|
||||||
@@ -263,8 +274,7 @@ def login_submit(
|
|||||||
cleanup_expired_sessions(db)
|
cleanup_expired_sessions(db)
|
||||||
user = db.query(User).filter(User.username == username).first()
|
user = db.query(User).filter(User.username == username).first()
|
||||||
if not user or not verify_password(password, user.password_hash):
|
if not user or not verify_password(password, user.password_hash):
|
||||||
return templates.TemplateResponse(
|
return _render(request, "login.html", {"error": "用户名或密码错误"}, status_code=401
|
||||||
"login.html", {"request": request, "error": "用户名或密码错误"}, status_code=401
|
|
||||||
)
|
)
|
||||||
is_remember = remember_me == "on"
|
is_remember = remember_me == "on"
|
||||||
token = create_session(user.id, db, remember_me=is_remember)
|
token = create_session(user.id, db, remember_me=is_remember)
|
||||||
@@ -283,7 +293,7 @@ def register_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
user = redirect_if_not_logged_in(request, db)
|
user = redirect_if_not_logged_in(request, db)
|
||||||
if user:
|
if user:
|
||||||
return _redirect("/dashboard")
|
return _redirect("/dashboard")
|
||||||
return templates.TemplateResponse("register.html", {"request": request})
|
return _render(request, "register.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register")
|
@router.post("/register")
|
||||||
@@ -298,30 +308,24 @@ def register_submit(
|
|||||||
# Validate invite code
|
# Validate invite code
|
||||||
code_obj = db.query(InviteCode).filter(InviteCode.code == invite_code).first()
|
code_obj = db.query(InviteCode).filter(InviteCode.code == invite_code).first()
|
||||||
if not code_obj:
|
if not code_obj:
|
||||||
return templates.TemplateResponse(
|
return _render(request, "register.html", {"error": "邀请码无效"}, status_code=400
|
||||||
"register.html", {"request": request, "error": "邀请码无效"}, status_code=400
|
|
||||||
)
|
)
|
||||||
if code_obj.expires_at and code_obj.expires_at < datetime.utcnow():
|
if code_obj.expires_at and code_obj.expires_at < datetime.utcnow():
|
||||||
return templates.TemplateResponse(
|
return _render(request, "register.html", {"error": "邀请码已过期"}, status_code=400
|
||||||
"register.html", {"request": request, "error": "邀请码已过期"}, status_code=400
|
|
||||||
)
|
)
|
||||||
if code_obj.max_uses is not None and code_obj.use_count >= code_obj.max_uses:
|
if code_obj.max_uses is not None and code_obj.use_count >= code_obj.max_uses:
|
||||||
return templates.TemplateResponse(
|
return _render(request, "register.html", {"error": "邀请码使用次数已用尽"}, status_code=400
|
||||||
"register.html", {"request": request, "error": "邀请码使用次数已用尽"}, status_code=400
|
|
||||||
)
|
)
|
||||||
# Validate passwords
|
# Validate passwords
|
||||||
if password != confirm_password:
|
if password != confirm_password:
|
||||||
return templates.TemplateResponse(
|
return _render(request, "register.html", {"error": "两次输入的密码不一致"}, status_code=400
|
||||||
"register.html", {"request": request, "error": "两次输入的密码不一致"}, status_code=400
|
|
||||||
)
|
)
|
||||||
if len(password) < 6:
|
if len(password) < 6:
|
||||||
return templates.TemplateResponse(
|
return _render(request, "register.html", {"error": "密码至少6个字符"}, status_code=400
|
||||||
"register.html", {"request": request, "error": "密码至少6个字符"}, status_code=400
|
|
||||||
)
|
)
|
||||||
# Check username
|
# Check username
|
||||||
if db.query(User).filter(User.username == username).first():
|
if db.query(User).filter(User.username == username).first():
|
||||||
return templates.TemplateResponse(
|
return _render(request, "register.html", {"error": "用户名已存在"}, status_code=400
|
||||||
"register.html", {"request": request, "error": "用户名已存在"}, status_code=400
|
|
||||||
)
|
)
|
||||||
# Create user
|
# Create user
|
||||||
new_user = User(username=username, password_hash=hash_password(password), invited_by_code_id=code_obj.id)
|
new_user = User(username=username, password_hash=hash_password(password), invited_by_code_id=code_obj.id)
|
||||||
@@ -359,7 +363,9 @@ def _admin_context(user, db, active_tab="invites", **extra):
|
|||||||
|
|
||||||
|
|
||||||
def _admin_render(request, user, db, active_tab="invites", **extra):
|
def _admin_render(request, user, db, active_tab="invites", **extra):
|
||||||
return templates.TemplateResponse("admin.html", _admin_context(user, db, active_tab, request=request, **extra))
|
ctx = _admin_context(user, db, active_tab, **extra)
|
||||||
|
ctx["request"] = request
|
||||||
|
return _render(request, "admin.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin", response_class=HTMLResponse)
|
@router.get("/admin", response_class=HTMLResponse)
|
||||||
@@ -501,8 +507,7 @@ def settings_page(
|
|||||||
for p in profiles:
|
for p in profiles:
|
||||||
profile_cards[p.id] = db.query(Postcard).filter(Postcard.profile_id == p.id).all()
|
profile_cards[p.id] = db.query(Postcard).filter(Postcard.profile_id == p.id).all()
|
||||||
api_keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).order_by(ApiKey.created_at.desc()).all()
|
api_keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).order_by(ApiKey.created_at.desc()).all()
|
||||||
return templates.TemplateResponse("settings.html", {
|
return _render(request, "settings.html", {"user": user, "profiles": profiles,
|
||||||
"request": request, "user": user, "profiles": profiles,
|
|
||||||
"profile_cards": profile_cards, "active_tab": active_tab,
|
"profile_cards": profile_cards, "active_tab": active_tab,
|
||||||
"api_keys": api_keys,
|
"api_keys": api_keys,
|
||||||
})
|
})
|
||||||
@@ -516,13 +521,11 @@ def change_username(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
if db.query(User).filter(User.username == new_username, User.id != user.id).first():
|
if db.query(User).filter(User.username == new_username, User.id != user.id).first():
|
||||||
return templates.TemplateResponse(
|
return _render(request, "settings.html", {"user": user, "error": "用户名已存在"}, status_code=400
|
||||||
"settings.html", {"request": request, "user": user, "error": "用户名已存在"}, status_code=400
|
|
||||||
)
|
)
|
||||||
user.username = new_username
|
user.username = new_username
|
||||||
db.commit()
|
db.commit()
|
||||||
return templates.TemplateResponse(
|
return _render(request, "settings.html", {"user": user, "success": "用户名修改成功"}
|
||||||
"settings.html", {"request": request, "user": user, "success": "用户名修改成功"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -536,21 +539,17 @@ def change_password(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
if not verify_password(old_password, user.password_hash):
|
if not verify_password(old_password, user.password_hash):
|
||||||
return templates.TemplateResponse(
|
return _render(request, "settings.html", {"user": user, "error": "当前密码错误"}, status_code=400
|
||||||
"settings.html", {"request": request, "user": user, "error": "当前密码错误"}, status_code=400
|
|
||||||
)
|
)
|
||||||
if new_password != confirm_password:
|
if new_password != confirm_password:
|
||||||
return templates.TemplateResponse(
|
return _render(request, "settings.html", {"user": user, "error": "两次输入的新密码不一致"}, status_code=400
|
||||||
"settings.html", {"request": request, "user": user, "error": "两次输入的新密码不一致"}, status_code=400
|
|
||||||
)
|
)
|
||||||
if len(new_password) < 6:
|
if len(new_password) < 6:
|
||||||
return templates.TemplateResponse(
|
return _render(request, "settings.html", {"user": user, "error": "新密码长度至少6位"}, status_code=400
|
||||||
"settings.html", {"request": request, "user": user, "error": "新密码长度至少6位"}, status_code=400
|
|
||||||
)
|
)
|
||||||
user.password_hash = hash_password(new_password)
|
user.password_hash = hash_password(new_password)
|
||||||
db.commit()
|
db.commit()
|
||||||
return templates.TemplateResponse(
|
return _render(request, "settings.html", {"user": user, "success": "密码修改成功"}
|
||||||
"settings.html", {"request": request, "user": user, "success": "密码修改成功"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -626,8 +625,7 @@ def dashboard(request: Request, user: User = Depends(get_current_user_web), db:
|
|||||||
|
|
||||||
from datetime import datetime, timezone as _tz
|
from datetime import datetime, timezone as _tz
|
||||||
|
|
||||||
return templates.TemplateResponse("dashboard.html", {
|
return _render(request, "dashboard.html", {"user": user, "profiles": profiles,
|
||||||
"request": request, "user": user, "profiles": profiles,
|
|
||||||
"total": total, "sent": sent, "delivered": delivered, "received": received,
|
"total": total, "sent": sent, "delivered": delivered, "received": received,
|
||||||
"countries": countries, "recent": recent,
|
"countries": countries, "recent": recent,
|
||||||
"sent_pct": sent_pct, "delivered_pct": delivered_pct, "received_pct": received_pct,
|
"sent_pct": sent_pct, "delivered_pct": delivered_pct, "received_pct": received_pct,
|
||||||
@@ -651,7 +649,7 @@ def profile_list(request: Request, user: User = Depends(get_current_user_web), d
|
|||||||
"received": sum(1 for c in cards if c.status == "received"),
|
"received": sum(1 for c in cards if c.status == "received"),
|
||||||
"countries": len({c.country_to for c in cards if c.country_to}),
|
"countries": len({c.country_to for c in cards if c.country_to}),
|
||||||
}
|
}
|
||||||
return templates.TemplateResponse("profiles.html", {"request": request, "user": user, "profiles": profiles, "extras": extras})
|
return _render(request, "profiles.html", {"user": user, "profiles": profiles, "extras": extras})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/profiles")
|
@router.post("/profiles")
|
||||||
@@ -719,9 +717,7 @@ def postcard_list(
|
|||||||
sent = [pc for pc in postcards if pc.status in ("pending", "sent", "delivered")]
|
sent = [pc for pc in postcards if pc.status in ("pending", "sent", "delivered")]
|
||||||
received = [pc for pc in postcards if pc.status == "received"]
|
received = [pc for pc in postcards if pc.status == "received"]
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return _render(request, "postcards.html", {"user": user, "profile": profile, "sent": sent, "received": received, "countries": all_countries, "country": country},
|
||||||
"postcards.html",
|
|
||||||
{"request": request, "user": user, "profile": profile, "sent": sent, "received": received, "countries": all_countries, "country": country},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -731,7 +727,7 @@ def postcard_new(request: Request, profile_id: int, create_type: str = Query("se
|
|||||||
if not profile:
|
if not profile:
|
||||||
return _redirect("/profiles")
|
return _redirect("/profiles")
|
||||||
today = datetime.now().strftime("%Y-%m-%d")
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
return templates.TemplateResponse("postcard_form.html", {"request": request, "user": user, "profile": profile, "postcard": None, "create_type": create_type, "today": today})
|
return _render(request, "postcard_form.html", {"user": user, "profile": profile, "postcard": None, "create_type": create_type, "today": today})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/profiles/{profile_id}/postcards")
|
@router.post("/profiles/{profile_id}/postcards")
|
||||||
@@ -760,8 +756,7 @@ def postcard_create(
|
|||||||
Postcard.card_number == cn, Profile.user_id == user.id
|
Postcard.card_number == cn, Profile.user_id == user.id
|
||||||
).first()
|
).first()
|
||||||
if dup:
|
if dup:
|
||||||
return templates.TemplateResponse("postcard_form.html", {
|
return _render(request, "postcard_form.html", {"user": user, "profile": profile, "postcard": None,
|
||||||
"request": request, "user": user, "profile": profile, "postcard": None,
|
|
||||||
"error": _t("pc_list.duplicate", user.language),
|
"error": _t("pc_list.duplicate", user.language),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -805,7 +800,7 @@ def postcard_detail(request: Request, postcard_id: int, user: User = Depends(get
|
|||||||
)
|
)
|
||||||
if not pc:
|
if not pc:
|
||||||
return _redirect("/profiles")
|
return _redirect("/profiles")
|
||||||
return templates.TemplateResponse("postcard_detail.html", {"request": request, "user": user, "postcard": pc})
|
return _render(request, "postcard_detail.html", {"user": user, "postcard": pc})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/postcards/{postcard_id}/edit", response_class=HTMLResponse)
|
@router.get("/postcards/{postcard_id}/edit", response_class=HTMLResponse)
|
||||||
@@ -818,7 +813,7 @@ def postcard_edit(request: Request, postcard_id: int, user: User = Depends(get_c
|
|||||||
)
|
)
|
||||||
if not pc:
|
if not pc:
|
||||||
return _redirect("/profiles")
|
return _redirect("/profiles")
|
||||||
return templates.TemplateResponse("postcard_form.html", {"request": request, "user": user, "profile": pc.profile, "postcard": pc})
|
return _render(request, "postcard_form.html", {"user": user, "profile": pc.profile, "postcard": pc})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/postcards/{postcard_id}/edit")
|
@router.post("/postcards/{postcard_id}/edit")
|
||||||
@@ -853,8 +848,7 @@ def postcard_update(
|
|||||||
Profile.user_id == user.id
|
Profile.user_id == user.id
|
||||||
).first()
|
).first()
|
||||||
if dup:
|
if dup:
|
||||||
return templates.TemplateResponse("postcard_form.html", {
|
return _render(request, "postcard_form.html", {"user": user, "profile": pc.profile, "postcard": pc,
|
||||||
"request": request, "user": user, "profile": pc.profile, "postcard": pc,
|
|
||||||
"error": _t("pc_list.duplicate", user.language),
|
"error": _t("pc_list.duplicate", user.language),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1000,8 +994,7 @@ def profile_showcase_page(
|
|||||||
if not p:
|
if not p:
|
||||||
return _redirect("/settings?tab=showcase")
|
return _redirect("/settings?tab=showcase")
|
||||||
cards = db.query(Postcard).filter(Postcard.profile_id == profile_id).all()
|
cards = db.query(Postcard).filter(Postcard.profile_id == profile_id).all()
|
||||||
return templates.TemplateResponse("profile_showcase.html", {
|
return _render(request, "profile_showcase.html", {"user": user, "profile": p, "cards": cards,
|
||||||
"request": request, "user": user, "profile": p, "cards": cards,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1073,8 +1066,7 @@ def public_showcase(request: Request, username: str, db: Session = Depends(get_d
|
|||||||
profile_groups.append({"profile": p, "sent": p_sent, "received": p_received})
|
profile_groups.append({"profile": p, "sent": p_sent, "received": p_received})
|
||||||
sent_all.sort(key=lambda c: c.send_time or c.created_at, reverse=True)
|
sent_all.sort(key=lambda c: c.send_time or c.created_at, reverse=True)
|
||||||
received_all.sort(key=lambda c: c.receive_time or c.created_at, reverse=True)
|
received_all.sort(key=lambda c: c.receive_time or c.created_at, reverse=True)
|
||||||
resp = templates.TemplateResponse("showcase.html", {
|
resp = _render(request, "showcase.html", {"profile_user": user, "mode": user.showcase_mode,
|
||||||
"request": request, "profile_user": user, "mode": user.showcase_mode,
|
|
||||||
"profile_groups": profile_groups, "sent_all": sent_all, "received_all": received_all,
|
"profile_groups": profile_groups, "sent_all": sent_all, "received_all": received_all,
|
||||||
"lang": visitor_lang, "available_languages": _avail,
|
"lang": visitor_lang, "available_languages": _avail,
|
||||||
})
|
})
|
||||||
@@ -1104,8 +1096,7 @@ def apikey_create(
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(ak)
|
db.refresh(ak)
|
||||||
api_keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).order_by(ApiKey.created_at.desc()).all()
|
api_keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).order_by(ApiKey.created_at.desc()).all()
|
||||||
return templates.TemplateResponse("settings.html", {
|
return _render(request, "settings.html", {"user": user, "active_tab": "apikeys",
|
||||||
"request": request, "user": user, "active_tab": "apikeys",
|
|
||||||
"api_keys": api_keys, "new_key": ak.key, "success": "API Key created",
|
"api_keys": api_keys, "new_key": ak.key, "success": "API Key created",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1153,6 +1144,5 @@ def api_docs_page(
|
|||||||
user: User = Depends(get_current_user_web),
|
user: User = Depends(get_current_user_web),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
return templates.TemplateResponse("api_docs.html", {
|
return _render(request, "api_docs.html", {"user": user,
|
||||||
"request": request, "user": user,
|
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user