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
|
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 .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r 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 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 werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
@ -52,10 +52,15 @@ from db import (
|
|||||||
get_thema_for_branche,
|
get_thema_for_branche,
|
||||||
get_question_count_for_thema,
|
get_question_count_for_thema,
|
||||||
get_all_themen_with_question_count,
|
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 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 = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
@ -63,6 +68,8 @@ app.config.from_object(Config)
|
|||||||
chart_dir = Path("generated_charts")
|
chart_dir = Path("generated_charts")
|
||||||
chart_dir.mkdir(exist_ok=True)
|
chart_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
pdf_dir = Path("generated_pdfs")
|
||||||
|
pdf_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_user():
|
def inject_user():
|
||||||
@ -314,26 +321,80 @@ def assessment_result(assessment_id):
|
|||||||
return redirect(url_for("dashboard"))
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
rows = get_assessment_result_rows(assessment_id, branche_id)
|
rows = get_assessment_result_rows(assessment_id, branche_id)
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
flash("Für diese Branche konnten keine Auswertungsdaten geladen werden.", "warning")
|
flash("Für diese Branche konnten keine Auswertungsdaten geladen werden.", "warning")
|
||||||
return redirect(url_for("dashboard"))
|
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]
|
labels = [row["kurztitel"] for row in rows]
|
||||||
values = [int(row["ja_anzahl"]) for row in rows]
|
values = [int(row["ja_anzahl"]) for row in rows]
|
||||||
|
|
||||||
filename = f"assessment_{assessment_id}_branche_{branche_id}.png"
|
chart_filename = f"assessment_{assessment_id}_branche_{branche_id}.png"
|
||||||
output_path = chart_dir / filename
|
chart_path = chart_dir / chart_filename
|
||||||
create_assessment_chart(labels, values, output_path)
|
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(
|
return render_template(
|
||||||
"result.html",
|
"result.html",
|
||||||
assessment_id=assessment_id,
|
assessment_id=assessment_id,
|
||||||
branche_id=branche_id,
|
branche_id=branche_id,
|
||||||
chart_file=filename,
|
chart_file=chart_filename,
|
||||||
|
pdf_file=pdf_filename,
|
||||||
rows=rows,
|
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>")
|
@app.route("/generated_charts/<path:filename>")
|
||||||
@login_required
|
@login_required
|
||||||
@ -839,5 +900,19 @@ def admin_question_edit_for_thema(thema_id, frage_id):
|
|||||||
min_questions=8,
|
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__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
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_PASSWORD = os.getenv('DB_PASSWORD', 'UnternehmenPWD')
|
||||||
DB_PORT = int(os.getenv('DB_PORT', '5432'))
|
DB_PORT = int(os.getenv('DB_PORT', '5432'))
|
||||||
|
|
||||||
SMTP_SERVER = os.getenv('SMTP_SERVER', 'mail.kolb.cc')
|
SMTP_SERVER = os.getenv("SMTP_SERVER")
|
||||||
SMTP_PORT = int(os.getenv('SMTP_PORT', '25'))
|
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
|
||||||
SMTP_USERNAME = os.getenv('SMTP_USERNAME', '')
|
SMTP_USERNAME = os.getenv("SMTP_USERNAME")
|
||||||
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', '')
|
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
|
||||||
MAIL_SENDER = os.getenv('MAIL_SENDER', 'admin@kolb.cc')
|
MAIL_SENDER = os.getenv("MAIL_SENDER")
|
||||||
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'false').lower() == 'true'
|
MAIL_USE_TLS = os.getenv("MAIL_USE_TLS", "true").lower() == "true"
|
||||||
|
|
||||||
APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://test.dasunternehmen.com')
|
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
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.kurztitel,
|
t.kurztitel,
|
||||||
|
t.titel,
|
||||||
COUNT(*) FILTER (WHERE aa.antwort = TRUE) AS ja_anzahl
|
COUNT(*) FILTER (WHERE aa.antwort = TRUE) AS ja_anzahl
|
||||||
FROM thema t
|
FROM thema t
|
||||||
JOIN branchenthemen bt
|
JOIN branchenthemen bt
|
||||||
@ -421,7 +422,7 @@ def get_assessment_result_rows(assessment_id, branche_id):
|
|||||||
ON aa.thema_id = t.id
|
ON aa.thema_id = t.id
|
||||||
AND aa.assessmentid = %s
|
AND aa.assessmentid = %s
|
||||||
WHERE bt.branche_id = %s
|
WHERE bt.branche_id = %s
|
||||||
GROUP BY t.id, t.kurztitel
|
GROUP BY t.id, t.kurztitel, t.titel
|
||||||
ORDER BY t.id
|
ORDER BY t.id
|
||||||
""",
|
""",
|
||||||
(assessment_id, branche_id),
|
(assessment_id, branche_id),
|
||||||
@ -759,4 +760,49 @@ def get_all_themen_with_question_count():
|
|||||||
GROUP BY t.id, t.kurztitel, t.titel, t.infotext, t.zusatztext
|
GROUP BY t.id, t.kurztitel, t.titel, t.infotext, t.zusatztext
|
||||||
ORDER BY t.id
|
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),
|
||||||
)
|
)
|
||||||
@ -3,4 +3,14 @@ psycopg2-binary==2.9.10
|
|||||||
Werkzeug==3.1.3
|
Werkzeug==3.1.3
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
matplotlib==3.10.1
|
matplotlib==3.10.1
|
||||||
gunicorn==23.0.0
|
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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Thema</th>
|
<th>Thema</th>
|
||||||
<th>JA-Antworten</th>
|
<th>Positive Einschätzugen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -30,5 +30,12 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
15
tools.py
15
tools.py
@ -8,6 +8,16 @@ matplotlib.use('Agg')
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from config import Config
|
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)
|
serializer = URLSafeTimedSerializer(Config.SECRET_KEY)
|
||||||
|
|
||||||
@ -40,10 +50,13 @@ def send_mail(to_address, subject, body):
|
|||||||
|
|
||||||
# Normales SMTP, z. B. Port 25
|
# Normales SMTP, z. B. Port 25
|
||||||
with smtplib.SMTP(Config.SMTP_SERVER, Config.SMTP_PORT) as server:
|
with smtplib.SMTP(Config.SMTP_SERVER, Config.SMTP_PORT) as server:
|
||||||
|
server.ehlo()
|
||||||
|
|
||||||
if Config.MAIL_USE_TLS:
|
if Config.MAIL_USE_TLS:
|
||||||
server.starttls(context=ssl.create_default_context())
|
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.login(Config.SMTP_USERNAME, Config.SMTP_PASSWORD)
|
||||||
|
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user