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:
@@ -101,13 +101,59 @@ def split_articles_by_type(articles: list[dict[str, Any]]) -> tuple[list[dict[st
|
||||
return new_articles, edited_articles
|
||||
|
||||
|
||||
def create_subject(days: int, category: str = "") -> str:
|
||||
base = f"Interner Wiki-Newsletter – {ORG_NAME} ({days} Tage)"
|
||||
def create_subject(days: int, category: str = "", period_label: str | None = None) -> str:
|
||||
label = period_label or f"{days} Tage"
|
||||
base = f"Interner Wiki-Newsletter – {ORG_NAME} ({label})"
|
||||
if category.strip():
|
||||
return f"{base} | Kategorie: {category.strip()}"
|
||||
return base
|
||||
|
||||
|
||||
_MONTHS_DE = [
|
||||
"Januar", "Februar", "März", "April", "Mai", "Juni",
|
||||
"Juli", "August", "September", "Oktober", "November", "Dezember",
|
||||
]
|
||||
|
||||
|
||||
def _month_label(dt: datetime) -> str:
|
||||
return f"{_MONTHS_DE[dt.month - 1]} {dt.year}"
|
||||
|
||||
|
||||
def resolve_period(mode: str, days: int) -> dict[str, Any]:
|
||||
"""Berechnet Start/Ende und Beschriftung für einen Zeitraum-Modus.
|
||||
|
||||
- "last_month": voriger Kalendermonat (z. B. 01.06.–01.07.)
|
||||
- sonst: die letzten N Tage
|
||||
"""
|
||||
now = datetime.now()
|
||||
if mode == "last_month":
|
||||
first_this_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
last_day_prev = first_this_month - timedelta(days=1)
|
||||
start = last_day_prev.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
end = first_this_month
|
||||
return {
|
||||
"mode": "last_month",
|
||||
"start": start,
|
||||
"end": end,
|
||||
"start_str": start.strftime("%d.%m.%Y"),
|
||||
"end_str": end.strftime("%d.%m.%Y"),
|
||||
"label": _month_label(start),
|
||||
"days": max(1, (end - start).days),
|
||||
}
|
||||
days = max(1, min(days, 90))
|
||||
end = now
|
||||
start = now - timedelta(days=days)
|
||||
return {
|
||||
"mode": "days",
|
||||
"start": start,
|
||||
"end": end,
|
||||
"start_str": start.strftime("%d.%m.%Y"),
|
||||
"end_str": end.strftime("%d.%m.%Y"),
|
||||
"label": f"{days} Tage",
|
||||
"days": days,
|
||||
}
|
||||
|
||||
|
||||
def parse_highlights(raw: str | None) -> list[str]:
|
||||
"""Zerlegt das Highlight-Eingabefeld (Titel pro Zeile) in eine Liste."""
|
||||
if not raw:
|
||||
@@ -155,17 +201,23 @@ def create_plain_text(
|
||||
category: str = "",
|
||||
editor_tip: str = "",
|
||||
highlights: list[str] | None = None,
|
||||
period_label: str | None = None,
|
||||
period: tuple[str, str] | None = None,
|
||||
article_type: str = "all",
|
||||
) -> str:
|
||||
display = display or DEFAULT_DISPLAY
|
||||
new_articles, edited_articles = split_articles_by_type(articles)
|
||||
period_start, period_end = _period_range(days)
|
||||
show_new = article_type in ("all", "new")
|
||||
show_edited = article_type in ("all", "edited")
|
||||
period_start, period_end = period if period else _period_range(days)
|
||||
period_desc = period_label or f"{days} Tage"
|
||||
created_at = datetime.now().strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
lines = [
|
||||
LINE,
|
||||
f"{ORG_NAME.upper()} – INTERNER WIKI-NEWSLETTER",
|
||||
LINE,
|
||||
f"Zeitraum: {period_start} bis {period_end} ({days} Tage)",
|
||||
f"Zeitraum: {period_start} bis {period_end} ({period_desc})",
|
||||
f"Erstellt am: {created_at}",
|
||||
]
|
||||
if category.strip():
|
||||
@@ -202,17 +254,19 @@ def create_plain_text(
|
||||
lines.append(_plain_footer(new_articles, edited_articles))
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
lines.extend(_plain_section("NEUE ARTIKEL", new_articles, display, empty_text="Keine neuen Artikel im Zeitraum."))
|
||||
lines.append("")
|
||||
lines.extend(
|
||||
_plain_section(
|
||||
"BEARBEITETE ARTIKEL",
|
||||
edited_articles,
|
||||
display,
|
||||
empty_text="Keine bearbeiteten Artikel im Zeitraum.",
|
||||
if show_new:
|
||||
lines.extend(_plain_section("NEUE ARTIKEL", new_articles, display, empty_text="Keine neuen Artikel im Zeitraum."))
|
||||
lines.append("")
|
||||
if show_edited:
|
||||
lines.extend(
|
||||
_plain_section(
|
||||
"BEARBEITETE ARTIKEL",
|
||||
edited_articles,
|
||||
display,
|
||||
empty_text="Keine bearbeiteten Artikel im Zeitraum.",
|
||||
)
|
||||
)
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
lines.extend(_plain_editor_tip(editor_tip))
|
||||
lines.append(_plain_footer(new_articles, edited_articles))
|
||||
return "\n".join(lines).strip()
|
||||
@@ -239,12 +293,17 @@ def create_html(
|
||||
category: str = "",
|
||||
editor_tip: str = "",
|
||||
highlights: list[str] | None = None,
|
||||
period_label: str | None = None,
|
||||
period: tuple[str, str] | None = None,
|
||||
article_type: str = "all",
|
||||
) -> str:
|
||||
display = display or DEFAULT_DISPLAY
|
||||
new_articles, edited_articles = split_articles_by_type(articles)
|
||||
period_start, period_end = _period_range(days)
|
||||
show_new = article_type in ("all", "new")
|
||||
show_edited = article_type in ("all", "edited")
|
||||
period_start, period_end = period if period else _period_range(days)
|
||||
total = len(new_articles) + len(edited_articles)
|
||||
subject = escape(create_subject(days, category))
|
||||
subject = escape(create_subject(days, category, period_label))
|
||||
preheader = escape(_summary_sentence(total))
|
||||
period_label = escape(f"{period_start} – {period_end}")
|
||||
summary_text = escape(_summary_sentence(total))
|
||||
@@ -255,12 +314,12 @@ def create_html(
|
||||
if not new_articles and not edited_articles:
|
||||
body = _html_empty_card("Für diesen Zeitraum wurden keine passenden Wiki-Änderungen gefunden.")
|
||||
else:
|
||||
body = "\n".join(
|
||||
[
|
||||
_html_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum.", kind="new"),
|
||||
_html_section("Bearbeitete Artikel", edited_articles, display, "Keine bearbeiteten Artikel im Zeitraum.", kind="edited"),
|
||||
]
|
||||
)
|
||||
sections = []
|
||||
if show_new:
|
||||
sections.append(_html_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum.", kind="new"))
|
||||
if show_edited:
|
||||
sections.append(_html_section("Bearbeitete Artikel", edited_articles, display, "Keine bearbeiteten Artikel im Zeitraum.", kind="edited"))
|
||||
body = "\n".join(sections)
|
||||
|
||||
summary_section = (
|
||||
f'<tr><td style="padding:24px 32px 0 32px;" class="mobile-padding">{_html_summary(new_articles, edited_articles)}</td></tr>'
|
||||
@@ -616,13 +675,18 @@ def create_outlook_html(
|
||||
category: str = "",
|
||||
editor_tip: str = "",
|
||||
highlights: list[str] | None = None,
|
||||
period_label: str | None = None,
|
||||
period: tuple[str, str] | None = None,
|
||||
article_type: str = "all",
|
||||
) -> str:
|
||||
"""Word/Outlook-kompatibles HTML mit font-Tags und bgcolor (überlebt Einfügen in Outlook)."""
|
||||
display = display or DEFAULT_DISPLAY
|
||||
new_articles, edited_articles = split_articles_by_type(articles)
|
||||
period_start, period_end = _period_range(days)
|
||||
show_new = article_type in ("all", "new")
|
||||
show_edited = article_type in ("all", "edited")
|
||||
period_start, period_end = period if period else _period_range(days)
|
||||
total = len(new_articles) + len(edited_articles)
|
||||
subject = escape(create_subject(days, category))
|
||||
subject = escape(create_subject(days, category, period_label))
|
||||
period_label = f"{period_start} – {period_end}"
|
||||
summary_text = _summary_sentence(total)
|
||||
|
||||
@@ -636,18 +700,12 @@ def create_outlook_html(
|
||||
</td>
|
||||
</tr>"""
|
||||
else:
|
||||
body_rows = "\n".join(
|
||||
[
|
||||
_outlook_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum.", kind="new"),
|
||||
_outlook_section(
|
||||
"Bearbeitete Artikel",
|
||||
edited_articles,
|
||||
display,
|
||||
"Keine bearbeiteten Artikel im Zeitraum.",
|
||||
kind="edited",
|
||||
),
|
||||
]
|
||||
)
|
||||
sections = []
|
||||
if show_new:
|
||||
sections.append(_outlook_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum.", kind="new"))
|
||||
if show_edited:
|
||||
sections.append(_outlook_section("Bearbeitete Artikel", edited_articles, display, "Keine bearbeiteten Artikel im Zeitraum.", kind="edited"))
|
||||
body_rows = "\n".join(sections)
|
||||
|
||||
category_block = ""
|
||||
if category.strip():
|
||||
|
||||
@@ -32,14 +32,16 @@ def send_newsletter(
|
||||
text_content: str,
|
||||
recipients: Sequence[str],
|
||||
scheduled: bool = False,
|
||||
) -> None:
|
||||
) -> tuple[str, str]:
|
||||
settings = get_smtp_settings(db)
|
||||
if settings["enabled"].lower() != "true":
|
||||
_log(db, subject, recipients, "skipped", "SMTP ist deaktiviert.", scheduled)
|
||||
return
|
||||
detail = "SMTP ist deaktiviert. Bitte in der SMTP-Konfiguration aktivieren."
|
||||
_log(db, subject, recipients, "skipped", detail, scheduled)
|
||||
return "skipped", detail
|
||||
if not settings["host"] or not settings["from_email"] or not recipients:
|
||||
_log(db, subject, recipients, "failed", "SMTP unvollständig konfiguriert.", scheduled)
|
||||
return
|
||||
detail = "SMTP unvollständig konfiguriert (Host, Absender oder Empfänger fehlen)."
|
||||
_log(db, subject, recipients, "failed", detail, scheduled)
|
||||
return "failed", detail
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg.attach(MIMEText(text_content, "plain", "utf-8"))
|
||||
@@ -56,9 +58,13 @@ def send_newsletter(
|
||||
if settings["username"]:
|
||||
server.login(settings["username"], settings["password"])
|
||||
server.sendmail(settings["from_email"], list(recipients), msg.as_string())
|
||||
_log(db, subject, recipients, "sent", "Versand erfolgreich.", scheduled)
|
||||
detail = f"Versand erfolgreich an {len(recipients)} Empfänger."
|
||||
_log(db, subject, recipients, "sent", detail, scheduled)
|
||||
return "sent", detail
|
||||
except Exception as exc:
|
||||
_log(db, subject, recipients, "failed", f"Versandfehler: {exc}", scheduled)
|
||||
detail = f"Versandfehler: {exc}"
|
||||
_log(db, subject, recipients, "failed", detail, scheduled)
|
||||
return "failed", detail
|
||||
|
||||
|
||||
def run_scheduled_send_if_due(db: Session, subject: str, html_content: str, text_content: str) -> None:
|
||||
|
||||
@@ -19,11 +19,18 @@ class WikiService:
|
||||
def __init__(self) -> None:
|
||||
self.api_url = settings.wiki_api_url
|
||||
|
||||
async def get_recent_changes(self, days: int = 30, only_edited: bool = False) -> list[dict[str, Any]]:
|
||||
now = datetime.now(timezone.utc)
|
||||
start = now - timedelta(days=days)
|
||||
rcstart = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
rcend = start.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
async def get_recent_changes(
|
||||
self,
|
||||
days: int = 30,
|
||||
article_type: str = "all",
|
||||
start: datetime | None = None,
|
||||
end: datetime | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
# MediaWiki listet neueste Änderungen zuerst: rcstart = obere (neuere) Grenze, rcend = untere (ältere).
|
||||
upper = end or datetime.now(timezone.utc)
|
||||
lower = start or (upper - timedelta(days=days))
|
||||
rcstart = upper.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
rcend = lower.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
params: dict[str, str] = {
|
||||
"action": "query",
|
||||
@@ -35,7 +42,9 @@ class WikiService:
|
||||
"rcend": rcend,
|
||||
"rcnamespace": "0",
|
||||
}
|
||||
if only_edited:
|
||||
if article_type == "new":
|
||||
params["rctype"] = "new"
|
||||
elif article_type == "edited":
|
||||
params["rctype"] = "edit"
|
||||
else:
|
||||
params["rctype"] = "new|edit"
|
||||
|
||||
Reference in New Issue
Block a user