diff --git a/Dockerfile b/Dockerfile index 91ed9bc..ea7b348 100644 --- a/Dockerfile +++ b/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 diff --git a/Kurzanleitung.docx b/Kurzanleitung.docx new file mode 100644 index 0000000..4c39f8d Binary files /dev/null and b/Kurzanleitung.docx differ diff --git a/app.py b/app.py index 44f975d..d7d2ad3 100644 --- a/app.py +++ b/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/") +@login_required +def generated_pdf(filename): + return send_from_directory(pdf_dir, filename, as_attachment=True) @app.route("/generated_charts/") @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) \ No newline at end of file diff --git a/config.py b/config.py index 440b9ec..dcd20dc 100644 --- a/config.py +++ b/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') diff --git a/db.py b/db.py index 3c1dffd..e93e434 100644 --- a/db.py +++ b/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), @@ -759,4 +760,49 @@ def get_all_themen_with_question_count(): GROUP BY t.id, t.kurztitel, t.titel, t.infotext, t.zusatztext 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), ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9d683bf..05a5640 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,14 @@ psycopg2-binary==2.9.10 Werkzeug==3.1.3 itsdangerous==2.2.0 matplotlib==3.10.1 -gunicorn==23.0.0 \ No newline at end of file +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 \ No newline at end of file diff --git a/static/pdf/logo_up.png b/static/pdf/logo_up.png new file mode 100644 index 0000000..61093f3 Binary files /dev/null and b/static/pdf/logo_up.png differ diff --git a/static/pdf/unternehmer.png b/static/pdf/unternehmer.png new file mode 100644 index 0000000..b0c6fb7 Binary files /dev/null and b/static/pdf/unternehmer.png differ diff --git a/templates/401.html b/templates/401.html new file mode 100644 index 0000000..97befc0 --- /dev/null +++ b/templates/401.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}401 - Nicht autorisiert{% endblock %} + +{% block content %} +
+

401 - Nicht autorisiert

+

+ Sie sind für diese Seite nicht autorisiert oder Ihre Anmeldung ist nicht mehr gültig. +

+ + +
+{% endblock %} \ No newline at end of file diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..a7d1cdf --- /dev/null +++ b/templates/404.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}404 - Seite nicht gefunden{% endblock %} + +{% block content %} +
+

404 - Seite nicht gefunden

+

+ Die angeforderte Seite wurde nicht gefunden. +

+ +
+ Zur Startseite + {% if current_user %} + Zum Dashboard + {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..15dddf2 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}500 - Interner Serverfehler{% endblock %} + +{% block content %} +
+

500 - Interner Serverfehler

+

+ Es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut. +

+ +
+ Zur Startseite + {% if current_user %} + Zum Dashboard + {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/pdf/assessment_report.html b/templates/pdf/assessment_report.html new file mode 100644 index 0000000..a3b3d68 --- /dev/null +++ b/templates/pdf/assessment_report.html @@ -0,0 +1,261 @@ + + + + + Selbsteinschätzung + + + + Logo + +
+

Selbsteinschätzung für {{ branche.branchenname }}

+
{{ user.name }}
+
{{ today_str }}
+
+ +
+

Grafische Auswertung

+ Assessment Chart +
+ +
+

Ergebnisübersicht

+ + + + + + + + + {% for row in rows %} + + + + + {% endfor %} + +
ThemaAnzahl positiver Einschätzungen
{{ row.titel }}{{ row.ja_anzahl }}
+
+ + {% if pdf_recommendations %} +
+

Hinweise und Ansprechpartner

+ + {% for item in pdf_recommendations %} +
+

{{ item.titel }}

+ + {% if item.zusatztext %} +

{{ item.zusatztext }}

+ {% endif %} + + {% if item.ansprechpartner %} +

+ Unser Ansprechpartner für Sie: + + {{ item.ansprechpartner.name }} + +

+ {% else %} +

+ Unser Ansprechpartner für Sie: + aktuell nicht hinterlegt +

+ {% endif %} +
+ {% endfor %} +
+ {% endif %} + +
+ Unternehmer +
+ + + +
+
+ Ihr Team der Berater von + DasUnternehmen.com +
+ +

+ freut sich darauf, Sie auf Ihrem Weg in eine optimierte Zukunft begleiten zu dürfen. +

+ +
+ Impressum +
+
+ + \ No newline at end of file diff --git a/templates/result.html b/templates/result.html index 0d01828..c70baf7 100644 --- a/templates/result.html +++ b/templates/result.html @@ -18,7 +18,7 @@ Thema - JA-Antworten + Positive Einschätzugen @@ -30,5 +30,12 @@ {% endfor %} + + {% endblock %} \ No newline at end of file diff --git a/tools.py b/tools.py index 829c184..6e068ac 100644 --- a/tools.py +++ b/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)