diff --git a/.env.example b/.env.example index e5eef06..0726834 100644 --- a/.env.example +++ b/.env.example @@ -28,3 +28,6 @@ ADMIN_BOOTSTRAP_RESET=false EDITOR_TIP_MAX_LENGTH=10000 # Maximale Länge für Highlights HIGHLIGHTS_MAX_LENGTH=5000 +# Standardmäßig ausgeschlossene Bearbeiter (kommagetrennt) +# Beispiel: Aranzinger, Testuser +DEFAULT_EXCLUDED_USERS=Aranzinger diff --git a/app/core/config.py b/app/core/config.py index 311ff65..4180f59 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -21,6 +21,7 @@ class Settings(BaseSettings): admin_bootstrap_reset: bool = False editor_tip_max_length: int = 10000 highlights_max_length: int = 5000 + default_excluded_users: str = "Aranzinger" model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") diff --git a/app/main.py b/app/main.py index 590bab6..ea9f2c8 100644 --- a/app/main.py +++ b/app/main.py @@ -36,6 +36,7 @@ from app.services.newsletter import ( create_plain_text, create_subject, filter_articles, + parse_excluded_users, parse_display_options, parse_highlights, resolve_period, @@ -117,6 +118,7 @@ async def _run_scheduled_send_if_due() -> None: editor_tip="", highlight_list=[], contact_email=settings_snapshot.get("reply_to") or settings_snapshot.get("from_email", ""), + excluded_users_raw=settings_snapshot.get("gen_excluded_users", ""), ) if gen["wiki_error"] or not gen["articles"]: # Kein Versand ohne Inhalt; erneuter Versuch beim nächsten Intervall. @@ -360,6 +362,7 @@ def _empty_filters() -> dict: "period": "last_month", "article_type": "new", "category": "", + "excluded_users": settings.default_excluded_users, "display": DEFAULT_DISPLAY, "editor_tip": "", "highlights": "", @@ -376,6 +379,7 @@ async def _generate_newsletter( editor_tip: str, highlight_list: list[str], contact_email: str = "", + excluded_users_raw: str = "", ) -> dict: p = resolve_period(period_mode, days) result = { @@ -390,11 +394,12 @@ async def _generate_newsletter( "subject": "", } try: + excluded_users = parse_excluded_users(excluded_users_raw) 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) + new_articles, edited_articles = split_articles_by_type(filtered, excluded_edited_users=excluded_users) prange = (p["start_str"], p["end_str"]) result.update( { @@ -402,9 +407,15 @@ async def _generate_newsletter( "articles_new": new_articles, "articles_edited": edited_articles, "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, contact_email), - "raw_html": create_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"], contact_email), - "outlook_html": create_outlook_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"], contact_email), + "plain_text": create_plain_text( + filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, contact_email, excluded_users + ), + "raw_html": create_html( + filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"], contact_email, excluded_users + ), + "outlook_html": create_outlook_html( + filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"], contact_email, excluded_users + ), } ) except WikiFetchError as exc: @@ -444,6 +455,7 @@ async def dashboard_generate( days: int = Form(30), article_type: str = Form("all"), category: str = Form(""), + excluded_users: str = Form(""), show_date: str | None = Form(None), show_user: str | None = Form(None), show_category: str | None = Form(None), @@ -474,6 +486,7 @@ async def dashboard_generate( editor_tip=editor_tip, highlight_list=highlight_list, contact_email=contact_email, + excluded_users_raw=excluded_users, ) context = _base_context(request, current_user, db) @@ -491,6 +504,7 @@ async def dashboard_generate( "period": period, "article_type": article_type, "category": category, + "excluded_users": excluded_users, "display": display, "editor_tip": editor_tip, "highlights": highlights, @@ -511,6 +525,7 @@ async def send_now( days: int = Form(30), article_type: str = Form("all"), category: str = Form(""), + excluded_users: str = Form(""), show_date: str | None = Form(None), show_user: str | None = Form(None), show_category: str | None = Form(None), @@ -541,6 +556,7 @@ async def send_now( editor_tip=editor_tip, highlight_list=highlight_list, contact_email=contact_email, + excluded_users_raw=excluded_users, ) send_result: dict[str, str] @@ -585,6 +601,7 @@ def update_smtp_settings( gen_days: str = Form("30"), gen_article_type: str = Form("all"), gen_category: str = Form(""), + gen_excluded_users: str = Form(""), db: Session = Depends(get_db), admin: User = Depends(get_admin_user), ): @@ -631,6 +648,7 @@ def update_smtp_settings( gen_article_type = "all" set_config(db, "smtp.gen_article_type", gen_article_type) set_config(db, "smtp.gen_category", gen_category.strip()) + set_config(db, "smtp.gen_excluded_users", gen_excluded_users.strip()) return RedirectResponse(url="/dashboard", status_code=status.HTTP_302_FOUND) diff --git a/app/services/newsletter.py b/app/services/newsletter.py index 5c06a6a..5feed99 100644 --- a/app/services/newsletter.py +++ b/app/services/newsletter.py @@ -76,11 +76,21 @@ def filter_articles( ] -def split_articles_by_type(articles: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: +def parse_excluded_users(raw: str | None) -> set[str]: + if not raw: + return set() + return {part.strip().lower() for part in raw.split(",") if part.strip()} + + +def split_articles_by_type( + articles: list[dict[str, Any]], + excluded_edited_users: set[str] | None = None, +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: sorted_articles = sorted(articles, key=lambda a: a.get("timestamp", ""), reverse=True) new_articles: list[dict[str, Any]] = [] edited_articles: list[dict[str, Any]] = [] new_titles: set[str] = set() + excluded_edited_users = excluded_edited_users or set() for item in sorted_articles: title = item.get("title", "") @@ -93,8 +103,11 @@ def split_articles_by_type(articles: list[dict[str, Any]]) -> tuple[list[dict[st seen_edited: set[str] = set() for item in sorted_articles: title = item.get("title", "") + editor = str(item.get("user", "")).strip().lower() if not title or title in new_titles or title in seen_edited: continue + if editor in excluded_edited_users: + continue edited_articles.append(item) seen_edited.add(title) @@ -204,9 +217,10 @@ def create_plain_text( period: tuple[str, str] | None = None, article_type: str = "all", contact_email: str = "", + excluded_edited_users: set[str] | None = None, ) -> str: display = display or DEFAULT_DISPLAY - new_articles, edited_articles = split_articles_by_type(articles) + new_articles, edited_articles = split_articles_by_type(articles, excluded_edited_users=excluded_edited_users) 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) @@ -308,9 +322,10 @@ def create_html( article_type: str = "all", month_label: str | None = None, contact_email: str = "", + excluded_edited_users: set[str] | None = None, ) -> str: display = display or DEFAULT_DISPLAY - new_articles, edited_articles = split_articles_by_type(articles) + new_articles, edited_articles = split_articles_by_type(articles, excluded_edited_users=excluded_edited_users) 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) @@ -707,10 +722,11 @@ def create_outlook_html( article_type: str = "all", month_label: str | None = None, contact_email: str = "", + excluded_edited_users: set[str] | None = None, ) -> 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) + new_articles, edited_articles = split_articles_by_type(articles, excluded_edited_users=excluded_edited_users) 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) diff --git a/app/services/smtp_sender.py b/app/services/smtp_sender.py index 4a40b9d..1048aaf 100644 --- a/app/services/smtp_sender.py +++ b/app/services/smtp_sender.py @@ -8,6 +8,7 @@ from typing import Sequence from sqlalchemy.orm import Session +from app.core.config import settings from app.core.email_utils import parse_recipient_list from app.models.system import SendLog from app.services.config_store import get_config @@ -40,6 +41,7 @@ def get_smtp_settings(db: Session) -> dict[str, str]: "gen_days": get_config(db, "smtp.gen_days", "30"), "gen_article_type": get_config(db, "smtp.gen_article_type", "new"), "gen_category": get_config(db, "smtp.gen_category", ""), + "gen_excluded_users": get_config(db, "smtp.gen_excluded_users", settings.default_excluded_users), } diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index a926b81..a8b3d03 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -49,7 +49,11 @@ + +

Nur bei „Bearbeitete Artikel“ relevant: Benutzer in dieser Liste werden aus den bearbeiteten Artikeln entfernt.

+ +

Diese Benutzer werden bei „Bearbeitete Artikel“ ignoriert (gilt für den geplanten Versand).