mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-03-25 14:36:31 +01:00
feat: revamp multicopy flow with include/exclude conflict dialogs
This commit is contained in:
parent
4aa41adb20
commit
804c750bb2
14 changed files with 665 additions and 166 deletions
|
|
@ -8,9 +8,10 @@ for deck building, including the card toggle endpoint and summary rendering.
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi import APIRouter, Request, Form, Query
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
from deck_builder import builder_constants as bc
|
||||
from ..app import ALLOW_MUST_HAVES, templates
|
||||
from ..services.build_utils import step5_base_ctx
|
||||
from ..services.tasks import get_session, new_sid
|
||||
|
|
@ -21,6 +22,15 @@ from .build import _merge_hx_trigger
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
def _is_multi_copy_archetype(card_name: str) -> dict | None:
|
||||
"""Return archetype dict if card_name exactly matches a known multi-copy archetype, else None."""
|
||||
normalized = card_name.strip().lower()
|
||||
for archetype in bc.MULTI_COPY_ARCHETYPES.values():
|
||||
if str(archetype.get("name", "")).strip().lower() == normalized:
|
||||
return archetype
|
||||
return None
|
||||
|
||||
|
||||
def _must_have_state(sess: dict) -> tuple[dict[str, Any], list[str], list[str]]:
|
||||
"""
|
||||
Extract include/exclude card lists and enforcement settings from session.
|
||||
|
|
@ -129,6 +139,22 @@ async def toggle_must_haves(
|
|||
sid = new_sid()
|
||||
sess = get_session(sid)
|
||||
|
||||
# R13: Multi-copy archetype conflict detection
|
||||
if enabled_flag and ALLOW_MUST_HAVES:
|
||||
archetype = _is_multi_copy_archetype(name)
|
||||
if list_key == "include" and archetype:
|
||||
ctx = {"request": request, "archetype": archetype, "card_name": name}
|
||||
resp = templates.TemplateResponse("partials/multicopy_include_dialog.html", ctx)
|
||||
resp.set_cookie("sid", sid, httponly=True, samesite="lax")
|
||||
return resp
|
||||
if list_key == "exclude":
|
||||
active_mc = sess.get("multi_copy") or {}
|
||||
if active_mc and str(active_mc.get("name", "")).strip().lower() == name.lower():
|
||||
ctx = {"request": request, "archetype": active_mc, "card_name": name}
|
||||
resp = templates.TemplateResponse("partials/multicopy_exclude_warning.html", ctx)
|
||||
resp.set_cookie("sid", sid, httponly=True, samesite="lax")
|
||||
return resp
|
||||
|
||||
includes = list(sess.get("include_cards") or [])
|
||||
excludes = list(sess.get("exclude_cards") or [])
|
||||
include_lookup = {str(v).strip().lower(): str(v) for v in includes if str(v).strip()}
|
||||
|
|
@ -214,3 +240,129 @@ async def toggle_must_haves(
|
|||
except Exception:
|
||||
pass
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/must-haves/summary", response_class=HTMLResponse)
|
||||
async def must_haves_summary(request: Request) -> HTMLResponse:
|
||||
"""Return the current include/exclude summary fragment (used by dialog cancel actions)."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
return _render_include_exclude_summary(request, sess, sid)
|
||||
|
||||
|
||||
@router.get("/must-haves/multicopy-dialog", response_class=HTMLResponse)
|
||||
async def multicopy_include_dialog_view(
|
||||
request: Request,
|
||||
card_name: str = Query(...),
|
||||
) -> HTMLResponse:
|
||||
"""Return the count-picker dialog fragment for a multi-copy archetype card."""
|
||||
archetype = _is_multi_copy_archetype(card_name)
|
||||
if not archetype:
|
||||
return HTMLResponse("", status_code=200)
|
||||
ctx = {"request": request, "archetype": archetype, "card_name": card_name}
|
||||
return templates.TemplateResponse("partials/multicopy_include_dialog.html", ctx)
|
||||
|
||||
|
||||
@router.get("/must-haves/exclude-archetype-warning", response_class=HTMLResponse)
|
||||
async def exclude_archetype_warning_view(
|
||||
request: Request,
|
||||
card_name: str = Query(...),
|
||||
) -> HTMLResponse:
|
||||
"""Return the warning dialog fragment for excluding the currently-active archetype."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
active_mc = sess.get("multi_copy") or {}
|
||||
ctx = {"request": request, "archetype": active_mc, "card_name": card_name}
|
||||
return templates.TemplateResponse("partials/multicopy_exclude_warning.html", ctx)
|
||||
|
||||
|
||||
@router.post("/must-haves/save-archetype-include", response_class=HTMLResponse)
|
||||
async def save_archetype_include(
|
||||
request: Request,
|
||||
card_name: str = Form(...),
|
||||
choice_id: str = Form(...),
|
||||
count: int = Form(None),
|
||||
thrumming: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Save multi-copy archetype selection and add card to the must-include list.
|
||||
|
||||
Combines the archetype save (multicopy/save) and include toggle into a single
|
||||
action, called from the multicopy_include_dialog when the user confirms.
|
||||
"""
|
||||
if not ALLOW_MUST_HAVES:
|
||||
return JSONResponse({"error": "Must-have lists are disabled"}, status_code=403)
|
||||
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
|
||||
# Persist archetype selection (mirrors multicopy/save logic)
|
||||
meta = bc.MULTI_COPY_ARCHETYPES.get(str(choice_id), {})
|
||||
archetype_name = meta.get("name") or card_name.strip()
|
||||
if count is None:
|
||||
count = int(meta.get("default_count", 25))
|
||||
try:
|
||||
count = int(count)
|
||||
except Exception:
|
||||
count = int(meta.get("default_count", 25))
|
||||
printed_cap = meta.get("printed_cap")
|
||||
if isinstance(printed_cap, int) and printed_cap > 0:
|
||||
count = max(1, min(printed_cap, count))
|
||||
sess["multi_copy"] = {
|
||||
"id": str(choice_id),
|
||||
"name": archetype_name,
|
||||
"count": count,
|
||||
"thrumming": str(thrumming or "").strip().lower() in {"1", "true", "on", "yes"},
|
||||
}
|
||||
try:
|
||||
if "mc_applied_key" in sess:
|
||||
del sess["mc_applied_key"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add card to include list
|
||||
name = card_name.strip()
|
||||
key = name.lower()
|
||||
includes = list(sess.get("include_cards") or [])
|
||||
include_lookup = {str(v).strip().lower(): str(v) for v in includes}
|
||||
if key not in include_lookup:
|
||||
includes.append(name)
|
||||
# Remove from excludes if present
|
||||
excludes = [c for c in (sess.get("exclude_cards") or []) if str(c).strip().lower() != key]
|
||||
sess["include_cards"] = includes
|
||||
sess["exclude_cards"] = excludes
|
||||
|
||||
return _render_include_exclude_summary(request, sess, sid)
|
||||
|
||||
|
||||
@router.post("/must-haves/clear-archetype", response_class=HTMLResponse)
|
||||
async def clear_archetype(
|
||||
request: Request,
|
||||
card_name: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Clear the active multi-copy archetype and optionally add card to the exclude list.
|
||||
|
||||
Called when user confirms the exclude-archetype-warning dialog.
|
||||
"""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
|
||||
for k in ("multi_copy", "mc_applied_key", "mc_seen_keys"):
|
||||
try:
|
||||
if k in sess:
|
||||
del sess[k]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if card_name:
|
||||
name = str(card_name).strip()
|
||||
key = name.lower()
|
||||
excludes = list(sess.get("exclude_cards") or [])
|
||||
exclude_lookup = {str(v).strip().lower(): str(v) for v in excludes}
|
||||
if key not in exclude_lookup:
|
||||
excludes.append(name)
|
||||
# Remove from includes if present
|
||||
includes = [c for c in (sess.get("include_cards") or []) if str(c).strip().lower() != key]
|
||||
sess["include_cards"] = includes
|
||||
sess["exclude_cards"] = excludes
|
||||
|
||||
return _render_include_exclude_summary(request, sess, sid)
|
||||
|
|
|
|||
|
|
@ -150,6 +150,10 @@ async def multicopy_check(request: Request) -> HTMLResponse:
|
|||
pass
|
||||
# Detect viable archetypes
|
||||
results = bu.detect_viable_multi_copy_archetypes(tmp) or []
|
||||
# R13: Filter out archetypes whose card is in the must-exclude list
|
||||
exclude_names = {str(c).strip().lower() for c in (sess.get("exclude_cards") or [])}
|
||||
if exclude_names:
|
||||
results = [r for r in results if str(r.get("name", "")).strip().lower() not in exclude_names]
|
||||
if not results:
|
||||
# Remember this key to avoid re-checking until tags/commander change
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -37,12 +37,27 @@ from .build_partners import (
|
|||
_partner_ui_context,
|
||||
_resolve_partner_selection,
|
||||
)
|
||||
from .build_wizard import _prepare_step2_theme_data, _section_themes_by_pool_size # R21: Pool size data
|
||||
from .build_themes import _prepare_step2_theme_data, _section_themes_by_pool_size
|
||||
from ..services import custom_theme_manager as theme_mgr
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Pre-built JS-serialisable map of multi-copy archetypes for client-side popup detection.
|
||||
# Keys are lowercased card names; values contain what the popup needs.
|
||||
_ARCHETYPE_JS_MAP: dict[str, dict] = {
|
||||
str(a["name"]).strip().lower(): {
|
||||
"id": a["id"],
|
||||
"name": a["name"],
|
||||
"default_count": a.get("default_count", 25),
|
||||
"printed_cap": a.get("printed_cap"),
|
||||
"rec_window": list(a["rec_window"]) if a.get("rec_window") else None,
|
||||
"thrumming_stone_synergy": bool(a.get("thrumming_stone_synergy", False)),
|
||||
}
|
||||
for a in bc.MULTI_COPY_ARCHETYPES.values()
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# New Deck Modal and Commander Search
|
||||
# ==============================================================================
|
||||
|
|
@ -99,6 +114,7 @@ async def build_new_modal(request: Request) -> HTMLResponse:
|
|||
"enable_custom_themes": ENABLE_CUSTOM_THEMES,
|
||||
"enable_batch_build": ENABLE_BATCH_BUILD,
|
||||
"ideals_ui_mode": WEB_IDEALS_UI, # 'input' or 'slider'
|
||||
"multi_copy_archetypes_js": _ARCHETYPE_JS_MAP,
|
||||
"form": {
|
||||
"commander": sess.get("commander", ""), # Pre-fill for quick-build
|
||||
"prefer_combos": bool(sess.get("prefer_combos")),
|
||||
|
|
@ -485,6 +501,7 @@ async def build_new_submit(
|
|||
"show_must_have_buttons": SHOW_MUST_HAVE_BUTTONS,
|
||||
"enable_custom_themes": ENABLE_CUSTOM_THEMES,
|
||||
"enable_batch_build": ENABLE_BATCH_BUILD,
|
||||
"multi_copy_archetypes_js": _ARCHETYPE_JS_MAP,
|
||||
"form": _form_state(suggested),
|
||||
"tag_slot_html": None,
|
||||
}
|
||||
|
|
@ -510,6 +527,7 @@ async def build_new_submit(
|
|||
"show_must_have_buttons": SHOW_MUST_HAVE_BUTTONS,
|
||||
"enable_custom_themes": ENABLE_CUSTOM_THEMES,
|
||||
"enable_batch_build": ENABLE_BATCH_BUILD,
|
||||
"multi_copy_archetypes_js": _ARCHETYPE_JS_MAP,
|
||||
"form": _form_state(commander),
|
||||
"tag_slot_html": None,
|
||||
}
|
||||
|
|
@ -615,6 +633,7 @@ async def build_new_submit(
|
|||
"show_must_have_buttons": SHOW_MUST_HAVE_BUTTONS,
|
||||
"enable_custom_themes": ENABLE_CUSTOM_THEMES,
|
||||
"enable_batch_build": ENABLE_BATCH_BUILD,
|
||||
"multi_copy_archetypes_js": _ARCHETYPE_JS_MAP,
|
||||
"form": _form_state(primary_commander_name),
|
||||
"tag_slot_html": tag_slot_html,
|
||||
}
|
||||
|
|
@ -754,6 +773,7 @@ async def build_new_submit(
|
|||
"show_must_have_buttons": SHOW_MUST_HAVE_BUTTONS,
|
||||
"enable_custom_themes": ENABLE_CUSTOM_THEMES,
|
||||
"enable_batch_build": ENABLE_BATCH_BUILD,
|
||||
"multi_copy_archetypes_js": _ARCHETYPE_JS_MAP,
|
||||
"form": _form_state(sess.get("commander", "")),
|
||||
"tag_slot_html": None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,58 @@ from ..app import (
|
|||
)
|
||||
from ..services.tasks import get_session, new_sid
|
||||
from ..services import custom_theme_manager as theme_mgr
|
||||
from ..services.theme_catalog_loader import load_index, slugify
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _prepare_step2_theme_data(tags: list[str], recommended: list[str]) -> tuple[list[str], list[str], dict[str, int]]:
|
||||
"""Load pool size data and sort themes for display.
|
||||
|
||||
Returns:
|
||||
Tuple of (sorted_tags, sorted_recommended, pool_size_dict)
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
theme_index = load_index()
|
||||
pool_size_by_slug = theme_index.pool_size_by_slug
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load theme index for pool sizes: {e}")
|
||||
pool_size_by_slug = {}
|
||||
|
||||
def sort_by_pool_size(theme_list: list[str]) -> list[str]:
|
||||
return sorted(
|
||||
theme_list,
|
||||
key=lambda t: (-pool_size_by_slug.get(slugify(t), 0), t.lower())
|
||||
)
|
||||
|
||||
return sort_by_pool_size(tags), sort_by_pool_size(recommended), pool_size_by_slug
|
||||
|
||||
|
||||
def _section_themes_by_pool_size(themes: list[str], pool_size: dict[str, int]) -> list[dict[str, Any]]:
|
||||
"""Group themes into sections by pool size.
|
||||
|
||||
Thresholds: Vast ≥1000, Large 500-999, Moderate 200-499, Small 50-199, Tiny <50
|
||||
"""
|
||||
sections = [
|
||||
{"label": "Vast", "min": 1000, "max": 9999999, "themes": []},
|
||||
{"label": "Large", "min": 500, "max": 999, "themes": []},
|
||||
{"label": "Moderate", "min": 200, "max": 499, "themes": []},
|
||||
{"label": "Small", "min": 50, "max": 199, "themes": []},
|
||||
{"label": "Tiny", "min": 0, "max": 49, "themes": []},
|
||||
]
|
||||
for theme in themes:
|
||||
theme_pool = pool_size.get(slugify(theme), 0)
|
||||
for section in sections:
|
||||
if section["min"] <= theme_pool <= section["max"]:
|
||||
section["themes"].append(theme)
|
||||
break
|
||||
return [s for s in sections if s["themes"]]
|
||||
|
||||
|
||||
_INVALID_THEME_MESSAGE = (
|
||||
"Theme names can only include letters, numbers, spaces, hyphens, apostrophes, and underscores."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Extracted from build.py as part of Phase 3 modularization (Roadmap 9 M1).
|
|||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request, Form, Query
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from typing import Any
|
||||
|
||||
from ..app import templates, ENABLE_PARTNER_MECHANICS, THEME_POOL_SECTIONS
|
||||
|
|
@ -182,25 +182,12 @@ def _get_current_deck_names(sess: dict) -> list[str]:
|
|||
|
||||
@router.get("/step1", response_class=HTMLResponse)
|
||||
async def build_step1(request: Request) -> HTMLResponse:
|
||||
"""Display commander search form."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
sess["last_step"] = 1
|
||||
resp = templates.TemplateResponse("build/_step1.html", {"request": request, "candidates": []})
|
||||
resp.set_cookie("sid", sid, httponly=True, samesite="lax")
|
||||
return resp
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
|
||||
|
||||
@router.post("/step1", response_class=HTMLResponse)
|
||||
async def build_step1_search(
|
||||
request: Request,
|
||||
query: str = Form(""),
|
||||
auto: str | None = Form(None),
|
||||
active: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Search for commander candidates and optionally auto-select."""
|
||||
query = (query or "").strip()
|
||||
auto_enabled = True if (auto == "1") else False
|
||||
async def build_step1_search(request: Request) -> HTMLResponse:
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
candidates = []
|
||||
if query:
|
||||
candidates = orch.commander_candidates(query, limit=10)
|
||||
|
|
@ -275,11 +262,8 @@ async def build_step1_search(
|
|||
|
||||
|
||||
@router.post("/step1/inspect", response_class=HTMLResponse)
|
||||
async def build_step1_inspect(request: Request, name: str = Form(...)) -> HTMLResponse:
|
||||
"""Preview commander details before confirmation."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
sess["last_step"] = 1
|
||||
async def build_step1_inspect(request: Request) -> HTMLResponse:
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
info = orch.commander_inspect(name)
|
||||
resp = templates.TemplateResponse(
|
||||
"build/_step1.html",
|
||||
|
|
@ -290,9 +274,8 @@ async def build_step1_inspect(request: Request, name: str = Form(...)) -> HTMLRe
|
|||
|
||||
|
||||
@router.post("/step1/confirm", response_class=HTMLResponse)
|
||||
async def build_step1_confirm(request: Request, name: str = Form(...)) -> HTMLResponse:
|
||||
"""Confirm commander selection and proceed to step 2."""
|
||||
res = orch.commander_select(name)
|
||||
async def build_step1_confirm(request: Request) -> HTMLResponse:
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
if not res.get("ok"):
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
|
|
@ -381,23 +364,7 @@ async def build_step1_confirm(request: Request, name: str = Form(...)) -> HTMLRe
|
|||
|
||||
@router.post("/reset-all", response_class=HTMLResponse)
|
||||
async def build_reset_all(request: Request) -> HTMLResponse:
|
||||
"""Clear all build-related session state and return Step 1."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
keys = [
|
||||
"commander","tags","tag_mode","bracket","ideals","build_ctx","last_step",
|
||||
"locks","replace_mode"
|
||||
]
|
||||
for k in keys:
|
||||
try:
|
||||
if k in sess:
|
||||
del sess[k]
|
||||
except Exception:
|
||||
pass
|
||||
sess["last_step"] = 1
|
||||
resp = templates.TemplateResponse("build/_step1.html", {"request": request, "candidates": []})
|
||||
resp.set_cookie("sid", sid, httponly=True, samesite="lax")
|
||||
return resp
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
|
@ -406,11 +373,7 @@ async def build_reset_all(request: Request) -> HTMLResponse:
|
|||
|
||||
@router.get("/step2", response_class=HTMLResponse)
|
||||
async def build_step2_get(request: Request) -> HTMLResponse:
|
||||
"""Display theme picker and partner selection."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
sess["last_step"] = 2
|
||||
commander = sess.get("commander")
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
if not commander:
|
||||
# Fallback to step1 if no commander in session
|
||||
resp = templates.TemplateResponse("build/_step1.html", {"request": request, "candidates": []})
|
||||
|
|
@ -513,24 +476,8 @@ async def build_step2_get(request: Request) -> HTMLResponse:
|
|||
|
||||
|
||||
@router.post("/step2", response_class=HTMLResponse)
|
||||
async def build_step2_submit(
|
||||
request: Request,
|
||||
commander: str = Form(...),
|
||||
primary_tag: str | None = Form(None),
|
||||
secondary_tag: str | None = Form(None),
|
||||
tertiary_tag: str | None = Form(None),
|
||||
tag_mode: str | None = Form("AND"),
|
||||
bracket: int = Form(...),
|
||||
partner_enabled: str | None = Form(None),
|
||||
secondary_commander: str | None = Form(None),
|
||||
background: str | None = Form(None),
|
||||
partner_selection_source: str | None = Form(None),
|
||||
partner_auto_opt_out: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Submit theme and partner selections, proceed to step 3."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
sess["last_step"] = 2
|
||||
async def build_step2_submit(request: Request) -> HTMLResponse:
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
|
||||
partner_feature_enabled = ENABLE_PARTNER_MECHANICS
|
||||
partner_flag = False
|
||||
|
|
@ -776,11 +723,7 @@ async def build_step2_submit(
|
|||
|
||||
@router.get("/step3", response_class=HTMLResponse)
|
||||
async def build_step3_get(request: Request) -> HTMLResponse:
|
||||
"""Display ideal card count sliders."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
sess["last_step"] = 3
|
||||
defaults = orch.ideal_defaults()
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
values = sess.get("ideals") or defaults
|
||||
|
||||
# Check if any skip flags are enabled to show skeleton automation page
|
||||
|
|
@ -850,19 +793,8 @@ async def build_step3_get(request: Request) -> HTMLResponse:
|
|||
|
||||
|
||||
@router.post("/step3", response_class=HTMLResponse)
|
||||
async def build_step3_submit(
|
||||
request: Request,
|
||||
ramp: int = Form(...),
|
||||
lands: int = Form(...),
|
||||
basic_lands: int = Form(...),
|
||||
creatures: int = Form(...),
|
||||
removal: int = Form(...),
|
||||
wipes: int = Form(...),
|
||||
card_advantage: int = Form(...),
|
||||
protection: int = Form(...),
|
||||
) -> HTMLResponse:
|
||||
"""Submit ideal card counts, proceed to step 4."""
|
||||
labels = orch.ideal_labels()
|
||||
async def build_step3_submit(request: Request) -> HTMLResponse:
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
submitted = {
|
||||
"ramp": ramp,
|
||||
"lands": lands,
|
||||
|
|
@ -944,11 +876,7 @@ async def build_step3_submit(
|
|||
|
||||
@router.get("/step4", response_class=HTMLResponse)
|
||||
async def build_step4_get(request: Request) -> HTMLResponse:
|
||||
"""Display review page with owned card preferences."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
sess["last_step"] = 4
|
||||
labels = orch.ideal_labels()
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
values = sess.get("ideals") or orch.ideal_defaults()
|
||||
commander = sess.get("commander")
|
||||
return templates.TemplateResponse(
|
||||
|
|
@ -966,17 +894,8 @@ async def build_step4_get(request: Request) -> HTMLResponse:
|
|||
|
||||
|
||||
@router.post("/toggle-owned-review", response_class=HTMLResponse)
|
||||
async def build_toggle_owned_review(
|
||||
request: Request,
|
||||
use_owned_only: str | None = Form(None),
|
||||
prefer_owned: str | None = Form(None),
|
||||
swap_mdfc_basics: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Toggle 'use owned only' and/or 'prefer owned' flags from the Review step and re-render Step 4."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
sess["last_step"] = 4
|
||||
only_val = True if (use_owned_only and str(use_owned_only).strip() in ("1","true","on","yes")) else False
|
||||
async def build_toggle_owned_review(request: Request) -> HTMLResponse:
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
pref_val = True if (prefer_owned and str(prefer_owned).strip() in ("1","true","on","yes")) else False
|
||||
swap_val = True if (swap_mdfc_basics and str(swap_mdfc_basics).strip() in ("1","true","on","yes")) else False
|
||||
sess["use_owned_only"] = only_val
|
||||
|
|
@ -1024,19 +943,12 @@ async def build_step5_get(request: Request) -> HTMLResponse:
|
|||
|
||||
@router.get("/step5/start", response_class=HTMLResponse)
|
||||
async def build_step5_start_get(request: Request) -> HTMLResponse:
|
||||
"""Allow GET as a fallback to start the build (delegates to POST handler)."""
|
||||
return await build_step5_start(request)
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
|
||||
|
||||
@router.post("/step5/start", response_class=HTMLResponse)
|
||||
async def build_step5_start(request: Request) -> HTMLResponse:
|
||||
"""Initialize build context and run first stage."""
|
||||
sid = request.cookies.get("sid") or new_sid()
|
||||
sess = get_session(sid)
|
||||
if "replace_mode" not in sess:
|
||||
sess["replace_mode"] = True
|
||||
# Validate commander exists before starting
|
||||
commander = sess.get("commander")
|
||||
return RedirectResponse("/build", status_code=302)
|
||||
if not commander:
|
||||
resp = templates.TemplateResponse(
|
||||
"build/_step1.html",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue