Compare commits

...

2 Commits

Author SHA1 Message Date
87c3af5dc9 Layout angepasst 2026-04-03 17:31:22 +02:00
69ecc2e1f2 Dokumentenmanagement plus Checkliste 2026-04-03 17:27:17 +02:00
6 changed files with 385 additions and 2 deletions

View File

@ -15,16 +15,18 @@ from flask import (
abort,
)
from werkzeug.security import check_password_hash, generate_password_hash
from werkzeug.utils import secure_filename
from config import Config, COUNTRY_VAT_LABELS
from db import get_connection, fetchone_dict, fetchall_dict
from auth import login_required
from permissions import is_video_allowed_for_level, is_course_allowed_for_level
from permissions import is_video_allowed_for_level, is_course_allowed_for_level, get_allowed_checklist_levels_for_mandant_level
from security import (
admin_required,
get_current_user,
get_current_user_mandant_level,
user_admin_required,
contentmanager_required
)
from logging_config import setup_logging
@ -1071,3 +1073,198 @@ def course_page(course_id, page_number):
page_number=page_number,
**get_current_user()
)
@app.route("/dokumente")
@contentmanager_required
def dokumente():
mandant_id = session.get("mandant_id")
mandant_level = session.get("mandant_level")
allowed_levels = get_allowed_checklist_levels_for_mandant_level(mandant_level)
conn = get_connection()
cur = conn.cursor()
cur.execute("""
SELECT
c.id,
c.level,
c.title,
c.short_description,
c.default_filename,
u.uploaded_at,
u.uploaded_by_user_id,
u.filesize,
u.stored_filename,
u.original_filename,
au.name AS uploaded_by_name
FROM certification_checklist c
LEFT JOIN mandant_checklist_upload u
ON u.checklist_item_id = c.id
AND u.mandant_id = %s
LEFT JOIN app_user au
ON au.id = u.uploaded_by_user_id
WHERE c.level = ANY(%s)
ORDER BY c.level, c.id
""", (mandant_id, allowed_levels))
items = fetchall_dict(cur)
cur.close()
conn.close()
return render_template(
"dokumente.html",
page_title="Dokumente",
active_page="dokumente",
items=items,
**get_current_user()
)
@app.route("/dokumente/upload/<int:item_id>", methods=["POST"])
@contentmanager_required
def dokument_upload(item_id):
mandant_id = session.get("mandant_id")
user_id = session.get("user_id")
uploaded_file = request.files.get("file")
if not uploaded_file or uploaded_file.filename == "":
return redirect(url_for("dokumente"))
conn = get_connection()
cur = conn.cursor()
cur.execute("""
SELECT id, default_filename
FROM certification_checklist
WHERE id = %s
""", (item_id,))
item = fetchone_dict(cur)
if not item:
cur.close()
conn.close()
abort(404)
original_filename = secure_filename(uploaded_file.filename)
final_name_part = item["default_filename"] or original_filename
stored_filename = f"{item_id}-{secure_filename(final_name_part)}"
mandant_dir = os.path.join("/files", str(mandant_id))
os.makedirs(mandant_dir, exist_ok=True)
full_path = os.path.join(mandant_dir, stored_filename)
uploaded_file.save(full_path)
filesize = os.path.getsize(full_path)
cur.execute("""
SELECT stored_filename
FROM mandant_checklist_upload
WHERE mandant_id = %s
AND checklist_item_id = %s
""", (mandant_id, item_id))
existing = cur.fetchone()
if existing:
old_filename = existing[0]
old_path = os.path.join(mandant_dir, old_filename)
if os.path.exists(old_path) and old_filename != stored_filename:
os.remove(old_path)
cur.execute("""
UPDATE mandant_checklist_upload
SET uploaded_at = CURRENT_TIMESTAMP,
uploaded_by_user_id = %s,
filesize = %s,
stored_filename = %s,
original_filename = %s
WHERE mandant_id = %s
AND checklist_item_id = %s
""", (user_id, filesize, stored_filename, original_filename, mandant_id, item_id))
else:
cur.execute("""
INSERT INTO mandant_checklist_upload (
mandant_id,
checklist_item_id,
uploaded_by_user_id,
filesize,
stored_filename,
original_filename
)
VALUES (%s, %s, %s, %s, %s, %s)
""", (mandant_id, item_id, user_id, filesize, stored_filename, original_filename))
conn.commit()
cur.close()
conn.close()
return redirect(url_for("dokumente"))
@app.route("/dokumente/delete/<int:item_id>", methods=["POST"])
@contentmanager_required
def dokument_delete(item_id):
mandant_id = session.get("mandant_id")
mandant_dir = os.path.join("/files", str(mandant_id))
conn = get_connection()
cur = conn.cursor()
cur.execute("""
SELECT stored_filename
FROM mandant_checklist_upload
WHERE mandant_id = %s
AND checklist_item_id = %s
""", (mandant_id, item_id))
row = cur.fetchone()
if row:
stored_filename = row[0]
full_path = os.path.join(mandant_dir, stored_filename)
if os.path.exists(full_path):
os.remove(full_path)
cur.execute("""
DELETE FROM mandant_checklist_upload
WHERE mandant_id = %s
AND checklist_item_id = %s
""", (mandant_id, item_id))
conn.commit()
cur.close()
conn.close()
return redirect(url_for("dokumente"))
@app.route("/dokumente/file/<int:item_id>")
@contentmanager_required
def dokument_file(item_id):
mandant_id = session.get("mandant_id")
conn = get_connection()
cur = conn.cursor()
cur.execute("""
SELECT stored_filename
FROM mandant_checklist_upload
WHERE mandant_id = %s
AND checklist_item_id = %s
""", (mandant_id, item_id))
row = cur.fetchone()
cur.close()
conn.close()
if not row:
abort(404)
stored_filename = row[0]
mandant_dir = os.path.join("/files", str(mandant_id))
return send_from_directory(mandant_dir, stored_filename)
@app.template_filter("datetime")
def format_datetime(value):
if not value:
return "-"
return value.strftime("%d.%m.%Y %H:%M")

