fix(admin): protect cornerstone .md files from bulk delete + fix PDF 500
- Bulk delete (both explicit-IDs and apply_to_all paths) now only unlinks source .md files for generated articles (template_slug IS NOT NULL). Manual cornerstone articles keep their .md source on disk. - _sync_static_articles() now also renders markdown → HTML and writes to BUILD_DIR/<lang>/<slug>.html after upserting the DB row, so cornerstones are immediately servable after a sync without a separate rebuild step. - scenario_pdf(): replace d = json.loads(scenario["calc_json"]) with d = calc(state) so all current calc fields (moic, dscr, cashOnCash, …) are present and the PDF route no longer 500s on stale stored JSON. - Restored data/content/articles/ cornerstone .md files via git checkout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||||
@@ -2601,12 +2613,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()
|
||||||
@@ -2655,13 +2670,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()
|
||||||
|
|||||||
Reference in New Issue
Block a user