mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-03-19 03:36:30 +01:00
refactor: modular route organization (Phase 1-2 complete)
- Split monolithic build route handler into focused modules - Extract validation, multi-copy, include/exclude, themes, and partner routes - Add response utilities and telemetry decorators - Create route pattern documentation - Fix multi-copy detection bug (tag key mismatch) - Improve code maintainability and testability Roadmap 9 M1 Phase 1-2
This commit is contained in:
parent
97da117ccb
commit
e81b47bccf
20 changed files with 2852 additions and 1552 deletions
205
code/web/routes/build_themes.py
Normal file
205
code/web/routes/build_themes.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
"""
|
||||
Custom theme management routes for deck building.
|
||||
|
||||
Handles user-defined custom themes including adding, removing, choosing
|
||||
suggestions, and switching between permissive/strict matching modes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from ..app import (
|
||||
ENABLE_CUSTOM_THEMES,
|
||||
USER_THEME_LIMIT,
|
||||
DEFAULT_THEME_MATCH_MODE,
|
||||
_sanitize_theme,
|
||||
templates,
|
||||
)
|
||||
from ..services.tasks import get_session, new_sid
|
||||
from ..services import custom_theme_manager as theme_mgr
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
_INVALID_THEME_MESSAGE = (
|
||||
"Theme names can only include letters, numbers, spaces, hyphens, apostrophes, and underscores."
|
||||
)
|
||||
|
||||
|
||||
def _custom_theme_context(
|
||||
request: Request,
|
||||
sess: dict,
|
||||
*,
|
||||
message: str | None = None,
|
||||
level: str = "info",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Assemble the Additional Themes section context for the modal.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
sess: Session dictionary
|
||||
message: Optional status message to display
|
||||
level: Message level ("info", "success", "warning", "error")
|
||||
|
||||
Returns:
|
||||
Context dictionary for rendering the additional themes template
|
||||
"""
|
||||
if not ENABLE_CUSTOM_THEMES:
|
||||
return {
|
||||
"request": request,
|
||||
"theme_state": None,
|
||||
"theme_message": message,
|
||||
"theme_message_level": level,
|
||||
"theme_limit": USER_THEME_LIMIT,
|
||||
"enable_custom_themes": False,
|
||||
}
|
||||
theme_mgr.set_limit(sess, USER_THEME_LIMIT)
|
||||
state = theme_mgr.get_view_state(sess, default_mode=DEFAULT_THEME_MATCH_MODE)
|
||||
return {
|
||||
"request": request,
|
||||
"theme_state": state,
|
||||
"theme_message": message,
|
||||
"theme_message_level": level,
|
||||
"theme_limit": USER_THEME_LIMIT,
|
||||
"enable_custom_themes": ENABLE_CUSTOM_THEMES,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/themes/add", response_class=HTMLResponse)
|
||||
async def build_theme_add(request: Request, theme: str = Form("")) -> HTMLResponse:
|
||||
"""
|
||||
Add a custom theme to the user's theme list.
|
||||
|
||||
Validates theme name format and enforces theme count limits.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
theme: Theme name to add (will be trimmed and sanitized)
|
||||
|
||||
Returns:
|
||||
HTMLResponse with updated themes list and status message
|
||||
"""
|
||||
if not ENABLE_CUSTOM_THEMES:
|
||||
return HTMLResponse("", status_code=204)
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
trimmed = theme.strip()
|
||||
sanitized = _sanitize_theme(trimmed) if trimmed else ""
|
||||
if trimmed and not sanitized:
|
||||
ctx = _custom_theme_context(request, sess, message=_INVALID_THEME_MESSAGE, level="error")
|
||||
else:
|
||||
value = sanitized if sanitized is not None else trimmed
|
||||
_, message, level = theme_mgr.add_theme(
|
||||
sess,
|
||||
value,
|
||||
commander_tags=list(sess.get("tags", [])),
|
||||
mode=sess.get("theme_match_mode", DEFAULT_THEME_MATCH_MODE),
|
||||
limit=USER_THEME_LIMIT,
|
||||
)
|
||||
ctx = _custom_theme_context(request, sess, message=message, level=level)
|
||||
resp = templates.TemplateResponse("build/_new_deck_additional_themes.html", ctx)
|
||||
resp.set_cookie("sid", sid, httponly=True, samesite="lax")
|
||||
return resp
|
||||
|
||||
|
||||
@router.post("/themes/remove", response_class=HTMLResponse)
|
||||
async def build_theme_remove(request: Request, theme: str = Form("")) -> HTMLResponse:
|
||||
"""
|
||||
Remove a custom theme from the user's theme list.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
theme: Theme name to remove
|
||||
|
||||
Returns:
|
||||
HTMLResponse with updated themes list and status message
|
||||
"""
|
||||
if not ENABLE_CUSTOM_THEMES:
|
||||
return HTMLResponse("", status_code=204)
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
value = _sanitize_theme(theme) or theme
|
||||
_, message, level = theme_mgr.remove_theme(
|
||||
sess,
|
||||
value,
|
||||
commander_tags=list(sess.get("tags", [])),
|
||||
mode=sess.get("theme_match_mode", DEFAULT_THEME_MATCH_MODE),
|
||||
)
|
||||
ctx = _custom_theme_context(request, sess, message=message, level=level)
|
||||
resp = templates.TemplateResponse("build/_new_deck_additional_themes.html", ctx)
|
||||
resp.set_cookie("sid", sid, httponly=True, samesite="lax")
|
||||
return resp
|
||||
|
||||
|
||||
@router.post("/themes/choose", response_class=HTMLResponse)
|
||||
async def build_theme_choose(
|
||||
request: Request,
|
||||
original: str = Form(""),
|
||||
choice: str = Form(""),
|
||||
) -> HTMLResponse:
|
||||
"""
|
||||
Replace an invalid theme with a suggested alternative.
|
||||
|
||||
When a user's custom theme doesn't perfectly match commander tags,
|
||||
the system suggests alternatives. This route accepts the user's
|
||||
choice from those suggestions.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
original: The original (invalid) theme name
|
||||
choice: The selected suggestion to use instead
|
||||
|
||||
Returns:
|
||||
HTMLResponse with updated themes list and status message
|
||||
"""
|
||||
if not ENABLE_CUSTOM_THEMES:
|
||||
return HTMLResponse("", status_code=204)
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
selection = _sanitize_theme(choice) or choice
|
||||
_, message, level = theme_mgr.choose_suggestion(
|
||||
sess,
|
||||
original,
|
||||
selection,
|
||||
commander_tags=list(sess.get("tags", [])),
|
||||
mode=sess.get("theme_match_mode", DEFAULT_THEME_MATCH_MODE),
|
||||
)
|
||||
ctx = _custom_theme_context(request, sess, message=message, level=level)
|
||||
resp = templates.TemplateResponse("build/_new_deck_additional_themes.html", ctx)
|
||||
resp.set_cookie("sid", sid, httponly=True, samesite="lax")
|
||||
return resp
|
||||
|
||||
|
||||
@router.post("/themes/mode", response_class=HTMLResponse)
|
||||
async def build_theme_mode(request: Request, mode: str = Form("permissive")) -> HTMLResponse:
|
||||
"""
|
||||
Switch theme matching mode between permissive and strict.
|
||||
|
||||
- Permissive: Suggests alternatives for invalid themes
|
||||
- Strict: Rejects invalid themes outright
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
mode: Either "permissive" or "strict"
|
||||
|
||||
Returns:
|
||||
HTMLResponse with updated themes list and status message
|
||||
"""
|
||||
if not ENABLE_CUSTOM_THEMES:
|
||||
return HTMLResponse("", status_code=204)
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
_, message, level = theme_mgr.set_mode(
|
||||
sess,
|
||||
mode,
|
||||
commander_tags=list(sess.get("tags", [])),
|
||||
)
|
||||
ctx = _custom_theme_context(request, sess, message=message, level=level)
|
||||
resp = templates.TemplateResponse("build/_new_deck_additional_themes.html", ctx)
|
||||
resp.set_cookie("sid", sid, httponly=True, samesite="lax")
|
||||
return resp
|
||||
Loading…
Add table
Add a link
Reference in a new issue