diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index b323977..f0446d0 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -1314,7 +1314,7 @@ async def template_preview(slug: str, row_key: str): @csrf_protect async def template_generate(slug: str): """Generate articles from template + DuckDB data.""" - from ..content import fetch_template_data, generate_articles, load_template + from ..content import fetch_template_data, load_template try: config = load_template(slug) @@ -1330,15 +1330,20 @@ async def template_generate(slug: str): start_date_str = form.get("start_date", "") articles_per_day = int(form.get("articles_per_day", 3) or 3) - if not start_date_str: - start_date = date.today() - else: - start_date = date.fromisoformat(start_date_str) + start_date = date.fromisoformat(start_date_str) if start_date_str else date.today() - generated = await generate_articles( - slug, start_date, articles_per_day, limit=500, + from ..worker import enqueue + await enqueue("generate_articles", { + "template_slug": slug, + "start_date": start_date.isoformat(), + "articles_per_day": articles_per_day, + "limit": 500, + }) + await flash( + f"Article generation queued for '{config['name']}'. " + "The worker will process it in the background.", + "success", ) - await flash(f"Generated {generated} articles with staggered publish dates.", "success") return redirect(url_for("admin.articles")) return await render_template( @@ -1354,7 +1359,7 @@ async def template_generate(slug: str): @csrf_protect async def template_regenerate(slug: str): """Re-generate all articles for a template with fresh DuckDB data.""" - from ..content import generate_articles, load_template + from ..content import load_template try: load_template(slug) @@ -1362,9 +1367,14 @@ async def template_regenerate(slug: str): await flash("Template not found.", "error") return redirect(url_for("admin.templates")) - # Use today as start date, keep existing publish dates via upsert - generated = await generate_articles(slug, date.today(), articles_per_day=500) - await flash(f"Regenerated {generated} articles from fresh data.", "success") + from ..worker import enqueue + await enqueue("generate_articles", { + "template_slug": slug, + "start_date": date.today().isoformat(), + "articles_per_day": 500, + "limit": 500, + }) + await flash("Regeneration queued. The worker will process it in the background.", "success") return redirect(url_for("admin.template_detail", slug=slug)) @@ -1877,13 +1887,29 @@ async def article_rebuild(article_id: int): @role_required("admin") @csrf_protect async def rebuild_all(): - """Re-render all articles.""" - articles = await fetch_all("SELECT id FROM articles") - count = 0 - for a in articles: + """Re-render all articles via background worker.""" + from ..content import discover_templates + from ..worker import enqueue + + templates = discover_templates() + for t in templates: + await enqueue("generate_articles", { + "template_slug": t["slug"], + "start_date": date.today().isoformat(), + "articles_per_day": 500, + "limit": 500, + }) + + # Manual articles still need inline rebuild + manual = await fetch_all("SELECT id FROM articles WHERE template_slug IS NULL") + for a in manual: await _rebuild_article(a["id"]) - count += 1 - await flash(f"Rebuilt {count} articles.", "success") + + await flash( + f"Queued rebuild for {len(templates)} templates" + f" + rebuilt {len(manual)} manual articles.", + "success", + ) return redirect(url_for("admin.articles")) diff --git a/web/src/padelnomics/worker.py b/web/src/padelnomics/worker.py index 96b48d1..c0d7843 100644 --- a/web/src/padelnomics/worker.py +++ b/web/src/padelnomics/worker.py @@ -709,6 +709,22 @@ async def handle_cleanup_seo_metrics(payload: dict) -> None: print(f"[WORKER] Cleaned up {deleted} old SEO metric rows") +@task("generate_articles") +async def handle_generate_articles(payload: dict) -> None: + """Generate articles from a template in the background.""" + from datetime import date as date_cls + + from .content import generate_articles + + slug = payload["template_slug"] + start_date = date_cls.fromisoformat(payload["start_date"]) + articles_per_day = payload.get("articles_per_day", 3) + limit = payload.get("limit", 500) + + count = await generate_articles(slug, start_date, articles_per_day, limit=limit) + print(f"[WORKER] Generated {count} articles for template '{slug}'") + + # ============================================================================= # Worker Loop # ============================================================================= diff --git a/web/tests/test_content.py b/web/tests/test_content.py index 6476212..bcc238f 100644 --- a/web/tests/test_content.py +++ b/web/tests/test_content.py @@ -1019,7 +1019,7 @@ class TestAdminTemplateGenerate: assert "3" in html # 3 rows available assert "Generate" in html - async def test_generate_creates_articles(self, admin_client, db, pseo_env): + async def test_generate_enqueues_task(self, admin_client, db, pseo_env): async with admin_client.session_transaction() as sess: sess["csrf_token"] = "test" @@ -1030,11 +1030,16 @@ class TestAdminTemplateGenerate: }) assert resp.status_code == 302 - articles = await fetch_all("SELECT * FROM articles") - assert len(articles) == 3 - - scenarios = await fetch_all("SELECT * FROM published_scenarios") - assert len(scenarios) == 3 + # Generation is now queued, not inline + tasks = await fetch_all( + "SELECT * FROM tasks WHERE task_name = 'generate_articles'" + ) + assert len(tasks) == 1 + import json + payload = json.loads(tasks[0]["payload"]) + assert payload["template_slug"] == "test-city" + assert payload["start_date"] == "2026-04-01" + assert payload["articles_per_day"] == 2 async def test_generate_unknown_template_redirects(self, admin_client, db, pseo_env): resp = await admin_client.get("/admin/templates/nonexistent/generate") @@ -1042,13 +1047,7 @@ class TestAdminTemplateGenerate: class TestAdminTemplateRegenerate: - async def test_regenerate_updates_articles(self, admin_client, db, pseo_env): - from padelnomics.content import generate_articles - # First generate - await generate_articles("test-city", date(2026, 3, 1), 10) - initial = await fetch_all("SELECT * FROM articles") - assert len(initial) == 3 - + async def test_regenerate_enqueues_task(self, admin_client, db, pseo_env): async with admin_client.session_transaction() as sess: sess["csrf_token"] = "test" @@ -1057,9 +1056,14 @@ class TestAdminTemplateRegenerate: }) assert resp.status_code == 302 - # Same count — upserted, not duplicated - articles = await fetch_all("SELECT * FROM articles") - assert len(articles) == 3 + # Regeneration is now queued, not inline + tasks = await fetch_all( + "SELECT * FROM tasks WHERE task_name = 'generate_articles'" + ) + assert len(tasks) == 1 + import json + payload = json.loads(tasks[0]["payload"]) + assert payload["template_slug"] == "test-city" async def test_regenerate_unknown_template_redirects(self, admin_client, db, pseo_env): async with admin_client.session_transaction() as sess: