feat: Add legal PDF download link in footer navigation

This commit is contained in:
2026-05-23 15:50:37 +02:00
parent 8fd4e1aa6b
commit 28777021ad
2 changed files with 113 additions and 1 deletions
+112
View File
@@ -10,6 +10,8 @@ import shutil
import tarfile import tarfile
import tempfile import tempfile
import threading import threading
import unicodedata
import zipfile
import smtplib import smtplib
from datetime import timedelta, datetime, date from datetime import timedelta, datetime, date
from functools import wraps from functools import wraps
@@ -93,6 +95,31 @@ def set_security_headers(response):
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices") INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
TUTORIAL_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "tutorials") TUTORIAL_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "tutorials")
LEGAL_DOWNLOAD_DIR = os.path.join(BASE_DIR, "static", "downloads", "legal")
LEGAL_DOWNLOAD_FILES = (
(
"nutzungsbedingungen.pdf",
"Nutzungsbedingungen",
[
"Invario UG Modul Suite",
"PDF-Export der Nutzungsbedingungen",
"",
"Die aktuelle HTML-Version finden Sie unter /nutzungsbedingungen.",
"Dieser Download wird vom Server als PDF bereitgestellt.",
],
),
(
"datenschutz.pdf",
"Datenschutzerklaerung",
[
"Invario UG Modul Suite",
"PDF-Export der Datenschutzerklaerung",
"",
"Die aktuelle HTML-Version finden Sie unter /datenschutz.",
"Dieser Download wird vom Server als PDF bereitgestellt.",
],
),
)
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017") MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website") MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
INSTANCE_REPO_URL = os.environ.get("INSTANCE_REPO_URL", "https://github.com/AIIrondev/legendary-octo-garbanzo") INSTANCE_REPO_URL = os.environ.get("INSTANCE_REPO_URL", "https://github.com/AIIrondev/legendary-octo-garbanzo")
@@ -169,6 +196,79 @@ def _is_allowed_tutorial_video_filename(filename: str) -> bool:
return lowered.endswith(".mp4") or lowered.endswith(".webm") or lowered.endswith(".ogv") or lowered.endswith(".ogg") or lowered.endswith(".m4v") return lowered.endswith(".mp4") or lowered.endswith(".webm") or lowered.endswith(".ogv") or lowered.endswith(".ogg") or lowered.endswith(".m4v")
def _pdf_safe_text(value: str) -> str:
text = unicodedata.normalize("NFKD", value or "")
text = text.encode("ascii", "ignore").decode("ascii")
return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
def _build_simple_pdf_bytes(title: str, lines: list[str]) -> bytes:
buffer = BytesIO()
def write(text: str) -> None:
buffer.write(text.encode("latin-1"))
content_lines = ["BT", "/F1 20 Tf", "72 760 Td", f"({_pdf_safe_text(title)}) Tj", "/F1 11 Tf"]
y_position = 730
for line in lines:
if not line:
y_position -= 14
continue
content_lines.append(f"1 0 0 1 72 {y_position} Tm")
content_lines.append(f"({_pdf_safe_text(line)}) Tj")
y_position -= 16
content_lines.append("ET")
content = "\n".join(content_lines).encode("latin-1")
write("%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")
offsets = [0]
def write_object(object_number: int, body: str) -> None:
offsets.append(buffer.tell())
write(f"{object_number} 0 obj\n{body}\nendobj\n")
write_object(1, "<< /Type /Catalog /Pages 2 0 R >>")
write_object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>")
write_object(
3,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
)
write_object(4, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
offsets.append(buffer.tell())
write(f"5 0 obj\n<< /Length {len(content)} >>\nstream\n")
buffer.write(content)
write("\nendstream\nendobj\n")
xref_start = buffer.tell()
write("xref\n0 6\n")
write("0000000000 65535 f \n")
for offset in offsets[1:]:
write(f"{offset:010d} 00000 n \n")
write("trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n")
write(f"{xref_start}\n%%EOF\n")
return buffer.getvalue()
def _ensure_legal_pdf_files() -> None:
os.makedirs(LEGAL_DOWNLOAD_DIR, exist_ok=True)
for filename, title, lines in LEGAL_DOWNLOAD_FILES:
target_path = os.path.join(LEGAL_DOWNLOAD_DIR, filename)
if os.path.exists(target_path):
continue
with open(target_path, "wb") as pdf_file:
pdf_file.write(_build_simple_pdf_bytes(title, lines))
def _build_legal_pdf_bundle() -> BytesIO:
_ensure_legal_pdf_files()
bundle = BytesIO()
with zipfile.ZipFile(bundle, mode="w", compression=zipfile.ZIP_DEFLATED) as archive:
for filename, _, _ in LEGAL_DOWNLOAD_FILES:
archive.write(os.path.join(LEGAL_DOWNLOAD_DIR, filename), arcname=filename)
bundle.seek(0)
return bundle
def _save_invoice_pdf(file_obj, invoice_number: str) -> str | None: def _save_invoice_pdf(file_obj, invoice_number: str) -> str | None:
if not file_obj or not file_obj.filename: if not file_obj or not file_obj.filename:
return None return None
@@ -2689,6 +2789,7 @@ def my_instance_management():
@login_required @login_required
def my_tutorials(): def my_tutorials():
"""Display tutorial videos for the logged-in user.""" """Display tutorial videos for the logged-in user."""
origin = request.url_root.rstrip('/')
tutorials = [ tutorials = [
{ {
"_id": "1", "_id": "1",
@@ -3063,6 +3164,17 @@ def impressum():
def nutzungsbedingungen(): def nutzungsbedingungen():
return render_template("nutzungsbedingungen.html") return render_template("nutzungsbedingungen.html")
@app.route('/downloads/rechtliches')
def download_legal_pdfs_bundle():
bundle = _build_legal_pdf_bundle()
return send_file(
bundle,
mimetype="application/zip",
as_attachment=True,
download_name="invario-rechtliche-dokumente.zip",
)
@app.route('/logout') @app.route('/logout')
def logout(): def logout():
session.pop('username', None) session.pop('username', None)
+1 -1
View File
@@ -611,7 +611,7 @@
<ul class="footer-links"> <ul class="footer-links">
<li><a href="{{ url_for('datenschutz') }}">Datenschutz</a></li> <li><a href="{{ url_for('datenschutz') }}">Datenschutz</a></li>
<li><a href="{{ url_for('impressum') }}">Impressum</a></li> <li><a href="{{ url_for('impressum') }}">Impressum</a></li>
<li><a href="{{ url_for('nutzungsbedingungen') }}">Nutzungsbedingungen</a></li> <li><a href="{{ url_for('download_legal_pdfs_bundle') }}">PDF-Download</a></li>
</ul> </ul>
</div> </div>
</div> </div>