dev #6

Merged
smueller merged 2 commits from dev into main 2026-07-07 13:45:25 +00:00
4 changed files with 70 additions and 36 deletions

View File

@@ -2,6 +2,7 @@ import asyncio
import logging import logging
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from urllib.parse import urlencode
from pydantic import ValidationError from pydantic import ValidationError
@@ -101,6 +102,7 @@ async def _run_scheduled_send_if_due() -> None:
display=DEFAULT_DISPLAY, display=DEFAULT_DISPLAY,
editor_tip="", editor_tip="",
highlight_list=[], highlight_list=[],
contact_email=settings_snapshot.get("reply_to") or settings_snapshot.get("from_email", ""),
) )
if gen["wiki_error"] or not gen["articles"]: if gen["wiki_error"] or not gen["articles"]:
# Kein Versand ohne Inhalt; erneuter Versuch beim nächsten Intervall. # Kein Versand ohne Inhalt; erneuter Versuch beim nächsten Intervall.
@@ -296,6 +298,7 @@ async def _generate_newsletter(
display, display,
editor_tip: str, editor_tip: str,
highlight_list: list[str], highlight_list: list[str],
contact_email: str = "",
) -> dict: ) -> dict:
p = resolve_period(period_mode, days) p = resolve_period(period_mode, days)
result = { result = {
@@ -322,9 +325,9 @@ async def _generate_newsletter(
"articles_new": new_articles, "articles_new": new_articles,
"articles_edited": edited_articles, "articles_edited": edited_articles,
"subject": create_subject(p["month_label"], p["days"]), "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), "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"]), "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"]), "outlook_html": create_outlook_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"], contact_email),
} }
) )
except WikiFetchError as exc: except WikiFetchError as exc:
@@ -335,6 +338,10 @@ async def _generate_newsletter(
@app.get("/dashboard", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): async def dashboard(request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
context = _base_context(request, current_user, db) context = _base_context(request, current_user, db)
send_status = request.query_params.get("send_status")
send_result = None
if send_status:
send_result = {"status": send_status, "message": request.query_params.get("send_msg", "")}
context.update({ context.update({
"articles": [], "articles": [],
"articles_new": [], "articles_new": [],
@@ -345,7 +352,7 @@ async def dashboard(request: Request, db: Session = Depends(get_db), current_use
"subject": "", "subject": "",
"filters": _empty_filters(), "filters": _empty_filters(),
"wiki_error": None, "wiki_error": None,
"send_result": None, "send_result": send_result,
}) })
logs = db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all() logs = db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all()
context["send_logs"] = logs context["send_logs"] = logs
@@ -374,6 +381,8 @@ async def dashboard_generate(
article_type = "all" article_type = "all"
display = parse_display_options(show_date, show_user, show_category) display = parse_display_options(show_date, show_user, show_category)
highlight_list = parse_highlights(highlights) highlight_list = parse_highlights(highlights)
smtp_cfg = get_smtp_settings(db)
contact_email = smtp_cfg["reply_to"] or smtp_cfg["from_email"]
gen = await _generate_newsletter( gen = await _generate_newsletter(
period_mode=period, period_mode=period,
@@ -383,6 +392,7 @@ async def dashboard_generate(
display=display, display=display,
editor_tip=editor_tip, editor_tip=editor_tip,
highlight_list=highlight_list, highlight_list=highlight_list,
contact_email=contact_email,
) )
context = _base_context(request, current_user, db) context = _base_context(request, current_user, db)
@@ -434,6 +444,8 @@ async def send_now(
article_type = "all" article_type = "all"
highlight_list = parse_highlights(highlights) highlight_list = parse_highlights(highlights)
days = max(1, min(days, 90)) days = max(1, min(days, 90))
smtp_cfg = get_smtp_settings(db)
contact_email = smtp_cfg["reply_to"] or smtp_cfg["from_email"]
gen = await _generate_newsletter( gen = await _generate_newsletter(
period_mode=period, period_mode=period,
@@ -443,6 +455,7 @@ async def send_now(
display=display, display=display,
editor_tip=editor_tip, editor_tip=editor_tip,
highlight_list=highlight_list, highlight_list=highlight_list,
contact_email=contact_email,
) )
send_result: dict[str, str] send_result: dict[str, str]
@@ -455,31 +468,8 @@ async def send_now(
status_code, detail = send_newsletter(db, gen["subject"], gen["raw_html"], gen["plain_text"], recipients, scheduled=False) status_code, detail = send_newsletter(db, gen["subject"], gen["raw_html"], gen["plain_text"], recipients, scheduled=False)
send_result = {"status": status_code, "message": detail} send_result = {"status": status_code, "message": detail}
context = _base_context(request, current_user, db) query = urlencode({"send_status": send_result["status"], "send_msg": send_result["message"]})
context.update( return RedirectResponse(url=f"/dashboard?{query}", status_code=status.HTTP_303_SEE_OTHER)
{
"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") @app.post("/admin/smtp")
@@ -492,6 +482,7 @@ def update_smtp_settings(
username: str = Form(""), username: str = Form(""),
password: str = Form(""), password: str = Form(""),
from_email: str = Form(""), from_email: str = Form(""),
reply_to: str = Form(""),
use_tls: str | None = Form(None), use_tls: str | None = Form(None),
recipients: str = Form(""), recipients: str = Form(""),
schedule_enabled: str | None = Form(None), schedule_enabled: str | None = Form(None),
@@ -513,6 +504,7 @@ def update_smtp_settings(
set_config(db, "smtp.username", username.strip()) set_config(db, "smtp.username", username.strip())
set_config(db, "smtp.password", password) set_config(db, "smtp.password", password)
set_config(db, "smtp.from_email", from_email.strip()) set_config(db, "smtp.from_email", from_email.strip())
set_config(db, "smtp.reply_to", reply_to.strip())
set_config(db, "smtp.use_tls", "true" if use_tls == "true" else "false") set_config(db, "smtp.use_tls", "true" if use_tls == "true" else "false")
set_config(db, "smtp.recipients", recipients.strip()) set_config(db, "smtp.recipients", recipients.strip())

View File

@@ -203,6 +203,7 @@ def create_plain_text(
period_label: str | None = None, period_label: str | None = None,
period: tuple[str, str] | None = None, period: tuple[str, str] | None = None,
article_type: str = "all", article_type: str = "all",
contact_email: str = "",
) -> str: ) -> str:
display = display or DEFAULT_DISPLAY display = display or DEFAULT_DISPLAY
new_articles, edited_articles = split_articles_by_type(articles) new_articles, edited_articles = split_articles_by_type(articles)
@@ -250,7 +251,7 @@ def create_plain_text(
] ]
) )
lines.extend(_plain_editor_tip(editor_tip)) lines.extend(_plain_editor_tip(editor_tip))
lines.append(_plain_footer(new_articles, edited_articles)) lines.append(_plain_footer(new_articles, edited_articles, contact_email))
return "\n".join(lines).strip() return "\n".join(lines).strip()
if show_new: if show_new:
@@ -267,7 +268,7 @@ def create_plain_text(
) )
lines.append("") lines.append("")
lines.extend(_plain_editor_tip(editor_tip)) lines.extend(_plain_editor_tip(editor_tip))
lines.append(_plain_footer(new_articles, edited_articles)) lines.append(_plain_footer(new_articles, edited_articles, contact_email))
return "\n".join(lines).strip() return "\n".join(lines).strip()
@@ -296,6 +297,7 @@ def create_html(
period: tuple[str, str] | None = None, period: tuple[str, str] | None = None,
article_type: str = "all", article_type: str = "all",
month_label: str | None = None, month_label: str | None = None,
contact_email: str = "",
) -> str: ) -> str:
display = display or DEFAULT_DISPLAY display = display or DEFAULT_DISPLAY
new_articles, edited_articles = split_articles_by_type(articles) new_articles, edited_articles = split_articles_by_type(articles)
@@ -334,6 +336,15 @@ def create_html(
f'<strong style="color:{COLOR_HEADING};">Filter:</strong> Kategorie {escape(category.strip())}</p></td></tr>' f'<strong style="color:{COLOR_HEADING};">Filter:</strong> Kategorie {escape(category.strip())}</p></td></tr>'
) )
if contact_email.strip():
ce = escape(contact_email.strip())
contact_html = (
f'Fragen oder ein Themenwunsch? Meldet euch gern beim '
f'<a href="mailto:{ce}" style="color:{COLOR_TK_ORANGE};text-decoration:underline;">Wiki-Team</a>.'
)
else:
contact_html = "Fragen oder ein Themenwunsch? Meldet euch gern beim Wiki-Team."
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
@@ -411,7 +422,7 @@ def create_html(
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"> <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr> <tr>
<td style="font-family:{FONT};font-size:15px;line-height:24px;color:{COLOR_TEXT};"> <td style="font-family:{FONT};font-size:15px;line-height:24px;color:{COLOR_TEXT};">
Fragen oder ein Themenwunsch? Meldet euch gern beim Wiki-Team.<br><br> {contact_html}<br><br>
Viele Grüße<br> Viele Grüße<br>
Euer Thomas-Krenn-Wiki Team Euer Thomas-Krenn-Wiki Team
</td> </td>
@@ -679,6 +690,7 @@ def create_outlook_html(
period: tuple[str, str] | None = None, period: tuple[str, str] | None = None,
article_type: str = "all", article_type: str = "all",
month_label: str | None = None, month_label: str | None = None,
contact_email: str = "",
) -> str: ) -> str:
"""Word/Outlook-kompatibles HTML mit font-Tags und bgcolor (überlebt Einfügen in Outlook).""" """Word/Outlook-kompatibles HTML mit font-Tags und bgcolor (überlebt Einfügen in Outlook)."""
display = display or DEFAULT_DISPLAY display = display or DEFAULT_DISPLAY
@@ -726,6 +738,19 @@ def create_outlook_html(
editor_tip_row = _outlook_editor_tip(editor_tip) editor_tip_row = _outlook_editor_tip(editor_tip)
if contact_email.strip():
contact_inner = (
f'{_outlook_font("Fragen oder ein Themenwunsch? Meldet euch gern beim Wiki-Team: ", size="1")}'
f'{_outlook_link(contact_email.strip(), f"mailto:{contact_email.strip()}", color=OUTLOOK_ORANGE, bold=False)}'
)
contact_row = f"""<tr>
<td {_outlook_td_attrs(extra_style="padding:12px 0 0;")}>
{contact_inner}
</td>
</tr>"""
else:
contact_row = ""
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word"> <html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word">
<head> <head>
@@ -785,6 +810,7 @@ def create_outlook_html(
{_outlook_cta(WIKI_HOME_URL, "Zum Thomas-Krenn-Wiki →")} {_outlook_cta(WIKI_HOME_URL, "Zum Thomas-Krenn-Wiki →")}
</td> </td>
</tr> </tr>
{contact_row}
<tr> <tr>
<td {_outlook_td_attrs(extra_style=f"padding:16px 0 0;border-top:1px solid {OUTLOOK_BORDER};")}> <td {_outlook_td_attrs(extra_style=f"padding:16px 0 0;border-top:1px solid {OUTLOOK_BORDER};")}>
{_outlook_font(f"Automatisch erstellter interner Newsletter · Nur für den Gebrauch bei {ORG_NAME}", color=OUTLOOK_MUTED, size="1")} {_outlook_font(f"Automatisch erstellter interner Newsletter · Nur für den Gebrauch bei {ORG_NAME}", color=OUTLOOK_MUTED, size="1")}
@@ -932,8 +958,15 @@ def _plain_item(idx: int, item: dict[str, Any], display: DisplayOptions) -> list
return lines return lines
def _plain_footer(new_articles: list[dict[str, Any]], edited_articles: list[dict[str, Any]]) -> str: def _plain_footer(
new_articles: list[dict[str, Any]],
edited_articles: list[dict[str, Any]],
contact_email: str = "",
) -> str:
total = len(new_articles) + len(edited_articles) total = len(new_articles) + len(edited_articles)
contact_lines = []
if contact_email.strip():
contact_lines = [f"Fragen oder ein Themenwunsch? E-Mail an das Wiki-Team: {contact_email.strip()}", ""]
return "\n".join( return "\n".join(
[ [
SUBLINE, SUBLINE,
@@ -945,6 +978,7 @@ def _plain_footer(new_articles: list[dict[str, Any]], edited_articles: list[dict
"", "",
f"Wiki: {WIKI_HOME_URL}", f"Wiki: {WIKI_HOME_URL}",
"", "",
*contact_lines,
SUBLINE, SUBLINE,
"Dieser Newsletter wurde automatisch erstellt und ist nur für den internen", "Dieser Newsletter wurde automatisch erstellt und ist nur für den internen",
f"Gebrauch bei {ORG_NAME} bestimmt.", f"Gebrauch bei {ORG_NAME} bestimmt.",

View File

@@ -18,6 +18,7 @@ def get_smtp_settings(db: Session) -> dict[str, str]:
"username": get_config(db, "smtp.username", ""), "username": get_config(db, "smtp.username", ""),
"password": get_config(db, "smtp.password", ""), "password": get_config(db, "smtp.password", ""),
"from_email": get_config(db, "smtp.from_email", ""), "from_email": get_config(db, "smtp.from_email", ""),
"reply_to": get_config(db, "smtp.reply_to", ""),
"use_tls": get_config(db, "smtp.use_tls", "true"), "use_tls": get_config(db, "smtp.use_tls", "true"),
"recipients": get_config(db, "smtp.recipients", ""), "recipients": get_config(db, "smtp.recipients", ""),
# Zeitplan # Zeitplan
@@ -91,6 +92,8 @@ def send_newsletter(
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = settings["from_email"] msg["From"] = settings["from_email"]
msg["To"] = ", ".join(recipients) msg["To"] = ", ".join(recipients)
if settings.get("reply_to"):
msg["Reply-To"] = settings["reply_to"]
try: try:
port = int(settings["port"]) port = int(settings["port"])

View File

@@ -253,9 +253,14 @@
<input type="password" name="password" value="{{ smtp.password }}"> <input type="password" name="password" value="{{ smtp.password }}">
</label> </label>
</div> </div>
<div class="form-row">
<label>Absender E-Mail <label>Absender E-Mail
<input type="email" name="from_email" value="{{ smtp.from_email }}"> <input type="email" name="from_email" value="{{ smtp.from_email }}">
</label> </label>
<label>Antwort-Adresse (Reply-To)
<input type="email" name="reply_to" value="{{ smtp.reply_to }}" placeholder="wiki-team@firma.de">
</label>
</div>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="use_tls" value="true" {% if smtp.use_tls == "true" %}checked{% endif %}> <input type="checkbox" name="use_tls" value="true" {% if smtp.use_tls == "true" %}checked{% endif %}>
STARTTLS verwenden STARTTLS verwenden