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 tempfile
import threading
import unicodedata
import zipfile
import smtplib
from datetime import timedelta, datetime, date
from functools import wraps
@@ -93,6 +95,31 @@ def set_security_headers(response):
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
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_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")
@@ -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")
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:
if not file_obj or not file_obj.filename:
return None
@@ -2689,6 +2789,7 @@ def my_instance_management():
@login_required
def my_tutorials():
"""Display tutorial videos for the logged-in user."""
origin = request.url_root.rstrip('/')
tutorials = [
{
"_id": "1",
@@ -3063,6 +3164,17 @@ def impressum():
def nutzungsbedingungen():
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')
def logout():
session.pop('username', None)
+1 -1
View File
@@ -611,7 +611,7 @@
<ul class="footer-links">
<li><a href="{{ url_for('datenschutz') }}">Datenschutz</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>
</div>
</div>