PDF generiert
This commit is contained in:
parent
8a9823e9de
commit
470a8b1881
11
Dockerfile
11
Dockerfile
@ -2,6 +2,17 @@ FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 👉 SYSTEM LIBRARIES für WeasyPrint
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpango-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
libcairo2 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info \
|
||||
fonts-dejavu-core \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
BIN
Kurzanleitung.docx
Normal file
BIN
Kurzanleitung.docx
Normal file
Binary file not shown.
91
app.py
91
app.py
@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from flask import Flask, flash, redirect, render_template, request, session, url_for, send_from_directory
|
||||
from flask import Flask, flash, redirect, render_template, request, session, url_for, send_from_directory, abort
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from config import Config
|
||||
@ -52,10 +52,15 @@ from db import (
|
||||
get_thema_for_branche,
|
||||
get_question_count_for_thema,
|
||||
get_all_themen_with_question_count,
|
||||
get_pdf_recommendation_topics,
|
||||
get_random_ansprechpartner_for_thema,
|
||||
create_empfehlung,
|
||||
)
|
||||
from permissions import admin_required, login_required
|
||||
from tools import create_assessment_chart, generate_activation_token, send_mail, verify_activation_token
|
||||
|
||||
from tools import create_assessment_chart, generate_activation_token, send_mail, verify_activation_token, generate_pdf_from_html
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from copy import deepcopy
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
@ -63,6 +68,8 @@ app.config.from_object(Config)
|
||||
chart_dir = Path("generated_charts")
|
||||
chart_dir.mkdir(exist_ok=True)
|
||||
|
||||
pdf_dir = Path("generated_pdfs")
|
||||
pdf_dir.mkdir(exist_ok=True)
|
||||
|
||||
@app.context_processor
|
||||
def inject_user():
|
||||
@ -314,26 +321,80 @@ def assessment_result(assessment_id):
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
rows = get_assessment_result_rows(assessment_id, branche_id)
|
||||
|
||||
if not rows:
|
||||
flash("Für diese Branche konnten keine Auswertungsdaten geladen werden.", "warning")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
branche = get_branche_by_id(branche_id)
|
||||
user = get_user_by_id(session["user_id"])
|
||||
|
||||
labels = [row["kurztitel"] for row in rows]
|
||||
values = [int(row["ja_anzahl"]) for row in rows]
|
||||
|
||||
filename = f"assessment_{assessment_id}_branche_{branche_id}.png"
|
||||
output_path = chart_dir / filename
|
||||
create_assessment_chart(labels, values, output_path)
|
||||
chart_filename = f"assessment_{assessment_id}_branche_{branche_id}.png"
|
||||
chart_path = chart_dir / chart_filename
|
||||
create_assessment_chart(labels, values, chart_path)
|
||||
|
||||
today_str = datetime.now().strftime("%d.%m.%Y")
|
||||
|
||||
logo_up_path = (Path(app.root_path) / "static" / "pdf" / "logo_up.png").resolve().as_uri()
|
||||
unternehmer_path = (Path(app.root_path) / "static" / "pdf" / "unternehmer.png").resolve().as_uri()
|
||||
chart_pdf_path = chart_path.resolve().as_uri()
|
||||
|
||||
today_str = datetime.now().strftime("%d.%m.%Y")
|
||||
|
||||
pdf_filename = f"assessment_{assessment_id}_branche_{branche_id}.pdf"
|
||||
pdf_path = pdf_dir / pdf_filename
|
||||
|
||||
recommendation_topics = get_pdf_recommendation_topics(assessment_id, branche_id)
|
||||
pdf_recommendations = []
|
||||
|
||||
for topic in recommendation_topics:
|
||||
topic_copy = deepcopy(topic)
|
||||
ansprechpartner = get_random_ansprechpartner_for_thema(topic["id"])
|
||||
|
||||
topic_copy["ansprechpartner"] = ansprechpartner
|
||||
pdf_recommendations.append(topic_copy)
|
||||
|
||||
if ansprechpartner:
|
||||
create_empfehlung(
|
||||
user_id=session["user_id"],
|
||||
thema_id=topic["id"],
|
||||
ansprechpartner_id=ansprechpartner["id"],
|
||||
)
|
||||
|
||||
pdf_html = render_template(
|
||||
"pdf/assessment_report.html",
|
||||
assessment_id=assessment_id,
|
||||
branche=branche,
|
||||
user=user,
|
||||
today_str=today_str,
|
||||
rows=rows,
|
||||
pdf_recommendations=pdf_recommendations,
|
||||
chart_pdf_path=chart_pdf_path,
|
||||
logo_up_path=logo_up_path,
|
||||
unternehmer_path=unternehmer_path,
|
||||
)
|
||||
|
||||
generate_pdf_from_html(
|
||||
html_string=pdf_html,
|
||||
output_path=pdf_path,
|
||||
base_url=request.url_root.rstrip("/") + "/",
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"result.html",
|
||||
assessment_id=assessment_id,
|
||||
branche_id=branche_id,
|
||||
chart_file=filename,
|
||||
chart_file=chart_filename,
|
||||
pdf_file=pdf_filename,
|
||||
rows=rows,
|
||||
)
|
||||
|
||||
@app.route("/generated_pdfs/<path:filename>")
|
||||
@login_required
|
||||
def generated_pdf(filename):
|
||||
return send_from_directory(pdf_dir, filename, as_attachment=True)
|
||||
|
||||
@app.route("/generated_charts/<path:filename>")
|
||||
@login_required
|
||||
@ -839,5 +900,19 @@ def admin_question_edit_for_thema(thema_id, frage_id):
|
||||
min_questions=8,
|
||||
)
|
||||
|
||||
@app.errorhandler(401)
|
||||
def unauthorized_error(error):
|
||||
return render_template("401.html"), 401
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
return render_template("404.html"), 404
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
return render_template("500.html"), 500
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
12
config.py
12
config.py
@ -9,11 +9,11 @@ class Config:
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD', 'UnternehmenPWD')
|
||||
DB_PORT = int(os.getenv('DB_PORT', '5432'))
|
||||
|
||||
SMTP_SERVER = os.getenv('SMTP_SERVER', 'mail.kolb.cc')
|
||||
SMTP_PORT = int(os.getenv('SMTP_PORT', '25'))
|
||||
SMTP_USERNAME = os.getenv('SMTP_USERNAME', '')
|
||||
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', '')
|
||||
MAIL_SENDER = os.getenv('MAIL_SENDER', 'admin@kolb.cc')
|
||||
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'false').lower() == 'true'
|
||||
SMTP_SERVER = os.getenv("SMTP_SERVER")
|
||||
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
|
||||
SMTP_USERNAME = os.getenv("SMTP_USERNAME")
|
||||
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
|
||||
MAIL_SENDER = os.getenv("MAIL_SENDER")
|
||||
MAIL_USE_TLS = os.getenv("MAIL_USE_TLS", "true").lower() == "true"
|
||||
|
||||
APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://test.dasunternehmen.com')
|
||||
|
||||
48
db.py
48
db.py
@ -413,6 +413,7 @@ def get_assessment_result_rows(assessment_id, branche_id):
|
||||
SELECT
|
||||
t.id,
|
||||
t.kurztitel,
|
||||
t.titel,
|
||||
COUNT(*) FILTER (WHERE aa.antwort = TRUE) AS ja_anzahl
|
||||
FROM thema t
|
||||
JOIN branchenthemen bt
|
||||
@ -421,7 +422,7 @@ def get_assessment_result_rows(assessment_id, branche_id):
|
||||
ON aa.thema_id = t.id
|
||||
AND aa.assessmentid = %s
|
||||
WHERE bt.branche_id = %s
|
||||
GROUP BY t.id, t.kurztitel
|
||||
GROUP BY t.id, t.kurztitel, t.titel
|
||||
ORDER BY t.id
|
||||
""",
|
||||
(assessment_id, branche_id),
|
||||
@ -760,3 +761,48 @@ def get_all_themen_with_question_count():
|
||||
ORDER BY t.id
|
||||
"""
|
||||
)
|
||||
|
||||
def get_random_ansprechpartner_for_thema(thema_id):
|
||||
return fetch_one(
|
||||
"""
|
||||
SELECT a.id, a.name, a.email, a.infotext
|
||||
FROM ansprechpartner a
|
||||
JOIN themaansprechpartner ta ON ta.ansprechpartner_id = a.id
|
||||
WHERE ta.thema_id = %s
|
||||
ORDER BY RANDOM()
|
||||
LIMIT 1
|
||||
""",
|
||||
(thema_id,),
|
||||
)
|
||||
|
||||
def create_empfehlung(user_id, thema_id, ansprechpartner_id):
|
||||
return execute_returning(
|
||||
"""
|
||||
INSERT INTO empfehlung (user_id, thema_id, ansprechpartner_id)
|
||||
VALUES (%s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(user_id, thema_id, ansprechpartner_id),
|
||||
)
|
||||
|
||||
def get_pdf_recommendation_topics(assessment_id, branche_id):
|
||||
return fetch_all(
|
||||
"""
|
||||
SELECT
|
||||
t.id,
|
||||
t.titel,
|
||||
t.zusatztext,
|
||||
COUNT(*) FILTER (WHERE aa.antwort = TRUE) AS ja_anzahl
|
||||
FROM thema t
|
||||
JOIN branchenthemen bt
|
||||
ON bt.thema_id = t.id
|
||||
LEFT JOIN assessmentanswer aa
|
||||
ON aa.thema_id = t.id
|
||||
AND aa.assessmentid = %s
|
||||
WHERE bt.branche_id = %s
|
||||
GROUP BY t.id, t.titel, t.zusatztext
|
||||
HAVING COUNT(*) FILTER (WHERE aa.antwort = TRUE) < 7
|
||||
ORDER BY t.id
|
||||
""",
|
||||
(assessment_id, branche_id),
|
||||
)
|
||||
@ -4,3 +4,13 @@ Werkzeug==3.1.3
|
||||
itsdangerous==2.2.0
|
||||
matplotlib==3.10.1
|
||||
gunicorn==23.0.0
|
||||
|
||||
weasyprint==60.2
|
||||
pydyf==0.8.0
|
||||
cairocffi==1.6.1
|
||||
|
||||
Pillow==10.4.0
|
||||
qrcode==7.4.2
|
||||
|
||||
pytest==8.3.2
|
||||
python-json-logger==2.0.7
|
||||
BIN
static/pdf/logo_up.png
Normal file
BIN
static/pdf/logo_up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 478 KiB |
BIN
static/pdf/unternehmer.png
Normal file
BIN
static/pdf/unternehmer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
17
templates/401.html
Normal file
17
templates/401.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}401 - Nicht autorisiert{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-card">
|
||||
<h1>401 - Nicht autorisiert</h1>
|
||||
<p>
|
||||
Sie sind für diese Seite nicht autorisiert oder Ihre Anmeldung ist nicht mehr gültig.
|
||||
</p>
|
||||
|
||||
<div class="form-actions" style="margin-top: 24px;">
|
||||
<a class="btn" href="{{ url_for('login') }}">Zum Login</a>
|
||||
<a class="btn btn-secondary" href="{{ url_for('index') }}">Zur Startseite</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
templates/404.html
Normal file
19
templates/404.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-card">
|
||||
<h1>404 - Seite nicht gefunden</h1>
|
||||
<p>
|
||||
Die angeforderte Seite wurde nicht gefunden.
|
||||
</p>
|
||||
|
||||
<div class="form-actions" style="margin-top: 24px;">
|
||||
<a class="btn" href="{{ url_for('index') }}">Zur Startseite</a>
|
||||
{% if current_user %}
|
||||
<a class="btn btn-secondary" href="{{ url_for('dashboard') }}">Zum Dashboard</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
templates/500.html
Normal file
19
templates/500.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}500 - Interner Serverfehler{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-card">
|
||||
<h1>500 - Interner Serverfehler</h1>
|
||||
<p>
|
||||
Es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut.
|
||||
</p>
|
||||
|
||||
<div class="form-actions" style="margin-top: 24px;">
|
||||
<a class="btn" href="{{ url_for('index') }}">Zur Startseite</a>
|
||||
{% if current_user %}
|
||||
<a class="btn btn-secondary" href="{{ url_for('dashboard') }}">Zum Dashboard</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
261
templates/pdf/assessment_report.html
Normal file
261
templates/pdf/assessment_report.html
Normal file
@ -0,0 +1,261 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Selbsteinschätzung</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm 15mm 22mm 15mm;
|
||||
|
||||
@bottom-center {
|
||||
content: "Seite " counter(page) " / " counter(pages);
|
||||
font-size: 10pt;
|
||||
color: #6b5a4d;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #3c332c;
|
||||
font-size: 11pt;
|
||||
line-height: 1.45;
|
||||
padding-right: 100px; /* Platz für Logo reservieren */
|
||||
}
|
||||
|
||||
.logo-top-right {
|
||||
position: fixed;
|
||||
top: 12mm;
|
||||
right: 5mm;
|
||||
width: 75px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 22pt;
|
||||
color: #8f6437;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16pt;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 11pt;
|
||||
color: #786a5d;
|
||||
}
|
||||
|
||||
.section {
|
||||
border: 1px solid #eadbc8;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 18px;
|
||||
background: #fffaf4;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin-top: 0;
|
||||
color: #8f6437;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #eadbc8;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #eadbc8;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f7f1e8;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.col-center {
|
||||
text-align: center;
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.recommendation-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #eadbc8;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.recommendation-block h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #8f6437;
|
||||
font-size: 12.5pt;
|
||||
}
|
||||
|
||||
.final-image-wrapper {
|
||||
text-align: center;
|
||||
margin-top: 22px;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
page-break-before: auto;
|
||||
}
|
||||
|
||||
.final-image {
|
||||
width: 40%;
|
||||
max-width: 410px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.closing-block {
|
||||
margin-top: 28px;
|
||||
padding: 18px 22px;
|
||||
border: 1px solid #eadbc8;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, #fffaf4 0%, #f8efe2 100%);
|
||||
text-align: center;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.closing-block .closing-headline {
|
||||
font-size: 16pt;
|
||||
font-weight: 700;
|
||||
color: #8f6437;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.closing-block .closing-text {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
margin: 0 auto 10px auto;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.closing-block .closing-link {
|
||||
font-weight: 700;
|
||||
color: #8f6437;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.closing-block .closing-impressum {
|
||||
margin-top: 10px;
|
||||
font-size: 10pt;
|
||||
color: #786a5d;
|
||||
}
|
||||
|
||||
.closing-block .closing-impressum a {
|
||||
color: #8f6437;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img src="{{ logo_up_path }}" class="logo-top-right" alt="Logo">
|
||||
|
||||
<div class="header">
|
||||
<h1>Selbsteinschätzung für {{ branche.branchenname }}</h1>
|
||||
<div class="user-name">{{ user.name }}</div>
|
||||
<div class="date">{{ today_str }}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Grafische Auswertung</h2>
|
||||
<img src="{{ chart_pdf_path }}" alt="Assessment Chart" class="chart">
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Ergebnisübersicht</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Thema</th>
|
||||
<th class="col-center">Anzahl positiver Einschätzungen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td>{{ row.titel }}</td>
|
||||
<td class="col-center">{{ row.ja_anzahl }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if pdf_recommendations %}
|
||||
<div class="section">
|
||||
<h2>Hinweise und Ansprechpartner</h2>
|
||||
|
||||
{% for item in pdf_recommendations %}
|
||||
<div class="recommendation-block">
|
||||
<h3>{{ item.titel }}</h3>
|
||||
|
||||
{% if item.zusatztext %}
|
||||
<p>{{ item.zusatztext }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if item.ansprechpartner %}
|
||||
<p>
|
||||
<strong>Unser Ansprechpartner für Sie:</strong>
|
||||
<a href="mailto:{{ item.ansprechpartner.email }}">
|
||||
{{ item.ansprechpartner.name }}
|
||||
</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<strong>Unser Ansprechpartner für Sie:</strong>
|
||||
aktuell nicht hinterlegt
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="final-image-wrapper">
|
||||
<img src="{{ unternehmer_path }}" class="final-image" alt="Unternehmer">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="closing-block">
|
||||
<div class="closing-headline">
|
||||
Ihr Team der Berater von
|
||||
<a href="https://dasunternehmen.com" class="closing-link">DasUnternehmen.com</a>
|
||||
</div>
|
||||
|
||||
<p class="closing-text">
|
||||
freut sich darauf, Sie auf Ihrem Weg in eine optimierte Zukunft begleiten zu dürfen.
|
||||
</p>
|
||||
|
||||
<div class="closing-impressum">
|
||||
<a href="https://dasunternehmen.com/impressum/">Impressum</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -18,7 +18,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Thema</th>
|
||||
<th>JA-Antworten</th>
|
||||
<th>Positive Einschätzugen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -30,5 +30,12 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="form-actions" style="margin-top: 20px;">
|
||||
<a class="btn"
|
||||
href="{{ url_for('generated_pdf', filename=pdf_file) }}">
|
||||
PDF herunterladen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
15
tools.py
15
tools.py
@ -8,6 +8,16 @@ matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from config import Config
|
||||
|
||||
from pathlib import Path
|
||||
from weasyprint import HTML
|
||||
|
||||
|
||||
def generate_pdf_from_html(html_string, output_path, base_url=None):
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
HTML(string=html_string, base_url=base_url).write_pdf(str(output_path))
|
||||
return output_path
|
||||
|
||||
|
||||
serializer = URLSafeTimedSerializer(Config.SECRET_KEY)
|
||||
|
||||
@ -40,10 +50,13 @@ def send_mail(to_address, subject, body):
|
||||
|
||||
# Normales SMTP, z. B. Port 25
|
||||
with smtplib.SMTP(Config.SMTP_SERVER, Config.SMTP_PORT) as server:
|
||||
server.ehlo()
|
||||
|
||||
if Config.MAIL_USE_TLS:
|
||||
server.starttls(context=ssl.create_default_context())
|
||||
server.ehlo()
|
||||
|
||||
if Config.SMTP_USERNAME:
|
||||
if Config.SMTP_USERNAME and Config.SMTP_USERNAME.strip():
|
||||
server.login(Config.SMTP_USERNAME, Config.SMTP_PASSWORD)
|
||||
|
||||
server.send_message(msg)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user