View File

@ -52,3 +52,24 @@ def is_course_allowed_for_level(code: str, mandant_level: int | None) -> bool:
return False
def get_allowed_checklist_levels_for_mandant_level(mandant_level: int | None) -> list[int]:
if mandant_level is None:
return []
# 0 = Admin -> alles
if mandant_level == 0:
return [1, 2, 3]
# 1 = Gold -> Bronze + Silber + Gold
if mandant_level == 1:
return [1, 2, 3]
# 2 = Silber -> Bronze + Silber
if mandant_level == 2:
return [1, 2]
# 3 = Bronze -> nur Bronze
if mandant_level == 3:
return [1]
return []

View File

@ -69,6 +69,7 @@ def get_current_user():
"is_logged_in": bool(session.get("user_id")),
"is_admin": user_is_admin() if session.get("user_id") else False,
"is_user_admin": user_is_user_admin() if session.get("user_id") else False,
"is_contentmanager": user_is_contentmanager() if session.get("user_id") else False,
"country": country,
}
@ -121,3 +122,40 @@ def user_admin_required(view_func):
return view_func(*args, **kwargs)
return wrapper
def user_is_contentmanager():
user_id = session.get("user_id")
current_mandant_id = session.get("mandant_id")
if not user_id or not current_mandant_id:
return False
conn = get_connection()
cur = conn.cursor()
cur.execute("""
SELECT 1
FROM user_group ug
JOIN app_group g ON g.id = ug.group_id
WHERE ug.user_id = %s
AND ug.mandant_id = %s
AND g.mandant_id = %s
AND g.group_name = 'Contentmanager'
LIMIT 1
""", (user_id, current_mandant_id, current_mandant_id))
result = cur.fetchone()
cur.close()
conn.close()
return result is not None
def contentmanager_required(view_func):
@wraps(view_func)
def wrapper(*args, **kwargs):
if not session.get("user_id"):
return redirect(url_for("login", next=request.path))
if not user_is_contentmanager():
abort(403)
return view_func(*args, **kwargs)
return wrapper

View File

@ -40,6 +40,9 @@
{% if is_user_admin %}
<a href="/useradmin/mandant">Useradministration</a>
{% endif %}
{% if is_contentmanager %}
<a href="/dokumente">Dokumente</a>
{% endif %}
<a href="/logout">Logout</a>
</div>

View File

@ -0,0 +1,103 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1>Dokumente</h1>
<p class="intro-text">Checkliste der hochzuladenden Zertifizierungsdokumente.</p>
</div>
<section class="admin-section">
<div class="admin-panel">
<div class="table-wrap">
<table class="mandanten-table">
<thead>
<tr>
<th>ID</th>
<th>Titel</th>
<th>Kurzbeschreibung</th>
<th>Datei</th>
<th>Status</th>
<th>Datum</th>
<th>User</th>
<th>Größe</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td class="col-id">{{ item.id }}</td>
<td>{{ item.title }}</td>
<td>{{ item.short_description or "-" }}</td>
<td>
{% if item.stored_filename %}
<a href="/dokumente/file/{{ item.id }}">
{{ item.original_filename or item.stored_filename }}
</a>
{% else %}
-
{% endif %}
</td>
<td>
{% if item.stored_filename %}
<span class="status-ok">✔ OK</span>
{% else %}
<span class="status-missing">Fehlt</span>
{% endif %}
</td>
<!-- 📅 Datum -->
<td>
{% if item.uploaded_at %}
{{ item.uploaded_at | datetime }}
{% else %}
-
{% endif %}
</td>
<!-- 👤 User -->
<td>
{{ item.uploaded_by_name or "-" }}
</td>
<!-- 📦 Filesize -->
<td>
{% if item.filesize %}
{% if item.filesize < 1024 %}
{{ item.filesize }} B
{% elif item.filesize < 1024*1024 %}
{{ (item.filesize / 1024)|round(1) }} KB
{% else %}
{{ (item.filesize / (1024*1024))|round(2) }} MB
{% endif %}
{% else %}
-
{% endif %}
</td>
<td>
<div class="table-actions">
{% if not item.stored_filename %}
<form method="post" action="/dokumente/upload/{{ item.id }}" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit" class="btn-primary btn-small">Upload</button>
</form>
{% else %}
<form method="post" action="/dokumente/delete/{{ item.id }}">
<button type="submit" class="btn-danger btn-small">Delete</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}

View File

@ -719,3 +719,24 @@ button {
justify-content: space-between;
margin-top: 30px;
}
/* =========================
Dokumente
========================= */
.status-ok {
color: #178b35;
font-weight: 700;
}
.status-missing {
color: #b62323;
font-weight: 700;
}
.col-id {
width: 60px;
}
.mandanten-table td,
.mandanten-table th {
vertical-align: middle;
}