Enhance newsletter generation and dashboard functionality
- Introduce a new period selection feature in the dashboard for generating newsletters, allowing users to choose between "last month" and "last X days." - Refactor newsletter generation logic to support filtering by article type (new, edited, or all) and improve the handling of filters in the dashboard. - Update the dashboard template to include new input fields for period and article type, enhancing user experience. - Improve CSS styles for send notifications and sections in the dashboard, providing clearer feedback on newsletter sending status.
This commit is contained in:
199
app/main.py
199
app/main.py
@@ -30,6 +30,7 @@ from app.services.newsletter import (
|
||||
filter_articles,
|
||||
parse_display_options,
|
||||
parse_highlights,
|
||||
resolve_period,
|
||||
split_articles_by_type,
|
||||
)
|
||||
from app.services.smtp_sender import get_smtp_settings, run_scheduled_send_if_due, send_newsletter
|
||||
@@ -72,7 +73,14 @@ async def add_security_headers(request: Request, call_next):
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
response.headers["Referrer-Policy"] = "same-origin"
|
||||
response.headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self'; script-src 'self';"
|
||||
# Die Newsletter-Vorschau läuft in einem srcdoc-iframe und erbt diese CSP.
|
||||
# E-Mail-HTML benötigt zwingend Inline-Styles; das Logo liegt auf einem externen https-Host.
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
"default-src 'self'; "
|
||||
"img-src 'self' https: data:; "
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"script-src 'self';"
|
||||
)
|
||||
if not cookie_token:
|
||||
response.set_cookie(
|
||||
CSRF_COOKIE_NAME,
|
||||
@@ -199,6 +207,63 @@ def logout(request: Request, csrf_token: str = Form(...)):
|
||||
return response
|
||||
|
||||
|
||||
def _empty_filters() -> dict:
|
||||
return {
|
||||
"days": 30,
|
||||
"period": "days",
|
||||
"article_type": "all",
|
||||
"category": "",
|
||||
"display": DEFAULT_DISPLAY,
|
||||
"editor_tip": "",
|
||||
"highlights": "",
|
||||
}
|
||||
|
||||
|
||||
async def _generate_newsletter(
|
||||
*,
|
||||
period_mode: str,
|
||||
days: int,
|
||||
article_type: str,
|
||||
category: str,
|
||||
display,
|
||||
editor_tip: str,
|
||||
highlight_list: list[str],
|
||||
) -> dict:
|
||||
p = resolve_period(period_mode, days)
|
||||
result = {
|
||||
"period": p,
|
||||
"wiki_error": None,
|
||||
"articles": [],
|
||||
"articles_new": [],
|
||||
"articles_edited": [],
|
||||
"plain_text": "",
|
||||
"raw_html": "",
|
||||
"outlook_html": "",
|
||||
"subject": "",
|
||||
}
|
||||
try:
|
||||
articles = await wiki_service.get_recent_changes(
|
||||
days=p["days"], article_type=article_type, start=p["start"], end=p["end"]
|
||||
)
|
||||
filtered = filter_articles(articles, category_filter=category or None)
|
||||
new_articles, edited_articles = split_articles_by_type(filtered)
|
||||
prange = (p["start_str"], p["end_str"])
|
||||
result.update(
|
||||
{
|
||||
"articles": filtered,
|
||||
"articles_new": new_articles,
|
||||
"articles_edited": edited_articles,
|
||||
"subject": create_subject(p["days"], category, p["label"]),
|
||||
"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),
|
||||
}
|
||||
)
|
||||
except WikiFetchError as exc:
|
||||
result["wiki_error"] = exc.message
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/dashboard", response_class=HTMLResponse)
|
||||
async def dashboard(request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
context = _base_context(request, current_user, db)
|
||||
@@ -209,15 +274,10 @@ async def dashboard(request: Request, db: Session = Depends(get_db), current_use
|
||||
"raw_html": "",
|
||||
"outlook_html": "",
|
||||
"plain_text": "",
|
||||
"filters": {
|
||||
"days": 30,
|
||||
"only_edited": False,
|
||||
"category": "",
|
||||
"display": DEFAULT_DISPLAY,
|
||||
"editor_tip": "",
|
||||
"highlights": "",
|
||||
},
|
||||
"subject": "",
|
||||
"filters": _empty_filters(),
|
||||
"wiki_error": None,
|
||||
"send_result": None,
|
||||
})
|
||||
logs = db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all()
|
||||
context["send_logs"] = logs
|
||||
@@ -228,8 +288,9 @@ async def dashboard(request: Request, db: Session = Depends(get_db), current_use
|
||||
async def dashboard_generate(
|
||||
request: Request,
|
||||
csrf_token: str = Form(...),
|
||||
period: str = Form("days"),
|
||||
days: int = Form(30),
|
||||
only_edited: str | None = Form(None),
|
||||
article_type: str = Form("all"),
|
||||
category: str = Form(""),
|
||||
show_date: str | None = Form(None),
|
||||
show_user: str | None = Form(None),
|
||||
@@ -241,57 +302,57 @@ async def dashboard_generate(
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
days = max(1, min(days, 90))
|
||||
only_edited_flag = only_edited == "true"
|
||||
if article_type not in ("all", "new", "edited"):
|
||||
article_type = "all"
|
||||
display = parse_display_options(show_date, show_user, show_category)
|
||||
highlight_list = parse_highlights(highlights)
|
||||
wiki_error = None
|
||||
filtered: list = []
|
||||
articles_new: list = []
|
||||
articles_edited: list = []
|
||||
plain_text = ""
|
||||
raw_html = ""
|
||||
outlook_html = ""
|
||||
try:
|
||||
articles = await wiki_service.get_recent_changes(days=days, only_edited=only_edited_flag)
|
||||
filtered = filter_articles(articles, category_filter=category or None)
|
||||
articles_new, articles_edited = split_articles_by_type(filtered)
|
||||
title = create_subject(days, category)
|
||||
plain_text = create_plain_text(filtered, days, display, category, editor_tip, highlight_list)
|
||||
raw_html = create_html(filtered, days, display, category, editor_tip, highlight_list)
|
||||
outlook_html = create_outlook_html(filtered, days, display, category, editor_tip, highlight_list)
|
||||
run_scheduled_send_if_due(db, title, raw_html, plain_text)
|
||||
except WikiFetchError as exc:
|
||||
wiki_error = exc.message
|
||||
|
||||
gen = await _generate_newsletter(
|
||||
period_mode=period,
|
||||
days=days,
|
||||
article_type=article_type,
|
||||
category=category,
|
||||
display=display,
|
||||
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(
|
||||
{
|
||||
"articles": filtered,
|
||||
"articles_new": articles_new,
|
||||
"articles_edited": articles_edited,
|
||||
"raw_html": raw_html,
|
||||
"outlook_html": outlook_html,
|
||||
"plain_text": plain_text,
|
||||
"articles": gen["articles"],
|
||||
"articles_new": gen["articles_new"],
|
||||
"articles_edited": gen["articles_edited"],
|
||||
"raw_html": gen["raw_html"],
|
||||
"outlook_html": gen["outlook_html"],
|
||||
"plain_text": gen["plain_text"],
|
||||
"subject": gen["subject"],
|
||||
"filters": {
|
||||
"days": days,
|
||||
"only_edited": only_edited_flag,
|
||||
"period": period,
|
||||
"article_type": article_type,
|
||||
"category": category,
|
||||
"display": display,
|
||||
"editor_tip": editor_tip,
|
||||
"highlights": highlights,
|
||||
},
|
||||
"send_logs": db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all(),
|
||||
"wiki_error": wiki_error,
|
||||
"wiki_error": gen["wiki_error"],
|
||||
"send_result": None,
|
||||
}
|
||||
)
|
||||
return _render(request, "dashboard.html", context)
|
||||
|
||||
|
||||
@app.post("/newsletter/send-now")
|
||||
@app.post("/newsletter/send-now", response_class=HTMLResponse)
|
||||
async def send_now(
|
||||
request: Request,
|
||||
csrf_token: str = Form(...),
|
||||
period: str = Form("days"),
|
||||
days: int = Form(30),
|
||||
only_edited: str | None = Form(None),
|
||||
article_type: str = Form("all"),
|
||||
category: str = Form(""),
|
||||
show_date: str | None = Form(None),
|
||||
show_user: str | None = Form(None),
|
||||
@@ -303,20 +364,56 @@ async def send_now(
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
display = parse_display_options(show_date, show_user, show_category)
|
||||
only_edited_flag = only_edited == "true"
|
||||
if article_type not in ("all", "new", "edited"):
|
||||
article_type = "all"
|
||||
highlight_list = parse_highlights(highlights)
|
||||
days = max(1, min(days, 90))
|
||||
try:
|
||||
articles = await wiki_service.get_recent_changes(days=days, only_edited=only_edited_flag)
|
||||
except WikiFetchError as exc:
|
||||
raise HTTPException(status_code=502, detail=exc.message) from exc
|
||||
filtered = filter_articles(articles, category_filter=category or None)
|
||||
title = create_subject(days, category)
|
||||
plain_text = create_plain_text(filtered, days, display, category, editor_tip, highlight_list)
|
||||
raw_html = create_html(filtered, days, display, category, editor_tip, highlight_list)
|
||||
recipients = [r.strip() for r in get_config(db, "smtp.recipients", "").split(",") if r.strip()]
|
||||
send_newsletter(db, title, raw_html, plain_text, recipients, scheduled=False)
|
||||
return RedirectResponse(url="/dashboard", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
gen = await _generate_newsletter(
|
||||
period_mode=period,
|
||||
days=days,
|
||||
article_type=article_type,
|
||||
category=category,
|
||||
display=display,
|
||||
editor_tip=editor_tip,
|
||||
highlight_list=highlight_list,
|
||||
)
|
||||
|
||||
send_result: dict[str, str]
|
||||
if gen["wiki_error"]:
|
||||
send_result = {"status": "failed", "message": f"Versand abgebrochen – Wiki-Fehler: {gen['wiki_error']}"}
|
||||
elif not gen["articles"]:
|
||||
send_result = {"status": "failed", "message": "Versand abgebrochen – im Zeitraum wurden keine Artikel gefunden."}
|
||||
else:
|
||||
recipients = [r.strip() for r in get_config(db, "smtp.recipients", "").split(",") if r.strip()]
|
||||
status_code, detail = send_newsletter(db, gen["subject"], gen["raw_html"], gen["plain_text"], recipients, scheduled=False)
|
||||
send_result = {"status": status_code, "message": detail}
|
||||
|
||||
context = _base_context(request, current_user, db)
|
||||
context.update(
|
||||
{
|
||||
"articles": gen["articles"],
|
||||
"articles_new": gen["articles_new"],
|
||||
"articles_edited": gen["articles_edited"],
|
||||
"raw_html": gen["raw_html"],
|
||||
"outlook_html": gen["outlook_html"],
|
||||
"plain_text": gen["plain_text"],
|
||||
"subject": gen["subject"],
|
||||
"filters": {
|
||||
"days": days,
|
||||
"period": period,
|
||||
"article_type": article_type,
|
||||
"category": category,
|
||||
"display": display,
|
||||
"editor_tip": editor_tip,
|
||||
"highlights": highlights,
|
||||
},
|
||||
"send_logs": db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all(),
|
||||
"wiki_error": gen["wiki_error"],
|
||||
"send_result": send_result,
|
||||
}
|
||||
)
|
||||
return _render(request, "dashboard.html", context)
|
||||
|
||||
|
||||
@app.post("/admin/smtp")
|
||||
|
||||
Reference in New Issue
Block a user