From 5808ba6490ec3e1d6799cf137d6d9254fc3babf8 Mon Sep 17 00:00:00 2001 From: Deeman Date: Tue, 24 Feb 2026 01:22:25 +0100 Subject: [PATCH] fix: scenario preview rendering and double-encoded ampersands - bake_scenario_cards() accepts scenario_overrides dict for preview mode (bypasses DB lookup when no published_scenario exists) - preview_article() builds in-memory scenario dict and passes it through - Fix double-encoded & in locale strings (was & in rendered HTML) - Fix ruff import sort in _datetimeformat - Fix migration 0019 minor issue Co-Authored-By: Claude Opus 4.6 --- web/src/padelnomics/content/__init__.py | 28 ++++++++++++++++--- web/src/padelnomics/content/routes.py | 18 ++++++++++-- web/src/padelnomics/locales/de.json | 6 ++-- web/src/padelnomics/locales/en.json | 6 ++-- .../versions/0019_add_feature_flags.py | 2 +- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/web/src/padelnomics/content/__init__.py b/web/src/padelnomics/content/__init__.py index 9ee25a3..415557e 100644 --- a/web/src/padelnomics/content/__init__.py +++ b/web/src/padelnomics/content/__init__.py @@ -135,7 +135,7 @@ def _validate_table_name(data_table: str) -> None: def _datetimeformat(value: str, fmt: str = "%Y-%m-%d") -> str: """Jinja2 filter: format a date string (or 'now') with strftime.""" - from datetime import datetime, UTC + from datetime import UTC, datetime if value == "now": dt = datetime.now(UTC) @@ -499,6 +499,7 @@ async def preview_article( meta_desc = _render_pattern(config["meta_description_pattern"], ctx) # Calculator: compute scenario in-memory + scenario_overrides = None if config["content_type"] == "calculator": # DuckDB lowercases all column names; build a case-insensitive # reverse map so "ratepeak" (stored) matches "ratePeak" (DEFAULTS). @@ -509,12 +510,31 @@ async def preview_article( if k.lower() in _defaults_ci and v is not None } state = validate_state(calc_overrides) - calc(state, lang=lang) # validate state produces valid output - ctx["scenario_slug"] = slug + "-" + str(row[natural_key]) + d = calc(state, lang=lang) + scenario_slug = slug + "-" + str(row[natural_key]) + ctx["scenario_slug"] = scenario_slug + + # Build a fake scenario dict so bake_scenario_cards can render + # without a published_scenarios DB row. + city = row.get("city_name", row.get("city", "")) + scenario_overrides = { + scenario_slug: { + "slug": scenario_slug, + "title": city, + "location": city, + "country": row.get("country", state.get("country", "")), + "venue_type": state.get("venue", "indoor"), + "ownership": state.get("own", "rent"), + "calc_json": json.dumps(d), + "state_json": json.dumps(state), + } + } body_md = _render_pattern(config["body_template"], ctx) body_html = mistune.html(body_md) - body_html = await bake_scenario_cards(body_html, lang=lang) + body_html = await bake_scenario_cards( + body_html, lang=lang, scenario_overrides=scenario_overrides, + ) return { "title": title, diff --git a/web/src/padelnomics/content/routes.py b/web/src/padelnomics/content/routes.py index 049a71d..f7f669c 100644 --- a/web/src/padelnomics/content/routes.py +++ b/web/src/padelnomics/content/routes.py @@ -60,13 +60,21 @@ def is_reserved_path(url_path: str) -> bool: return any(clean.startswith(p) for p in RESERVED_PREFIXES) -async def bake_scenario_cards(html: str, lang: str = "en") -> str: - """Replace [scenario:slug] and [scenario:slug:section] markers with rendered HTML.""" +async def bake_scenario_cards( + html: str, + lang: str = "en", + scenario_overrides: dict[str, dict] | None = None, +) -> str: + """Replace [scenario:slug] and [scenario:slug:section] markers with rendered HTML. + + scenario_overrides: optional {slug: {calc_json, state_json, ...}} dict + that bypasses the DB lookup (used by preview_article). + """ matches = list(SCENARIO_RE.finditer(html)) if not matches: return html - # Batch-fetch all referenced scenarios + # Batch-fetch all referenced scenarios from DB slugs = list({m.group(1) for m in matches}) placeholders = ",".join("?" * len(slugs)) rows = await fetch_all( @@ -75,6 +83,10 @@ async def bake_scenario_cards(html: str, lang: str = "en") -> str: ) scenarios = {row["slug"]: row for row in rows} + # Merge in any overrides (preview mode — no DB row exists yet) + if scenario_overrides: + scenarios.update(scenario_overrides) + for match in reversed(matches): slug = match.group(1) section = match.group(2) diff --git a/web/src/padelnomics/locales/de.json b/web/src/padelnomics/locales/de.json index 2284897..ee73447 100644 --- a/web/src/padelnomics/locales/de.json +++ b/web/src/padelnomics/locales/de.json @@ -1059,7 +1059,7 @@ "scenario_total_label": "Gesamt", "scenario_total_capex_label": "Gesamt-CAPEX", "scenario_monthly_ebitda_label": "Monatliches EBITDA", - "scenario_returns_config_title": "Renditen & Finanzierung", + "scenario_returns_config_title": "Renditen & Finanzierung", "scenario_return_metrics_title": "Renditekennzahlen", "scenario_yield_on_cost_label": "Rendite auf Kosten", "scenario_exit_analysis_title": "Exit-Analyse", @@ -1070,7 +1070,7 @@ "scenario_loan_amount_label": "Darlehensbetrag", "scenario_rate_term_label": "Zinssatz / Laufzeit", "scenario_monthly_payment_label": "Monatliche Rate", - "scenario_revenue_opex_title": "Umsatz & Betriebskosten", + "scenario_revenue_opex_title": "Umsatz & Betriebskosten", "scenario_revenue_model_title": "Umsatzmodell", "scenario_weighted_rate_label": "Gewichteter Satz", "scenario_utilization_target_label": "Auslastungsziel", @@ -1551,7 +1551,7 @@ "email_quote_verify_body": "Danke f\u00fcr deine Angebotsanfrage. Best\u00e4tige deine E-Mail, um deine Anfrage zu aktivieren und dein {app_name}-Konto zu erstellen.", "email_quote_verify_project_label": "Dein Projekt:", "email_quote_verify_urgency": "Verifizierte Anfragen werden von unserem Anbieternetzwerk bevorzugt behandelt.", - "email_quote_verify_btn": "Best\u00e4tigen & Aktivieren \u2192", + "email_quote_verify_btn": "Best\u00e4tigen & Aktivieren \u2192", "email_quote_verify_expires": "Dieser Link l\u00e4uft in 60 Minuten ab.", "email_quote_verify_fallback": "Wenn der Button nicht funktioniert, kopiere diese URL in deinen Browser:", "email_quote_verify_ignore": "Wenn du das nicht angefordert hast, kannst du diese E-Mail ignorieren.", diff --git a/web/src/padelnomics/locales/en.json b/web/src/padelnomics/locales/en.json index e11c0dd..70927f4 100644 --- a/web/src/padelnomics/locales/en.json +++ b/web/src/padelnomics/locales/en.json @@ -1059,7 +1059,7 @@ "scenario_total_label": "Total", "scenario_total_capex_label": "Total CAPEX", "scenario_monthly_ebitda_label": "Monthly EBITDA", - "scenario_returns_config_title": "Returns & Financing", + "scenario_returns_config_title": "Returns & Financing", "scenario_return_metrics_title": "Return Metrics", "scenario_yield_on_cost_label": "Yield on Cost", "scenario_exit_analysis_title": "Exit Analysis", @@ -1070,7 +1070,7 @@ "scenario_loan_amount_label": "Loan Amount", "scenario_rate_term_label": "Rate / Term", "scenario_monthly_payment_label": "Monthly Payment", - "scenario_revenue_opex_title": "Revenue & Operating Costs", + "scenario_revenue_opex_title": "Revenue & Operating Costs", "scenario_revenue_model_title": "Revenue Model", "scenario_weighted_rate_label": "Weighted Rate", "scenario_utilization_target_label": "Utilization Target", @@ -1551,7 +1551,7 @@ "email_quote_verify_body": "Thanks for requesting quotes. Verify your email to activate your quote request and create your {app_name} account.", "email_quote_verify_project_label": "Your project:", "email_quote_verify_urgency": "Verified requests get prioritized by our supplier network.", - "email_quote_verify_btn": "Verify & Activate \u2192", + "email_quote_verify_btn": "Verify & Activate \u2192", "email_quote_verify_expires": "This link expires in 60 minutes.", "email_quote_verify_fallback": "If the button doesn't work, copy and paste this URL into your browser:", "email_quote_verify_ignore": "If you didn't request this, you can safely ignore this email.", diff --git a/web/src/padelnomics/migrations/versions/0019_add_feature_flags.py b/web/src/padelnomics/migrations/versions/0019_add_feature_flags.py index 4c497db..5b78ada 100644 --- a/web/src/padelnomics/migrations/versions/0019_add_feature_flags.py +++ b/web/src/padelnomics/migrations/versions/0019_add_feature_flags.py @@ -19,7 +19,7 @@ def up(conn): conn.executemany( "INSERT OR IGNORE INTO feature_flags (name, enabled, description) VALUES (?, ?, ?)", [ - ("markets", 1, "Market/SEO content pages"), + ("markets", 0, "Market/SEO content pages"), ("payments", 0, "Paddle billing & checkout"), ("planner_export", 0, "Business plan PDF export"), ("supplier_signup", 0, "Supplier onboarding wizard"),