Version 1.0 Basis Verwaltung

This commit is contained in:
Bernhard Kolb 2026-04-09 13:46:56 +02:00
parent 64876a81f4
commit 3446ab11a0
15 changed files with 1505 additions and 274 deletions

603
app.py
View File

@ -1,208 +1,240 @@
from pathlib import Path
from flask import Flask, flash, redirect, render_template, request, session, url_for, send_from_directory
from werkzeug.security import check_password_hash, generate_password_hash
from config import Config
from db import execute, execute_returning, fetch_all, fetch_one
from db import (
get_user_by_id,
get_user_by_email,
get_all_users,
create_user,
activate_user_by_email,
update_user_password,
update_user_last_login,
log_access,
get_user_groups,
create_assessment,
get_next_thema_id,
get_assessment_result_rows,
get_thema_questions,
get_thema_ansprechpartner,
save_assessment_answer,
get_all_themen,
get_thema_by_id,
get_all_ansprechpartner,
get_ansprechpartner_by_id,
get_ansprechpartner_ids_for_thema,
create_thema,
update_thema,
delete_thema,
create_ansprechpartner,
update_ansprechpartner,
delete_ansprechpartner,
get_all_questions_with_thema,
get_all_contacts,
get_question_by_id,
create_question,
update_question,
delete_question,
activate_user,
delete_user,
)
from permissions import admin_required, login_required
from tools import create_assessment_chart, generate_activation_token, send_mail, verify_activation_token
app = Flask(__name__)
app.config.from_object(Config)
chart_dir = Path('generated_charts')
chart_dir = Path("generated_charts")
chart_dir.mkdir(exist_ok=True)
@app.context_processor
def inject_user():
user = None
is_admin = False
if session.get('user_id'):
user = fetch_one('SELECT id, name, email FROM benutzer WHERE id = %s', (session['user_id'],))
admin_row = fetch_one(
'''
SELECT 1
FROM benutzer_gruppen bg
JOIN gruppen g ON g.id = bg.gruppen_id
WHERE bg.benutzer_id = %s AND g.gruppenname = 'Admins'
''',
(session['user_id'],),
)
is_admin = bool(admin_row)
return {'current_user': user, 'is_admin': is_admin}
user_is_admin = False
if session.get("user_id"):
user = get_user_by_id(session["user_id"])
groups = get_user_groups(session["user_id"])
session["groups"] = groups
user_is_admin = "Admins" in groups
return {
"current_user": user,
"is_admin": user_is_admin,
}
@app.route('/')
@app.route("/")
def index():
if session.get('user_id'):
return redirect(url_for('dashboard'))
return render_template('index.html')
if session.get("user_id"):
return redirect(url_for("dashboard"))
return render_template("index.html")
@app.route('/register', methods=['GET', 'POST'])
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == 'POST':
name = request.form['name'].strip()
email = request.form['email'].strip().lower()
password = request.form['password']
if request.method == "POST":
name = request.form["name"].strip()
email = request.form["email"].strip().lower()
password = request.form["password"]
existing = fetch_one('SELECT id FROM benutzer WHERE email = %s', (email,))
existing = get_user_by_email(email)
if existing:
flash('E-Mail ist bereits registriert.', 'danger')
return render_template('register.html')
flash("E-Mail ist bereits registriert.", "danger")
return render_template("register.html")
password_hash = generate_password_hash(password)
execute(
'''
INSERT INTO benutzer (name, email, passwort_hash, is_active)
VALUES (%s, %s, %s, FALSE)
''',
(name, email, password_hash),
)
create_user(name, email, password_hash, is_active=False)
token = generate_activation_token(email)
activation_link = f"{Config.APP_BASE_URL}{url_for('activate_account', token=token)}"
send_mail(
email,
'Account aktivieren',
f'Hallo {name},\n\nbitte aktiviere deinen Account:\n{activation_link}\n',
"Account aktivieren",
f"Hallo {name},\n\nbitte aktiviere deinen Account:\n{activation_link}\n",
)
flash('Registrierung gespeichert. Bitte E-Mail zur Aktivierung prüfen.', 'success')
return redirect(url_for('login'))
return render_template('register.html')
flash("Registrierung gespeichert. Bitte E-Mail zur Aktivierung prüfen.", "success")
return redirect(url_for("login"))
return render_template("register.html")
@app.route('/activate/<token>')
@app.route("/activate/<token>")
def activate_account(token):
try:
email = verify_activation_token(token)
except Exception:
flash('Aktivierungslink ist ungültig oder abgelaufen.', 'danger')
return redirect(url_for('login'))
flash("Aktivierungslink ist ungültig oder abgelaufen.", "danger")
return redirect(url_for("login"))
execute('UPDATE benutzer SET is_active = TRUE WHERE email = %s', (email,))
flash('Account wurde aktiviert. Bitte anmelden.', 'success')
return redirect(url_for('login'))
activate_user_by_email(email)
flash("Account wurde aktiviert. Bitte anmelden.", "success")
return redirect(url_for("login"))
@app.route('/login', methods=['GET', 'POST'])
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == 'POST':
email = request.form['email'].strip().lower()
password = request.form['password']
user = fetch_one('SELECT * FROM benutzer WHERE email = %s', (email,))
if request.method == "POST":
email = request.form["email"].strip().lower()
password = request.form["password"]
user = get_user_by_email(email)
if not user:
flash('Ungültige Zugangsdaten.', 'danger')
return render_template('login.html')
if not user['is_active']:
flash('Account ist noch nicht aktiviert.', 'warning')
return render_template('login.html')
flash("Ungültige Zugangsdaten.", "danger")
return render_template("login.html")
stored_password = user['passwort_hash']
if not user["is_active"]:
flash("Account ist noch nicht aktiviert.", "warning")
return render_template("login.html")
stored_password = user["passwort_hash"]
password_ok = False
if stored_password == 'topsecret' and password == 'topsecret':
if stored_password == "topsecret" and password == "topsecret":
new_hash = generate_password_hash(password)
execute('UPDATE benutzer SET passwort_hash = %s WHERE id = %s', (new_hash, user['id']))
update_user_password(user["id"], new_hash)
password_ok = True
else:
password_ok = check_password_hash(stored_password, password)
if not password_ok:
flash('Ungültige Zugangsdaten.', 'danger')
return render_template('login.html')
flash("Ungültige Zugangsdaten.", "danger")
return render_template("login.html")
session['user_id'] = user['id']
execute('UPDATE benutzer SET last_login = NOW() WHERE id = %s', (user['id'],))
execute('INSERT INTO accesslog (userid) VALUES (%s)', (user['id'],))
return redirect(url_for('dashboard'))
return render_template('login.html')
session["user_id"] = user["id"]
session["groups"] = get_user_groups(user["id"])
update_user_last_login(user["id"])
log_access(user["id"])
return redirect(url_for("dashboard"))
return render_template("login.html")
@app.route('/logout')
@app.route("/logout")
def logout():
session.clear()
flash('Erfolgreich abgemeldet.', 'success')
return redirect(url_for('login'))
flash("Erfolgreich abgemeldet.", "success")
return redirect(url_for("login"))
@app.route('/dashboard')
@app.route("/dashboard")
@login_required
def dashboard():
themen = fetch_all('SELECT * FROM thema ORDER BY id')
return render_template('dashboard.html', themen=themen)
themen = get_all_themen()
return render_template("dashboard.html", themen=themen)
@app.route('/profil', methods=['GET', 'POST'])
@app.route("/profil", methods=["GET", "POST"])
@login_required
def profile():
user = fetch_one('SELECT id, name, email FROM benutzer WHERE id = %s', (session['user_id'],))
if request.method == 'POST':
new_password = request.form['password']
execute(
'UPDATE benutzer SET passwort_hash = %s WHERE id = %s',
(generate_password_hash(new_password), session['user_id'])
)
flash('Passwort wurde geändert.', 'success')
return redirect(url_for('profile'))
return render_template('profile.html', user=user)
user = get_user_by_id(session["user_id"])
if request.method == "POST":
new_password = request.form["password"].strip()
if not new_password:
flash("Bitte ein Passwort eingeben.", "warning")
return render_template("profile.html", user=user)
update_user_password(session["user_id"], generate_password_hash(new_password))
flash("Passwort wurde geändert.", "success")
return redirect(url_for("profile"))
return render_template("profile.html", user=user)
@app.route('/thema/<int:thema_id>', methods=['GET', 'POST'])
@app.route("/thema/<int:thema_id>", methods=["GET", "POST"])
@login_required
def topic(thema_id):
thema = fetch_one('SELECT * FROM thema WHERE id = %s', (thema_id,))
fragen = fetch_all('SELECT * FROM fragen WHERE themaid = %s ORDER BY id', (thema_id,))
ansprechpartner = fetch_all(
'''
SELECT a.*
FROM ansprechpartner a
JOIN themaansprechpartner ta ON ta.ansprechpartnerid = a.id
WHERE ta.themaid = %s
ORDER BY a.name
''',
(thema_id,),
)
thema = get_thema_by_id(thema_id)
if not thema:
flash("Thema nicht gefunden.", "danger")
return redirect(url_for("dashboard"))
fragen = get_thema_questions(thema_id)
ansprechpartner = get_thema_ansprechpartner(thema_id)
if request.method == "POST":
assessment_id = request.form.get("assessment_id")
if request.method == 'POST':
assessment_id = request.form.get('assessment_id')
if not assessment_id:
assessment = execute_returning(
'INSERT INTO assessment (userid) VALUES (%s) RETURNING id',
(session['user_id'],),
)
assessment_id = assessment['id']
assessment_id = create_assessment(session["user_id"])
for frage in fragen:
value = request.form.get(f'frage_{frage["id"]}')
if value not in ('ja', 'nein'):
flash('Bitte alle Fragen beantworten.', 'warning')
if value not in ("ja", "nein"):
flash("Bitte alle Fragen beantworten.", "warning")
return render_template(
'topic.html',
"topic.html",
thema=thema,
fragen=fragen,
ansprechpartner=ansprechpartner,
assessment_id=assessment_id,
)
execute(
'''
INSERT INTO assessmentanswer (assessmentid, themaid, frageid, antwort)
VALUES (%s, %s, %s, %s)
ON CONFLICT (assessmentid, frageid)
DO UPDATE SET antwort = EXCLUDED.antwort
''',
(assessment_id, thema_id, frage['id'], value == 'ja'),
save_assessment_answer(
assessment_id=assessment_id,
thema_id=thema_id,
frage_id=frage["id"],
antwort=(value == "ja"),
)
next_topic = fetch_one('SELECT id FROM thema WHERE id > %s ORDER BY id LIMIT 1', (thema_id,))
if next_topic:
return redirect(url_for('topic', thema_id=next_topic['id'], assessment_id=assessment_id))
return redirect(url_for('assessment_result', assessment_id=assessment_id))
next_thema_id = get_next_thema_id(thema_id)
if next_thema_id:
return redirect(url_for("topic", thema_id=next_thema_id, assessment_id=assessment_id))
assessment_id = request.args.get('assessment_id', '')
return redirect(url_for("assessment_result", assessment_id=assessment_id))
assessment_id = request.args.get("assessment_id", "")
return render_template(
'topic.html',
"topic.html",
thema=thema,
fragen=fragen,
ansprechpartner=ansprechpartner,
@ -210,61 +242,328 @@ def topic(thema_id):
)
@app.route('/assessment/<int:assessment_id>/result')
@app.route("/assessment/<int:assessment_id>/result")
@login_required
def assessment_result(assessment_id):
rows = fetch_all(
'''
SELECT t.kurztitel, COUNT(*) FILTER (WHERE aa.antwort = TRUE) AS ja_anzahl
FROM thema t
LEFT JOIN assessmentanswer aa ON aa.themaid = t.id AND aa.assessmentid = %s
GROUP BY t.id, t.kurztitel
ORDER BY t.id
''',
(assessment_id,),
)
labels = [row['kurztitel'] for row in rows]
values = [int(row['ja_anzahl']) for row in rows]
filename = f'assessment_{assessment_id}.png'
rows = get_assessment_result_rows(assessment_id)
labels = [row["kurztitel"] for row in rows]
values = [int(row["ja_anzahl"]) for row in rows]
filename = f"assessment_{assessment_id}.png"
output_path = chart_dir / filename
create_assessment_chart(labels, values, output_path)
return render_template('result.html', assessment_id=assessment_id, chart_file=filename, rows=rows)
return render_template(
"result.html",
assessment_id=assessment_id,
chart_file=filename,
rows=rows,
)
@app.route('/generated_charts/<path:filename>')
@app.route("/generated_charts/<path:filename>")
@login_required
def generated_chart(filename):
return send_from_directory(chart_dir, filename)
@app.route('/admin')
@app.route("/admin")
@admin_required
def admin_index():
return render_template('admin/index.html')
return render_template("admin/index.html")
@app.route('/admin/themen')
@app.route("/admin/user")
@admin_required
def admin_topics():
themen = fetch_all('SELECT * FROM thema ORDER BY id')
return render_template('admin/topics.html', themen=themen)
def admin_users():
users = get_all_users()
return render_template("admin/users.html", users=users)
@app.route('/admin/fragen')
@app.route("/admin/themen")
@admin_required
def admin_questions():
fragen = fetch_all(
'''SELECT f.id, f.text, t.kurztitel FROM fragen f JOIN thema t ON t.id = f.themaid ORDER BY t.id, f.id'''
def admin_themen():
themen = get_all_themen()
return render_template("admin/themen_list.html", themen=themen)
@app.route("/admin/themen/new", methods=["GET", "POST"])
@admin_required
def admin_thema_new():
ansprechpartner = get_all_ansprechpartner()
if request.method == "POST":
kurztitel = request.form.get("kurztitel", "").strip()
titel = request.form.get("titel", "").strip()
infotext = request.form.get("infotext", "").strip()
zusatztext = request.form.get("zusatztext", "").strip()
ansprechpartner_ids = [int(x) for x in request.form.getlist("ansprechpartner_ids")]
if not kurztitel or not titel:
flash("Kurztitel und Titel sind Pflichtfelder.", "error")
return render_template(
"admin/thema_form.html",
mode="new",
thema=request.form,
ansprechpartner=ansprechpartner,
selected_ansprechpartner_ids=ansprechpartner_ids,
)
create_thema(kurztitel, titel, infotext, zusatztext, ansprechpartner_ids)
flash("Thema wurde erstellt.", "success")
return redirect(url_for("admin_themen"))
return render_template(
"admin/thema_form.html",
mode="new",
thema={},
ansprechpartner=ansprechpartner,
selected_ansprechpartner_ids=[],
)
return render_template('admin/questions.html', fragen=fragen)
@app.route('/admin/ansprechpartner')
@app.route("/admin/themen/<int:thema_id>/edit", methods=["GET", "POST"])
@admin_required
def admin_thema_edit(thema_id):
thema = get_thema_by_id(thema_id)
if not thema:
flash("Thema nicht gefunden.", "error")
return redirect(url_for("admin_themen"))
ansprechpartner = get_all_ansprechpartner()
if request.method == "POST":
kurztitel = request.form.get("kurztitel", "").strip()
titel = request.form.get("titel", "").strip()
infotext = request.form.get("infotext", "").strip()
zusatztext = request.form.get("zusatztext", "").strip()
ansprechpartner_ids = [int(x) for x in request.form.getlist("ansprechpartner_ids")]
if not kurztitel or not titel:
flash("Kurztitel und Titel sind Pflichtfelder.", "error")
thema_form = {
"id": thema_id,
"kurztitel": kurztitel,
"titel": titel,
"infotext": infotext,
"zusatztext": zusatztext,
}
return render_template(
"admin/thema_form.html",
mode="edit",
thema=thema_form,
ansprechpartner=ansprechpartner,
selected_ansprechpartner_ids=ansprechpartner_ids,
)
update_thema(thema_id, kurztitel, titel, infotext, zusatztext, ansprechpartner_ids)
flash("Thema wurde gespeichert.", "success")
return redirect(url_for("admin_themen"))
selected_ansprechpartner_ids = get_ansprechpartner_ids_for_thema(thema_id)
return render_template(
"admin/thema_form.html",
mode="edit",
thema=thema,
ansprechpartner=ansprechpartner,
selected_ansprechpartner_ids=selected_ansprechpartner_ids,
)
@app.route("/admin/themen/<int:thema_id>/delete", methods=["POST"])
@admin_required
def admin_thema_delete(thema_id):
delete_thema(thema_id)
flash("Thema wurde gelöscht.", "success")
return redirect(url_for("admin_themen"))
@app.route("/admin/ansprechpartner")
@admin_required
def admin_contacts():
contacts = fetch_all('SELECT * FROM ansprechpartner ORDER BY name')
return render_template('admin/contacts.html', contacts=contacts)
contacts = get_all_contacts()
return render_template("admin/contacts.html", contacts=contacts)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
@app.route("/admin/ansprechpartner/new", methods=["GET", "POST"])
@admin_required
def admin_contact_new():
if request.method == "POST":
name = request.form.get("name", "").strip()
email = request.form.get("email", "").strip()
infotext = request.form.get("infotext", "").strip()
if not name or not email:
flash("Name und E-Mail sind Pflichtfelder.", "error")
return render_template(
"admin/contact_form.html",
mode="new",
contact=request.form,
)
create_ansprechpartner(name, email, infotext)
flash("Ansprechpartner wurde erstellt.", "success")
return redirect(url_for("admin_contacts"))
return render_template(
"admin/contact_form.html",
mode="new",
contact={},
)
@app.route("/admin/ansprechpartner/<int:ansprechpartner_id>/edit", methods=["GET", "POST"])
@admin_required
def admin_contact_edit(ansprechpartner_id):
contact = get_ansprechpartner_by_id(ansprechpartner_id)
if not contact:
flash("Ansprechpartner nicht gefunden.", "error")
return redirect(url_for("admin_contacts"))
if request.method == "POST":
name = request.form.get("name", "").strip()
email = request.form.get("email", "").strip()
infotext = request.form.get("infotext", "").strip()
if not name or not email:
flash("Name und E-Mail sind Pflichtfelder.", "error")
contact_form = {
"id": ansprechpartner_id,
"name": name,
"email": email,
"infotext": infotext,
}
return render_template(
"admin/contact_form.html",
mode="edit",
contact=contact_form,
)
update_ansprechpartner(ansprechpartner_id, name, email, infotext)
flash("Ansprechpartner wurde gespeichert.", "success")
return redirect(url_for("admin_contacts"))
return render_template(
"admin/contact_form.html",
mode="edit",
contact=contact,
)
@app.route("/admin/ansprechpartner/<int:ansprechpartner_id>/delete", methods=["POST"])
@admin_required
def admin_contact_delete(ansprechpartner_id):
delete_ansprechpartner(ansprechpartner_id)
flash("Ansprechpartner wurde gelöscht.", "success")
return redirect(url_for("admin_contacts"))
@app.route("/admin/fragen")
@admin_required
def admin_questions():
fragen = get_all_questions_with_thema()
themen = get_all_themen()
return render_template("admin/questions.html", fragen=fragen, themen=themen)
@app.route("/admin/fragen/new", methods=["GET", "POST"])
@admin_required
def admin_question_new():
themen = get_all_themen()
if request.method == "POST":
thema_id = request.form.get("thema_id")
text = request.form.get("text", "").strip()
if not thema_id or not text:
flash("Thema und Text sind Pflichtfelder.", "error")
return render_template(
"admin/question_form.html",
mode="new",
frage=request.form,
themen=themen,
)
create_question(thema_id, text)
flash("Frage wurde erstellt.", "success")
return redirect(url_for("admin_questions"))
return render_template(
"admin/question_form.html",
mode="new",
frage={},
themen=themen,
)
@app.route("/admin/fragen/<int:frage_id>/edit", methods=["GET", "POST"])
@admin_required
def admin_question_edit(frage_id):
frage = get_question_by_id(frage_id)
themen = get_all_themen()
if not frage:
flash("Frage nicht gefunden.", "error")
return redirect(url_for("admin_questions"))
if request.method == "POST":
thema_id = request.form.get("thema_id")
text = request.form.get("text", "").strip()
if not thema_id or not text:
frage_form = {
"id": frage_id,
"thema_id": thema_id,
"text": text,
}
flash("Thema und Text sind Pflichtfelder.", "error")
return render_template(
"admin/question_form.html",
mode="edit",
frage=frage_form,
themen=themen,
)
update_question(frage_id, thema_id, text)
flash("Frage wurde gespeichert.", "success")
return redirect(url_for("admin_questions"))
return render_template(
"admin/question_form.html",
mode="edit",
frage=frage,
themen=themen,
)
@app.route("/admin/fragen/<int:frage_id>/delete", methods=["POST"])
@admin_required
def admin_question_delete(frage_id):
delete_question(frage_id)
flash("Frage wurde gelöscht.", "success")
return redirect(url_for("admin_questions"))
@app.route("/admin/user/<int:user_id>/activate", methods=["POST"])
@admin_required
def admin_user_activate(user_id):
if user_id == session.get("user_id"):
flash("Der aktuell angemeldete Benutzer ist bereits aktiv.", "warning")
return redirect(url_for("admin_users"))
activate_user(user_id)
flash("Benutzer wurde aktiviert.", "success")
return redirect(url_for("admin_users"))
@app.route("/admin/user/<int:user_id>/delete", methods=["POST"])
@admin_required
def admin_user_delete(user_id):
if user_id == session.get("user_id"):
flash("Du kannst deinen eigenen Benutzer nicht löschen.", "danger")
return redirect(url_for("admin_users"))
delete_user(user_id)
flash("Benutzer wurde gelöscht.", "success")
return redirect(url_for("admin_users"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

View File

@ -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', 'smtp.example.com')
SMTP_PORT = int(os.getenv('SMTP_PORT', '587'))
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', 'noreply@example.com')
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'true').lower() == 'true'
MAIL_SENDER = os.getenv('MAIL_SENDER', 'admin@kolb.cc')
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'false').lower() == 'true'
APP_BASE_URL = os.getenv('APP_BASE_URL', 'http://localhost:5050')
APP_BASE_URL = os.getenv('APP_BASE_URL', 'http://localhost:8086')

441
db.py
View File

@ -52,3 +52,444 @@ def execute_returning(query, params=None):
with get_cursor(commit=True) as cur:
cur.execute(query, params or ())
return cur.fetchone()
###############
# Benutzer
###############
def get_user_by_id(user_id):
return fetch_one(
"""
SELECT id, name, email, passwort_hash, is_active, last_login
FROM benutzer
WHERE id = %s
""",
(user_id,),
)
def get_user_by_email(email):
return fetch_one(
"""
SELECT id, name, email, passwort_hash, is_active, last_login
FROM benutzer
WHERE email = %s
""",
(email,),
)
def get_all_users():
return fetch_all(
"""
SELECT id, name, email, last_login, is_active
FROM benutzer
ORDER BY name, email
"""
)
def create_user(name, email, password_hash, is_active=False):
return execute_returning(
"""
INSERT INTO benutzer (name, email, passwort_hash, is_active)
VALUES (%s, %s, %s, %s)
RETURNING id
""",
(name, email, password_hash, is_active),
)
def activate_user_by_email(email):
execute(
"""
UPDATE benutzer
SET is_active = TRUE
WHERE email = %s
""",
(email,),
)
def update_user_password(user_id, password_hash):
execute(
"""
UPDATE benutzer
SET passwort_hash = %s
WHERE id = %s
""",
(password_hash, user_id),
)
def update_user_last_login(user_id):
execute(
"""
UPDATE benutzer
SET last_login = NOW()
WHERE id = %s
""",
(user_id,),
)
def log_access(user_id):
execute(
"""
INSERT INTO accesslog (user_id)
VALUES (%s)
""",
(user_id,),
)
def get_user_groups(user_id):
rows = fetch_all(
"""
SELECT g.gruppenname
FROM benutzer_gruppen bg
JOIN gruppen g ON g.id = bg.gruppen_id
WHERE bg.benutzer_id = %s
ORDER BY g.gruppenname
""",
(user_id,),
)
return [row["gruppenname"] for row in rows]
###############
# Themen
###############
def get_all_themen():
return fetch_all(
"""
SELECT id, kurztitel, titel, infotext, zusatztext
FROM thema
ORDER BY id
"""
)
def get_thema_by_id(thema_id):
return fetch_one(
"""
SELECT id, kurztitel, titel, infotext, zusatztext
FROM thema
WHERE id = %s
""",
(thema_id,),
)
def get_next_thema_id(current_thema_id):
row = fetch_one(
"""
SELECT id
FROM thema
WHERE id > %s
ORDER BY id
LIMIT 1
""",
(current_thema_id,),
)
return row["id"] if row else None
def get_all_ansprechpartner():
return fetch_all(
"""
SELECT id, name, email, infotext
FROM ansprechpartner
ORDER BY name ASC
"""
)
def get_all_contacts():
return fetch_all(
"""
SELECT id, name, email, infotext
FROM ansprechpartner
ORDER BY name ASC
"""
)
def get_thema_ansprechpartner(thema_id):
return fetch_all(
"""
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 a.name ASC
""",
(thema_id,),
)
def get_ansprechpartner_ids_for_thema(thema_id):
rows = fetch_all(
"""
SELECT ansprechpartner_id
FROM themaansprechpartner
WHERE thema_id = %s
""",
(thema_id,),
)
return [row["ansprechpartner_id"] for row in rows]
def create_thema(kurztitel, titel, infotext, zusatztext, ansprechpartner_ids):
row = execute_returning(
"""
INSERT INTO thema (kurztitel, titel, infotext, zusatztext)
VALUES (%s, %s, %s, %s)
RETURNING id
""",
(kurztitel, titel, infotext, zusatztext),
)
thema_id = row["id"]
for ap_id in ansprechpartner_ids:
execute(
"""
INSERT INTO themaansprechpartner (thema_id, ansprechpartner_id)
VALUES (%s, %s)
""",
(thema_id, ap_id),
)
return thema_id
def update_thema(thema_id, kurztitel, titel, infotext, zusatztext, ansprechpartner_ids):
execute(
"""
UPDATE thema
SET kurztitel = %s,
titel = %s,
infotext = %s,
zusatztext = %s
WHERE id = %s
""",
(kurztitel, titel, infotext, zusatztext, thema_id),
)
execute(
"""
DELETE FROM themaansprechpartner
WHERE thema_id = %s
""",
(thema_id,),
)
for ap_id in ansprechpartner_ids:
execute(
"""
INSERT INTO themaansprechpartner (thema_id, ansprechpartner_id)
VALUES (%s, %s)
""",
(thema_id, ap_id),
)
def delete_thema(thema_id):
execute("DELETE FROM themaansprechpartner WHERE thema_id = %s", (thema_id,))
execute("DELETE FROM fragen WHERE thema_id = %s", (thema_id,))
execute("DELETE FROM thema WHERE id = %s", (thema_id,))
###############
# Fragen
###############
def get_thema_questions(thema_id):
return fetch_all(
"""
SELECT id, thema_id, text
FROM fragen
WHERE thema_id = %s
ORDER BY id
""",
(thema_id,),
)
def get_all_questions_with_thema():
return fetch_all(
"""
SELECT f.id, f.text, f.thema_id, t.kurztitel, t.titel
FROM fragen f
JOIN thema t ON t.id = f.thema_id
ORDER BY t.id, f.id
"""
)
###############
# Assessment
###############
def create_assessment(user_id):
row = execute_returning(
"""
INSERT INTO assessment (user_id)
VALUES (%s)
RETURNING id
""",
(user_id,),
)
return row["id"]
def save_assessment_answer(assessment_id, thema_id, frage_id, antwort):
execute(
"""
INSERT INTO assessmentanswer (assessmentid, thema_id, frage_id, antwort)
VALUES (%s, %s, %s, %s)
ON CONFLICT (assessmentid, frage_id)
DO UPDATE SET antwort = EXCLUDED.antwort
""",
(assessment_id, thema_id, frage_id, antwort),
)
def get_assessment_result_rows(assessment_id):
return fetch_all(
"""
SELECT t.kurztitel, COUNT(*) FILTER (WHERE aa.antwort = TRUE) AS ja_anzahl
FROM thema t
LEFT JOIN assessmentanswer aa
ON aa.thema_id = t.id
AND aa.assessmentid = %s
GROUP BY t.id, t.kurztitel
ORDER BY t.id
""",
(assessment_id,),
)
############
# Ansprechpartner
############
def get_ansprechpartner_by_id(ansprechpartner_id):
return fetch_one(
"""
SELECT id, name, email, infotext
FROM ansprechpartner
WHERE id = %s
""",
(ansprechpartner_id,),
)
def create_ansprechpartner(name, email, infotext):
row = execute_returning(
"""
INSERT INTO ansprechpartner (name, email, infotext)
VALUES (%s, %s, %s)
RETURNING id
""",
(name, email, infotext),
)
return row["id"]
def update_ansprechpartner(ansprechpartner_id, name, email, infotext):
execute(
"""
UPDATE ansprechpartner
SET name = %s,
email = %s,
infotext = %s
WHERE id = %s
""",
(name, email, infotext, ansprechpartner_id),
)
def delete_ansprechpartner(ansprechpartner_id):
execute(
"""
DELETE FROM themaansprechpartner
WHERE ansprechpartner_id = %s
""",
(ansprechpartner_id,),
)
execute(
"""
DELETE FROM ansprechpartner
WHERE id = %s
""",
(ansprechpartner_id,),
)
###############
# Fragen
###############
def get_question_by_id(frage_id):
return fetch_one(
"""
SELECT id, thema_id, text
FROM fragen
WHERE id = %s
""",
(frage_id,),
)
def create_question(thema_id, text):
return execute_returning(
"""
INSERT INTO fragen (thema_id, text)
VALUES (%s, %s)
RETURNING id
""",
(thema_id, text),
)
def update_question(frage_id, thema_id, text):
execute(
"""
UPDATE fragen
SET thema_id = %s,
text = %s
WHERE id = %s
""",
(thema_id, text, frage_id),
)
def delete_question(frage_id):
execute(
"DELETE FROM fragen WHERE id = %s",
(frage_id,),
)
##########
# usermanagement
##########
def activate_user(user_id):
execute(
"""
UPDATE benutzer
SET is_active = TRUE
WHERE id = %s
""",
(user_id,),
)
def delete_user(user_id, current_user_id=None):
if current_user_id is not None and int(user_id) == int(current_user_id):
raise ValueError("Der aktuell angemeldete Benutzer kann nicht gelöscht werden.")
execute(
"""
DELETE FROM benutzer
WHERE id = %s
""",
(user_id,),
)

View File

@ -1,38 +1,23 @@
from functools import wraps
from flask import session, redirect, url_for, flash
from db import fetch_one
from flask import flash, redirect, session, url_for
def login_required(view_func):
@wraps(view_func)
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not session.get('user_id'):
flash('Bitte zuerst anmelden.', 'warning')
return redirect(url_for('login'))
return view_func(*args, **kwargs)
if not session.get("user_id"):
flash("Bitte zuerst anmelden.", "warning")
return redirect(url_for("login"))
return func(*args, **kwargs)
return wrapper
def admin_required(view_func):
@wraps(view_func)
def admin_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
user_id = session.get('user_id')
if not user_id:
flash('Bitte zuerst anmelden.', 'warning')
return redirect(url_for('login'))
row = fetch_one(
'''
SELECT 1
FROM benutzer_gruppen bg
JOIN gruppen g ON g.id = bg.gruppen_id
WHERE bg.benutzer_id = %s AND g.gruppenname = 'Admins'
''',
(user_id,),
)
if not row:
flash('Keine Berechtigung für diesen Bereich.', 'danger')
return redirect(url_for('dashboard'))
return view_func(*args, **kwargs)
return wrapper
groups = session.get("groups", [])
if "Admins" not in groups:
flash("Keine Berechtigung.", "danger")
return redirect(url_for("dashboard"))
return func(*args, **kwargs)
return wrapper

View File

@ -9,52 +9,107 @@
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
color: var(--text);
background: linear-gradient(180deg, #fbf7f1, var(--bg));
}
.container, .page-wrap {
width: min(1100px, calc(100% - 32px));
margin: 0 auto;
}
.page-wrap { padding: 24px 0 48px; }
.page-wrap {
padding: 24px 0 48px;
}
.site-header, .site-footer {
background: rgba(255,255,255,0.6);
backdrop-filter: blur(6px);
border-bottom: 1px solid var(--border);
}
.site-footer { border-top: 1px solid var(--border); border-bottom: 0; padding: 20px 0; margin-top: 32px; }
.site-footer {
border-top: 1px solid var(--border);
border-bottom: 0;
padding: 20px 0;
margin-top: 32px;
}
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 0;
}
.brand {
font-size: 1.4rem;
font-weight: 700;
color: var(--accent-dark);
text-decoration: none;
}
.main-nav { display: flex; gap: 16px; align-items: center; }
.main-nav a, .user-menu span { color: var(--text); text-decoration: none; }
.user-menu { position: relative; padding: 10px 14px; background: var(--panel); border-radius: 999px; border: 1px solid var(--border); }
.user-menu:hover .dropdown { display: block; }
.main-nav {
display: flex;
gap: 16px;
align-items: center;
}
.main-nav a {
color: var(--text);
text-decoration: none;
}
.user-menu {
position: relative;
display: inline-block;
padding: 10px 14px;
background: var(--panel);
border-radius: 999px;
border: 1px solid var(--border);
}
.user-menu span,
.user-menu > a {
color: var(--text);
text-decoration: none;
}
.user-menu:hover .dropdown,
.user-menu:focus-within .dropdown {
display: block;
}
.dropdown {
display: none;
position: absolute;
top: calc(100% + 8px);
top: 100%;
right: 0;
min-width: 180px;
margin-top: 0;
min-width: 220px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 18px;
border-radius: 20px;
overflow: hidden;
z-index: 1000;
}
.dropdown a { display: block; padding: 12px 14px; }
.dropdown a:hover { background: var(--soft); }
.dropdown a {
display: block;
padding: 14px 16px;
color: var(--text);
text-decoration: none;
white-space: nowrap;
}
.dropdown a:hover {
background: var(--soft);
}
.card {
background: var(--panel);
border: 1px solid var(--border);
@ -63,9 +118,19 @@ body {
margin-bottom: 20px;
box-shadow: 0 15px 40px rgba(80, 58, 34, 0.08);
}
.hero-card { padding: 40px 32px; }
.form-card { max-width: 560px; }
input[type="text"], input[type="email"], input[type="password"] {
.hero-card {
padding: 40px 32px;
}
.form-card {
max-width: 560px;
}
input[type="text"],
input[type="email"],
input[type="password"],
textarea {
width: 100%;
margin: 8px 0 18px;
padding: 14px 16px;
@ -73,35 +138,92 @@ input[type="text"], input[type="email"], input[type="password"] {
border: 1px solid var(--border);
background: #fff;
}
.btn {
display: inline-block;
padding: 12px 18px;
border-radius: 16px;
background: var(--accent);
color: #fff;
padding: 10px 18px;
border: 1px solid #cdb693;
border-radius: 999px;
background: #efe3d1;
color: #3f342c;
text-decoration: none;
border: 0;
cursor: pointer;
}
.btn-secondary { background: var(--accent-dark); }
.button-row { display: flex; flex-wrap: wrap; gap: 12px; }
.btn-secondary {
background: #f7f3ee;
}
.btn-danger {
background: #d86a5f;
border-color: #d86a5f;
color: #fff;
}
.btn-small {
padding: 8px 14px;
font-size: 14px;
}
.button-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.topic-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 18px;
}
.topic-box, .question-box, .contact-box {
.topic-box,
.question-box,
.contact-box {
background: #fff;
border: 1px solid var(--border);
border-radius: 20px;
padding: 18px;
}
.question-box { margin-bottom: 16px; }
.radio-row { display: flex; gap: 18px; }
.result-chart { width: 100%; max-width: 1000px; border-radius: 18px; border: 1px solid var(--border); }
.result-table { width: 100%; border-collapse: collapse; margin-top: 18px; }
.result-table th, .result-table td { padding: 10px; border-bottom: 1px solid var(--border); text-align: left; }
.flash-wrapper { margin-bottom: 16px; }
.topic-box {
text-decoration: none;
color: var(--text);
}
.question-box {
margin-bottom: 16px;
}
.radio-row {
display: flex;
gap: 18px;
}
.result-chart {
width: 100%;
max-width: 1000px;
border-radius: 18px;
border: 1px solid var(--border);
}
.result-table {
width: 100%;
border-collapse: collapse;
margin-top: 18px;
}
.result-table th,
.result-table td {
padding: 10px;
border-bottom: 1px solid var(--border);
text-align: left;
}
.flash-wrapper {
margin-bottom: 16px;
}
.flash {
padding: 14px 16px;
border-radius: 16px;
@ -109,23 +231,87 @@ input[type="text"], input[type="email"], input[type="password"] {
border: 1px solid var(--border);
background: #fff;
}
.flash.success { border-color: #9fc89f; }
.flash.warning { border-color: #e0b86d; }
.flash.danger { border-color: #d89f9f; }
.muted { color: #786a5d; }
.flash.danger,
.flash.error { border-color: #d89f9f; }
.muted {
color: #786a5d;
}
.content-card {
background: #fff;
border: 1px solid #dccdb7;
border-radius: 28px;
padding: 24px;
box-shadow: 0 8px 24px rgba(0,0,0,0.05);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.admin-table {
width: 100%;
border-collapse: collapse;
}
.admin-table th,
.admin-table td {
padding: 12px 10px;
border-bottom: 1px solid #e7dccd;
text-align: left;
vertical-align: top;
}
.actions {
white-space: nowrap;
}
.admin-form .form-group {
margin-bottom: 18px;
}
.admin-form textarea {
min-height: 120px;
resize: vertical;
}
.checkbox-list {
display: grid;
gap: 10px;
padding: 14px;
border: 1px solid #d8c7ae;
border-radius: 16px;
background: #fffdf9;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 10px;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 24px;
}
@media (max-width: 700px) {
.nav-bar { flex-direction: column; gap: 12px; }
}
.nav-bar {
flex-direction: column;
gap: 12px;
}
.user-menu {
position: relative;
display: inline-block;
}
.user-menu-dropdown {
position: absolute;
top: 100%; /* direkt unter dem Button */
left: 0;
margin-top: 0; /* GANZ WICHTIG */
padding-top: 0; /* falls vorhanden */
.page-header {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block content %}
<div class="content-card">
<h1>
{% if mode == "edit" %}
Ansprechpartner bearbeiten
{% else %}
Neuen Ansprechpartner erstellen
{% endif %}
</h1>
<form method="post" class="admin-form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" value="{{ contact.name or '' }}" required>
</div>
<div class="form-group">
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" value="{{ contact.email or '' }}" required>
</div>
<div class="form-group">
<label for="infotext">InfoText</label>
<textarea id="infotext" name="infotext" rows="6">{{ contact.infotext or '' }}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn">Speichern</button>
<a class="btn btn-secondary" href="{{ url_for('admin_contacts') }}">Abbrechen</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -1,13 +1,46 @@
{% extends 'base.html' %}
{% block title %}Admin Ansprechpartner{% endblock %}
{% extends "base.html" %}
{% block content %}
<section class="card">
<h1>Ansprechpartner</h1>
<p>CRUD folgt im nächsten Schritt. Aktuell Listenansicht als Platzhalter.</p>
<ul>
{% for item in contacts %}
<li>{{ item.name }} {{ item.email }}</li>
{% endfor %}
</ul>
</section>
{% endblock %}
<div class="content-card">
<div class="page-header">
<h1>Ansprechpartner</h1>
<a class="btn" href="{{ url_for('admin_contact_new') }}">Neuen Ansprechpartner erstellen</a>
</div>
{% if contacts %}
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>E-Mail</th>
<th>Info</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for contact in contacts %}
<tr>
<td>{{ contact.id }}</td>
<td>{{ contact.name }}</td>
<td>{{ contact.email }}</td>
<td>{{ contact.infotext or "" }}</td>
<td class="actions">
<a class="btn btn-small" href="{{ url_for('admin_contact_edit', ansprechpartner_id=contact.id) }}">Bearbeiten</a>
<form method="post"
action="{{ url_for('admin_contact_delete', ansprechpartner_id=contact.id) }}"
style="display:inline;"
onsubmit="return confirm('Ansprechpartner wirklich löschen?');">
<button type="submit" class="btn btn-small btn-danger">Löschen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>Keine Ansprechpartner vorhanden.</p>
{% endif %}
</div>
{% endblock %}

View File

@ -1,12 +1,32 @@
{% extends 'base.html' %}
{% block title %}Admin{% endblock %}
{% extends "base.html" %}
{% block content %}
<section class="card">
<h1>Admin Bereich</h1>
<div class="button-row">
<a class="btn" href="{{ url_for('admin_topics') }}">Themen</a>
<a class="btn" href="{{ url_for('admin_questions') }}">Fragen</a>
<a class="btn" href="{{ url_for('admin_contacts') }}">Ansprechpartner</a>
<div class="content-card">
<h1>Admin</h1>
<p class="muted">Wähle einen Bereich für die Verwaltung.</p>
<div class="topic-grid" style="margin-top: 24px;">
<a class="topic-box" href="{{ url_for('admin_users') }}">
<h2>Userverwaltung</h2>
<p>Benutzer anzeigen und verwalten.</p>
</a>
<a class="topic-box" href="{{ url_for('admin_themen') }}">
<h2>Themenverwaltung</h2>
<p>Themen pflegen und strukturieren.</p>
</a>
<a class="topic-box" href="{{ url_for('admin_questions') }}">
<h2>Fragenverwaltung</h2>
<p>Fragen erstellen, bearbeiten und Themen zuordnen.</p>
</a>
<a class="topic-box" href="{{ url_for('admin_contacts') }}">
<h2>Ansprechpartner</h2>
<p>Ansprechpartner anzeigen und verwalten.</p>
</a>
</div>
</section>
{% endblock %}
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block content %}
<div class="content-card">
<h1>
{% if mode == "edit" %}
Frage bearbeiten
{% else %}
Neue Frage
{% endif %}
</h1>
<form method="post" class="admin-form">
<div class="form-group">
<label>Thema</label>
<select name="thema_id" required>
<option value="">-- auswählen --</option>
{% for t in themen %}
<option value="{{ t.id }}"
{% if frage.thema_id == t.id %}selected{% endif %}>
{{ t.kurztitel }} - {{ t.titel }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>Frage</label>
<textarea name="text" rows="5" required>{{ frage.text or '' }}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn">Speichern</button>
<a class="btn btn-secondary" href="{{ url_for('admin_questions') }}">Abbrechen</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -1,13 +1,41 @@
{% extends 'base.html' %}
{% block title %}Admin Fragen{% endblock %}
{% extends "base.html" %}
{% block content %}
<section class="card">
<h1>Fragen</h1>
<p>CRUD folgt im nächsten Schritt. Aktuell Listenansicht als Platzhalter.</p>
<ul>
{% for item in fragen %}
<li>{{ item.kurztitel }} {{ item.text }}</li>
{% endfor %}
</ul>
</section>
{% endblock %}
<div class="content-card">
<div class="page-header">
<h1>Fragen</h1>
<a class="btn" href="{{ url_for('admin_question_new') }}">Neue Frage</a>
</div>
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>Thema</th>
<th>Frage</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for f in fragen %}
<tr>
<td>{{ f.id }}</td>
<td>{{ f.kurztitel }}</td>
<td>{{ f.text }}</td>
<td class="actions">
<a class="btn btn-small"
href="{{ url_for('admin_question_edit', frage_id=f.id) }}">Bearbeiten</a>
<form method="post"
action="{{ url_for('admin_question_delete', frage_id=f.id) }}"
style="display:inline;"
onsubmit="return confirm('Frage löschen?');">
<button class="btn btn-small btn-danger">Löschen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block content %}
<div class="content-card">
<h1>
{% if mode == "edit" %}
Thema bearbeiten
{% else %}
Neues Thema erstellen
{% endif %}
</h1>
<form method="post" class="admin-form">
<div class="form-group">
<label for="kurztitel">Kurztitel</label>
<input type="text" id="kurztitel" name="kurztitel" value="{{ thema.kurztitel or '' }}" required>
</div>
<div class="form-group">
<label for="titel">Titel</label>
<input type="text" id="titel" name="titel" value="{{ thema.titel or '' }}" required>
</div>
<div class="form-group">
<label for="infotext">InfoText</label>
<textarea id="infotext" name="infotext" rows="6">{{ thema.infotext or '' }}</textarea>
</div>
<div class="form-group">
<label for="zusatztext">Zusatztext</label>
<textarea id="zusatztext" name="zusatztext" rows="6">{{ thema.zusatztext or '' }}</textarea>
</div>
<div class="form-group">
<label>Ansprechpartner</label>
<div class="checkbox-list">
{% for ap in ansprechpartner %}
<label class="checkbox-item">
<input
type="checkbox"
name="ansprechpartner_ids"
value="{{ ap.id }}"
{% if ap.id in selected_ansprechpartner_ids %}checked{% endif %}
>
<span>{{ ap.name }}{% if ap.email %} ({{ ap.email }}){% endif %}</span>
</label>
{% endfor %}
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn">Speichern</button>
<a class="btn btn-secondary" href="{{ url_for('admin_themen') }}">Abbrechen</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block content %}
<div class="content-card">
<div class="page-header">
<h1>Themen</h1>
<a class="btn" href="{{ url_for('admin_thema_new') }}">Neues Thema erstellen</a>
</div>
{% if themen %}
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>Kurztitel</th>
<th>Titel</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for thema in themen %}
<tr>
<td>{{ thema.id }}</td>
<td>{{ thema.kurztitel }}</td>
<td>{{ thema.titel }}</td>
<td class="actions">
<a class="btn btn-small" href="{{ url_for('admin_thema_edit', thema_id=thema.id) }}">Bearbeiten</a>
<form method="post" action="{{ url_for('admin_thema_delete', thema_id=thema.id) }}" style="display:inline;" onsubmit="return confirm('Thema wirklich löschen?');">
<button type="submit" class="btn btn-small btn-danger">Löschen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>Keine Themen vorhanden.</p>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block content %}
<div class="content-card">
<div class="page-header">
<h1>Userverwaltung</h1>
</div>
{% if users %}
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>E-Mail</th>
<th>Aktiv</th>
<th>Letzter Login</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ "Ja" if user.is_active else "Nein" }}</td>
<td>{{ user.last_login or "-" }}</td>
<td class="actions">
{% if not user.is_active %}
<form method="post"
action="{{ url_for('admin_user_activate', user_id=user.id) }}"
style="display:inline;">
<button type="submit" class="btn btn-small">Aktivieren</button>
</form>
{% endif %}
{% if user.id != session.get('user_id') %}
<form method="post"
action="{{ url_for('admin_user_delete', user_id=user.id) }}"
style="display:inline;"
onsubmit="return confirm('Benutzer wirklich löschen?');">
<button type="submit" class="btn btn-small btn-danger">Löschen</button>
</form>
{% else %}
<span class="muted">Aktueller Benutzer</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>Keine Benutzer vorhanden.</p>
{% endif %}
</div>
{% endblock %}

View File

@ -1,23 +1,28 @@
<header class="site-header">
<div class="container nav-bar">
<a href="{{ url_for('index') }}" class="brand">dasunternehmen</a>
<nav class="main-nav">
{% if current_user %}
<a href="{{ url_for('dashboard') }}">Themen</a>
{% if session.get('user_id') %}
<div class="user-menu">
<span>{{ current_user.name }}</span>
<div class="dropdown">
<a href="{{ url_for('profile') }}">Profil</a>
{% if is_admin %}<a href="{{ url_for('admin_index') }}">Admin</a>{% endif %}
<a href="{{ url_for('logout') }}">Logout</a>
</div>
<div class="user-menu">
<span>{{ current_user.name }}</span>
<div class="dropdown">
<a href="{{ url_for('profile') }}">Profil</a>
{% if is_admin %}
<a href="{{ url_for('admin_index') }}">Admin</a>
<a href="{{ url_for('admin_themen') }}">Themen</a>
<a href="{{ url_for('admin_users') }}">User</a>
<a href="{{ url_for('admin_contacts') }}">Ansprechpartner</a>
{% endif %}
<a href="{{ url_for('logout') }}">Logout</a>
</div>
{% endif %}
</div>
{% else %}
<a href="{{ url_for('login') }}">Login</a>
<a href="{{ url_for('register') }}">Registrieren</a>
{% endif %}
</nav>
</div>
</header>
</header>

View File

@ -29,6 +29,7 @@ def send_mail(to_address, subject, body):
msg['To'] = to_address
msg.set_content(body)
# SMTPS
if Config.SMTP_PORT == 465:
context = ssl.create_default_context()
with smtplib.SMTP_SSL(Config.SMTP_SERVER, Config.SMTP_PORT, context=context) as server:
@ -37,11 +38,14 @@ def send_mail(to_address, subject, body):
server.send_message(msg)
return
# Normales SMTP, z. B. Port 25
with smtplib.SMTP(Config.SMTP_SERVER, Config.SMTP_PORT) as server:
if Config.MAIL_USE_TLS:
server.starttls(context=ssl.create_default_context())
if Config.SMTP_USERNAME:
server.login(Config.SMTP_USERNAME, Config.SMTP_PASSWORD)
server.send_message(msg)