fix(admin): protect cornerstone .md files from bulk delete + fix PDF 500
All checks were successful
CI / test (push) Successful in 1m0s
CI / tag (push) Successful in 3s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-03-07 11:13:23 +01:00

View File

@@ -2142,7 +2142,7 @@ async def scenario_preview(scenario_id: int):
async def scenario_pdf(scenario_id: int): async def scenario_pdf(scenario_id: int):
"""Generate and immediately download a business plan PDF for a published scenario.""" """Generate and immediately download a business plan PDF for a published scenario."""
from ..businessplan import get_plan_sections from ..businessplan import get_plan_sections
from ..planner.calculator import validate_state from ..planner.calculator import calc, validate_state
scenario = await fetch_one("SELECT * FROM published_scenarios WHERE id = ?", (scenario_id,)) scenario = await fetch_one("SELECT * FROM published_scenarios WHERE id = ?", (scenario_id,))
if not scenario: if not scenario:
@@ -2153,7 +2153,7 @@ async def scenario_pdf(scenario_id: int):
lang = "en" lang = "en"
state = validate_state(json.loads(scenario["state_json"])) state = validate_state(json.loads(scenario["state_json"]))
d = json.loads(scenario["calc_json"]) d = calc(state)
sections = get_plan_sections(state, d, lang) sections = get_plan_sections(state, d, lang)
sections["scenario_name"] = scenario["title"] sections["scenario_name"] = scenario["title"]
sections["location"] = scenario.get("location", "") sections["location"] = scenario.get("location", "")
@@ -2274,6 +2274,18 @@ async def _sync_static_articles() -> None:
template_slug, group_key, now_iso, now_iso), template_slug, group_key, now_iso, now_iso),
) )
# Build HTML so the article is immediately servable (cornerstones have no template)
if template_slug is None:
from ..content.routes import BUILD_DIR, bake_product_cards, bake_scenario_cards
body = raw[m.end():]
body_html = mistune.html(body)
body_html = await bake_scenario_cards(body_html, lang=language)
body_html = await bake_product_cards(body_html, lang=language)
build_dir = BUILD_DIR / language
build_dir.mkdir(parents=True, exist_ok=True)
(build_dir / f"{slug}.html").write_text(body_html)
def _build_article_where( def _build_article_where(
status: str = None, status: str = None,
@@ -2613,12 +2625,15 @@ async def articles_bulk():
from ..content.routes import BUILD_DIR from ..content.routes import BUILD_DIR
rows = await fetch_all( rows = await fetch_all(
f"SELECT id, slug FROM articles WHERE {where} LIMIT 5000", tuple(where_params) f"SELECT id, slug, template_slug FROM articles WHERE {where} LIMIT 5000",
tuple(where_params),
) )
for a in rows: for a in rows:
build_path = BUILD_DIR / f"{a['slug']}.html" build_path = BUILD_DIR / f"{a['slug']}.html"
if build_path.exists(): if build_path.exists():
build_path.unlink() build_path.unlink()
# Only remove source .md for generated articles; cornerstones have no template
if a["template_slug"] is not None:
md_path = Path("data/content/articles") / f"{a['slug']}.md" md_path = Path("data/content/articles") / f"{a['slug']}.md"
if md_path.exists(): if md_path.exists():
md_path.unlink() md_path.unlink()
@@ -2667,13 +2682,15 @@ async def articles_bulk():
from ..content.routes import BUILD_DIR from ..content.routes import BUILD_DIR
articles_rows = await fetch_all( articles_rows = await fetch_all(
f"SELECT id, slug FROM articles WHERE id IN ({placeholders})", f"SELECT id, slug, template_slug FROM articles WHERE id IN ({placeholders})",
tuple(article_ids), tuple(article_ids),
) )
for a in articles_rows: for a in articles_rows:
build_path = BUILD_DIR / f"{a['slug']}.html" build_path = BUILD_DIR / f"{a['slug']}.html"
if build_path.exists(): if build_path.exists():
build_path.unlink() build_path.unlink()
# Only remove source .md for generated articles; cornerstones have no template
if a["template_slug"] is not None:
md_path = Path("data/content/articles") / f"{a['slug']}.md" md_path = Path("data/content/articles") / f"{a['slug']}.md"
if md_path.exists(): if md_path.exists():
md_path.unlink() md_path.unlink()