Implement newsletter scheduling features
- Added a background scheduler to manage newsletter dispatch based on user-defined settings. - Enhanced the SMTP settings to include scheduling options such as frequency, time, and specific weekdays. - Updated the dashboard UI to allow users to configure scheduling preferences for automated newsletter sending. - Modified the newsletter generation logic to accommodate new scheduling parameters and ensure proper content delivery.
This commit is contained in:
107
app/main.py
107
app/main.py
@@ -1,3 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import ValidationError
|
||||
@@ -33,7 +36,7 @@ from app.services.newsletter import (
|
||||
resolve_period,
|
||||
split_articles_by_type,
|
||||
)
|
||||
from app.services.smtp_sender import get_smtp_settings, run_scheduled_send_if_due, send_newsletter
|
||||
from app.services.smtp_sender import get_smtp_settings, mark_scheduled_sent, schedule_is_due, send_newsletter
|
||||
from app.services.wiki import WikiFetchError, WikiService
|
||||
|
||||
app = FastAPI(title=settings.app_name)
|
||||
@@ -45,6 +48,10 @@ templates = Jinja2Templates(directory="app/templates")
|
||||
templates.env.globals["wiki_article_url"] = article_url
|
||||
wiki_service = WikiService()
|
||||
|
||||
logger = logging.getLogger("newsletter.scheduler")
|
||||
SCHEDULER_INTERVAL_SECONDS = 60
|
||||
_scheduler_task: asyncio.Task | None = None
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup() -> None:
|
||||
@@ -52,6 +59,59 @@ def on_startup() -> None:
|
||||
Base.metadata.create_all(bind=engine)
|
||||
_ensure_schema_upgrades()
|
||||
_bootstrap_admin()
|
||||
global _scheduler_task
|
||||
_scheduler_task = asyncio.create_task(_scheduler_loop())
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def on_shutdown() -> None:
|
||||
if _scheduler_task is not None:
|
||||
_scheduler_task.cancel()
|
||||
|
||||
|
||||
async def _scheduler_loop() -> None:
|
||||
"""Prüft periodisch, ob laut Zeitplan ein Newsletter-Versand fällig ist."""
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(SCHEDULER_INTERVAL_SECONDS)
|
||||
await _run_scheduled_send_if_due()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
logger.exception("Fehler im Scheduler-Loop")
|
||||
|
||||
|
||||
async def _run_scheduled_send_if_due() -> None:
|
||||
db = next(get_db())
|
||||
try:
|
||||
settings_snapshot = get_smtp_settings(db)
|
||||
now = datetime.now()
|
||||
if not schedule_is_due(settings_snapshot, now):
|
||||
return
|
||||
|
||||
try:
|
||||
days = int(settings_snapshot.get("gen_days") or "30")
|
||||
except ValueError:
|
||||
days = 30
|
||||
gen = await _generate_newsletter(
|
||||
period_mode=settings_snapshot.get("gen_period", "days"),
|
||||
days=max(1, min(days, 90)),
|
||||
article_type=settings_snapshot.get("gen_article_type", "all"),
|
||||
category=settings_snapshot.get("gen_category", ""),
|
||||
display=DEFAULT_DISPLAY,
|
||||
editor_tip="",
|
||||
highlight_list=[],
|
||||
)
|
||||
if gen["wiki_error"] or not gen["articles"]:
|
||||
# Kein Versand ohne Inhalt; erneuter Versuch beim nächsten Intervall.
|
||||
logger.info("Geplanter Versand übersprungen (kein Inhalt/Wiki-Fehler).")
|
||||
return
|
||||
|
||||
recipients = [r.strip() for r in settings_snapshot.get("recipients", "").split(",") if r.strip()]
|
||||
send_newsletter(db, gen["subject"], gen["raw_html"], gen["plain_text"], recipients, scheduled=True)
|
||||
mark_scheduled_sent(db, now)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _ensure_schema_upgrades() -> None:
|
||||
@@ -218,8 +278,8 @@ def logout(request: Request, csrf_token: str = Form(...)):
|
||||
def _empty_filters() -> dict:
|
||||
return {
|
||||
"days": 30,
|
||||
"period": "days",
|
||||
"article_type": "all",
|
||||
"period": "last_month",
|
||||
"article_type": "new",
|
||||
"category": "",
|
||||
"display": DEFAULT_DISPLAY,
|
||||
"editor_tip": "",
|
||||
@@ -261,10 +321,10 @@ async def _generate_newsletter(
|
||||
"articles": filtered,
|
||||
"articles_new": new_articles,
|
||||
"articles_edited": edited_articles,
|
||||
"subject": create_subject(p["days"], category, p["label"]),
|
||||
"subject": create_subject(p["month_label"], p["days"]),
|
||||
"plain_text": create_plain_text(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type),
|
||||
"raw_html": create_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type),
|
||||
"outlook_html": create_outlook_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type),
|
||||
"raw_html": create_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"]),
|
||||
"outlook_html": create_outlook_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"]),
|
||||
}
|
||||
)
|
||||
except WikiFetchError as exc:
|
||||
@@ -324,8 +384,6 @@ async def dashboard_generate(
|
||||
editor_tip=editor_tip,
|
||||
highlight_list=highlight_list,
|
||||
)
|
||||
if not gen["wiki_error"]:
|
||||
run_scheduled_send_if_due(db, gen["subject"], gen["raw_html"], gen["plain_text"])
|
||||
|
||||
context = _base_context(request, current_user, db)
|
||||
context.update(
|
||||
@@ -436,7 +494,15 @@ def update_smtp_settings(
|
||||
from_email: str = Form(""),
|
||||
use_tls: str | None = Form(None),
|
||||
recipients: str = Form(""),
|
||||
schedule_enabled: str | None = Form(None),
|
||||
schedule_frequency: str = Form("monthly"),
|
||||
schedule_time: str = Form("08:00"),
|
||||
schedule_weekdays: list[str] = Form(default=[]),
|
||||
schedule_dom: str = Form("1"),
|
||||
gen_period: str = Form("days"),
|
||||
gen_days: str = Form("30"),
|
||||
gen_article_type: str = Form("all"),
|
||||
gen_category: str = Form(""),
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
@@ -449,7 +515,32 @@ def update_smtp_settings(
|
||||
set_config(db, "smtp.from_email", from_email.strip())
|
||||
set_config(db, "smtp.use_tls", "true" if use_tls == "true" else "false")
|
||||
set_config(db, "smtp.recipients", recipients.strip())
|
||||
|
||||
set_config(db, "smtp.schedule_enabled", "true" if schedule_enabled == "true" else "false")
|
||||
if schedule_frequency not in ("daily", "weekly", "monthly"):
|
||||
schedule_frequency = "monthly"
|
||||
set_config(db, "smtp.schedule_frequency", schedule_frequency)
|
||||
set_config(db, "smtp.schedule_time", schedule_time.strip() or "08:00")
|
||||
valid_weekdays = [d for d in schedule_weekdays if d in {"0", "1", "2", "3", "4", "5", "6"}]
|
||||
set_config(db, "smtp.schedule_weekdays", ",".join(valid_weekdays))
|
||||
try:
|
||||
dom = max(1, min(int(schedule_dom), 28))
|
||||
except ValueError:
|
||||
dom = 1
|
||||
set_config(db, "smtp.schedule_dom", str(dom))
|
||||
|
||||
if gen_period not in ("days", "last_month"):
|
||||
gen_period = "days"
|
||||
set_config(db, "smtp.gen_period", gen_period)
|
||||
try:
|
||||
gdays = max(1, min(int(gen_days), 90))
|
||||
except ValueError:
|
||||
gdays = 30
|
||||
set_config(db, "smtp.gen_days", str(gdays))
|
||||
if gen_article_type not in ("all", "new", "edited"):
|
||||
gen_article_type = "all"
|
||||
set_config(db, "smtp.gen_article_type", gen_article_type)
|
||||
set_config(db, "smtp.gen_category", gen_category.strip())
|
||||
return RedirectResponse(url="/dashboard", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user