Version 1.0 Basis Verwaltung
This commit is contained in:
parent
64876a81f4
commit
3446ab11a0
603
app.py
603
app.py
@ -1,208 +1,240 @@
|
|||||||
from pathlib import Path
|
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
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from config import Config
|
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 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
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
chart_dir = Path('generated_charts')
|
|
||||||
|
chart_dir = Path("generated_charts")
|
||||||
chart_dir.mkdir(exist_ok=True)
|
chart_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_user():
|
def inject_user():
|
||||||
user = None
|
user = None
|
||||||
is_admin = False
|
user_is_admin = False
|
||||||
if session.get('user_id'):
|
|
||||||
user = fetch_one('SELECT id, name, email FROM benutzer WHERE id = %s', (session['user_id'],))
|
if session.get("user_id"):
|
||||||
admin_row = fetch_one(
|
user = get_user_by_id(session["user_id"])
|
||||||
'''
|
groups = get_user_groups(session["user_id"])
|
||||||
SELECT 1
|
session["groups"] = groups
|
||||||
FROM benutzer_gruppen bg
|
user_is_admin = "Admins" in groups
|
||||||
JOIN gruppen g ON g.id = bg.gruppen_id
|
|
||||||
WHERE bg.benutzer_id = %s AND g.gruppenname = 'Admins'
|
return {
|
||||||
''',
|
"current_user": user,
|
||||||
(session['user_id'],),
|
"is_admin": user_is_admin,
|
||||||
)
|
}
|
||||||
is_admin = bool(admin_row)
|
|
||||||
return {'current_user': user, 'is_admin': is_admin}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
if session.get('user_id'):
|
if session.get("user_id"):
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for("dashboard"))
|
||||||
return render_template('index.html')
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
@app.route("/register", methods=["GET", "POST"])
|
||||||
def register():
|
def register():
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
name = request.form['name'].strip()
|
name = request.form["name"].strip()
|
||||||
email = request.form['email'].strip().lower()
|
email = request.form["email"].strip().lower()
|
||||||
password = request.form['password']
|
password = request.form["password"]
|
||||||
|
|
||||||
existing = fetch_one('SELECT id FROM benutzer WHERE email = %s', (email,))
|
existing = get_user_by_email(email)
|
||||||
if existing:
|
if existing:
|
||||||
flash('E-Mail ist bereits registriert.', 'danger')
|
flash("E-Mail ist bereits registriert.", "danger")
|
||||||
return render_template('register.html')
|
return render_template("register.html")
|
||||||
|
|
||||||
password_hash = generate_password_hash(password)
|
password_hash = generate_password_hash(password)
|
||||||
execute(
|
create_user(name, email, password_hash, is_active=False)
|
||||||
'''
|
|
||||||
INSERT INTO benutzer (name, email, passwort_hash, is_active)
|
|
||||||
VALUES (%s, %s, %s, FALSE)
|
|
||||||
''',
|
|
||||||
(name, email, password_hash),
|
|
||||||
)
|
|
||||||
|
|
||||||
token = generate_activation_token(email)
|
token = generate_activation_token(email)
|
||||||
activation_link = f"{Config.APP_BASE_URL}{url_for('activate_account', token=token)}"
|
activation_link = f"{Config.APP_BASE_URL}{url_for('activate_account', token=token)}"
|
||||||
|
|
||||||
send_mail(
|
send_mail(
|
||||||
email,
|
email,
|
||||||
'Account aktivieren',
|
"Account aktivieren",
|
||||||
f'Hallo {name},\n\nbitte aktiviere deinen Account:\n{activation_link}\n',
|
f"Hallo {name},\n\nbitte aktiviere deinen Account:\n{activation_link}\n",
|
||||||
)
|
)
|
||||||
|
|
||||||
flash('Registrierung gespeichert. Bitte E-Mail zur Aktivierung prüfen.', 'success')
|
flash("Registrierung gespeichert. Bitte E-Mail zur Aktivierung prüfen.", "success")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("login"))
|
||||||
return render_template('register.html')
|
|
||||||
|
return render_template("register.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/activate/<token>')
|
@app.route("/activate/<token>")
|
||||||
def activate_account(token):
|
def activate_account(token):
|
||||||
try:
|
try:
|
||||||
email = verify_activation_token(token)
|
email = verify_activation_token(token)
|
||||||
except Exception:
|
except Exception:
|
||||||
flash('Aktivierungslink ist ungültig oder abgelaufen.', 'danger')
|
flash("Aktivierungslink ist ungültig oder abgelaufen.", "danger")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
execute('UPDATE benutzer SET is_active = TRUE WHERE email = %s', (email,))
|
activate_user_by_email(email)
|
||||||
flash('Account wurde aktiviert. Bitte anmelden.', 'success')
|
flash("Account wurde aktiviert. Bitte anmelden.", "success")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
email = request.form['email'].strip().lower()
|
email = request.form["email"].strip().lower()
|
||||||
password = request.form['password']
|
password = request.form["password"]
|
||||||
user = fetch_one('SELECT * FROM benutzer WHERE email = %s', (email,))
|
|
||||||
|
user = get_user_by_email(email)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
flash('Ungültige Zugangsdaten.', 'danger')
|
flash("Ungültige Zugangsdaten.", "danger")
|
||||||
return render_template('login.html')
|
return render_template("login.html")
|
||||||
if not user['is_active']:
|
|
||||||
flash('Account ist noch nicht aktiviert.', 'warning')
|
|
||||||
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
|
password_ok = False
|
||||||
if stored_password == 'topsecret' and password == 'topsecret':
|
|
||||||
|
if stored_password == "topsecret" and password == "topsecret":
|
||||||
new_hash = generate_password_hash(password)
|
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
|
password_ok = True
|
||||||
else:
|
else:
|
||||||
password_ok = check_password_hash(stored_password, password)
|
password_ok = check_password_hash(stored_password, password)
|
||||||
|
|
||||||
if not password_ok:
|
if not password_ok:
|
||||||
flash('Ungültige Zugangsdaten.', 'danger')
|
flash("Ungültige Zugangsdaten.", "danger")
|
||||||
return render_template('login.html')
|
return render_template("login.html")
|
||||||
|
|
||||||
session['user_id'] = user['id']
|
session["user_id"] = user["id"]
|
||||||
execute('UPDATE benutzer SET last_login = NOW() WHERE id = %s', (user['id'],))
|
session["groups"] = get_user_groups(user["id"])
|
||||||
execute('INSERT INTO accesslog (userid) VALUES (%s)', (user['id'],))
|
|
||||||
return redirect(url_for('dashboard'))
|
update_user_last_login(user["id"])
|
||||||
return render_template('login.html')
|
log_access(user["id"])
|
||||||
|
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
return render_template("login.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
session.clear()
|
session.clear()
|
||||||
flash('Erfolgreich abgemeldet.', 'success')
|
flash("Erfolgreich abgemeldet.", "success")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dashboard')
|
@app.route("/dashboard")
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
themen = fetch_all('SELECT * FROM thema ORDER BY id')
|
themen = get_all_themen()
|
||||||
return render_template('dashboard.html', themen=themen)
|
return render_template("dashboard.html", themen=themen)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/profil', methods=['GET', 'POST'])
|
@app.route("/profil", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def profile():
|
def profile():
|
||||||
user = fetch_one('SELECT id, name, email FROM benutzer WHERE id = %s', (session['user_id'],))
|
user = get_user_by_id(session["user_id"])
|
||||||
if request.method == 'POST':
|
|
||||||
new_password = request.form['password']
|
if request.method == "POST":
|
||||||
execute(
|
new_password = request.form["password"].strip()
|
||||||
'UPDATE benutzer SET passwort_hash = %s WHERE id = %s',
|
if not new_password:
|
||||||
(generate_password_hash(new_password), session['user_id'])
|
flash("Bitte ein Passwort eingeben.", "warning")
|
||||||
)
|
return render_template("profile.html", user=user)
|
||||||
flash('Passwort wurde geändert.', 'success')
|
|
||||||
return redirect(url_for('profile'))
|
update_user_password(session["user_id"], generate_password_hash(new_password))
|
||||||
return render_template('profile.html', user=user)
|
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
|
@login_required
|
||||||
def topic(thema_id):
|
def topic(thema_id):
|
||||||
thema = fetch_one('SELECT * FROM thema WHERE id = %s', (thema_id,))
|
thema = get_thema_by_id(thema_id)
|
||||||
fragen = fetch_all('SELECT * FROM fragen WHERE themaid = %s ORDER BY id', (thema_id,))
|
if not thema:
|
||||||
ansprechpartner = fetch_all(
|
flash("Thema nicht gefunden.", "danger")
|
||||||
'''
|
return redirect(url_for("dashboard"))
|
||||||
SELECT a.*
|
|
||||||
FROM ansprechpartner a
|
fragen = get_thema_questions(thema_id)
|
||||||
JOIN themaansprechpartner ta ON ta.ansprechpartnerid = a.id
|
ansprechpartner = get_thema_ansprechpartner(thema_id)
|
||||||
WHERE ta.themaid = %s
|
|
||||||
ORDER BY a.name
|
if request.method == "POST":
|
||||||
''',
|
assessment_id = request.form.get("assessment_id")
|
||||||
(thema_id,),
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
assessment_id = request.form.get('assessment_id')
|
|
||||||
if not assessment_id:
|
if not assessment_id:
|
||||||
assessment = execute_returning(
|
assessment_id = create_assessment(session["user_id"])
|
||||||
'INSERT INTO assessment (userid) VALUES (%s) RETURNING id',
|
|
||||||
(session['user_id'],),
|
|
||||||
)
|
|
||||||
assessment_id = assessment['id']
|
|
||||||
|
|
||||||
for frage in fragen:
|
for frage in fragen:
|
||||||
value = request.form.get(f'frage_{frage["id"]}')
|
value = request.form.get(f'frage_{frage["id"]}')
|
||||||
if value not in ('ja', 'nein'):
|
if value not in ("ja", "nein"):
|
||||||
flash('Bitte alle Fragen beantworten.', 'warning')
|
flash("Bitte alle Fragen beantworten.", "warning")
|
||||||
return render_template(
|
return render_template(
|
||||||
'topic.html',
|
"topic.html",
|
||||||
thema=thema,
|
thema=thema,
|
||||||
fragen=fragen,
|
fragen=fragen,
|
||||||
ansprechpartner=ansprechpartner,
|
ansprechpartner=ansprechpartner,
|
||||||
assessment_id=assessment_id,
|
assessment_id=assessment_id,
|
||||||
)
|
)
|
||||||
execute(
|
|
||||||
'''
|
save_assessment_answer(
|
||||||
INSERT INTO assessmentanswer (assessmentid, themaid, frageid, antwort)
|
assessment_id=assessment_id,
|
||||||
VALUES (%s, %s, %s, %s)
|
thema_id=thema_id,
|
||||||
ON CONFLICT (assessmentid, frageid)
|
frage_id=frage["id"],
|
||||||
DO UPDATE SET antwort = EXCLUDED.antwort
|
antwort=(value == "ja"),
|
||||||
''',
|
|
||||||
(assessment_id, thema_id, frage['id'], value == 'ja'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
next_topic = fetch_one('SELECT id FROM thema WHERE id > %s ORDER BY id LIMIT 1', (thema_id,))
|
next_thema_id = get_next_thema_id(thema_id)
|
||||||
if next_topic:
|
if next_thema_id:
|
||||||
return redirect(url_for('topic', thema_id=next_topic['id'], assessment_id=assessment_id))
|
return redirect(url_for("topic", thema_id=next_thema_id, assessment_id=assessment_id))
|
||||||
return redirect(url_for('assessment_result', 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(
|
return render_template(
|
||||||
'topic.html',
|
"topic.html",
|
||||||
thema=thema,
|
thema=thema,
|
||||||
fragen=fragen,
|
fragen=fragen,
|
||||||
ansprechpartner=ansprechpartner,
|
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
|
@login_required
|
||||||
def assessment_result(assessment_id):
|
def assessment_result(assessment_id):
|
||||||
rows = fetch_all(
|
rows = get_assessment_result_rows(assessment_id)
|
||||||
'''
|
|
||||||
SELECT t.kurztitel, COUNT(*) FILTER (WHERE aa.antwort = TRUE) AS ja_anzahl
|
labels = [row["kurztitel"] for row in rows]
|
||||||
FROM thema t
|
values = [int(row["ja_anzahl"]) for row in rows]
|
||||||
LEFT JOIN assessmentanswer aa ON aa.themaid = t.id AND aa.assessmentid = %s
|
|
||||||
GROUP BY t.id, t.kurztitel
|
filename = f"assessment_{assessment_id}.png"
|
||||||
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'
|
|
||||||
output_path = chart_dir / filename
|
output_path = chart_dir / filename
|
||||||
create_assessment_chart(labels, values, output_path)
|
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
|
@login_required
|
||||||
def generated_chart(filename):
|
def generated_chart(filename):
|
||||||
return send_from_directory(chart_dir, filename)
|
return send_from_directory(chart_dir, filename)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin')
|
@app.route("/admin")
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_index():
|
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
|
@admin_required
|
||||||
def admin_topics():
|
def admin_users():
|
||||||
themen = fetch_all('SELECT * FROM thema ORDER BY id')
|
users = get_all_users()
|
||||||
return render_template('admin/topics.html', themen=themen)
|
return render_template("admin/users.html", users=users)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/fragen')
|
@app.route("/admin/themen")
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_questions():
|
def admin_themen():
|
||||||
fragen = fetch_all(
|
themen = get_all_themen()
|
||||||
'''SELECT f.id, f.text, t.kurztitel FROM fragen f JOIN thema t ON t.id = f.themaid ORDER BY t.id, f.id'''
|
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
|
@admin_required
|
||||||
def admin_contacts():
|
def admin_contacts():
|
||||||
contacts = fetch_all('SELECT * FROM ansprechpartner ORDER BY name')
|
contacts = get_all_contacts()
|
||||||
return render_template('admin/contacts.html', contacts=contacts)
|
return render_template("admin/contacts.html", contacts=contacts)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
@app.route("/admin/ansprechpartner/new", methods=["GET", "POST"])
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
@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)
|
||||||
10
config.py
10
config.py
@ -9,11 +9,11 @@ class Config:
|
|||||||
DB_PASSWORD = os.getenv('DB_PASSWORD', 'UnternehmenPWD')
|
DB_PASSWORD = os.getenv('DB_PASSWORD', 'UnternehmenPWD')
|
||||||
DB_PORT = int(os.getenv('DB_PORT', '5432'))
|
DB_PORT = int(os.getenv('DB_PORT', '5432'))
|
||||||
|
|
||||||
SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.example.com')
|
SMTP_SERVER = os.getenv('SMTP_SERVER', 'mail.kolb.cc')
|
||||||
SMTP_PORT = int(os.getenv('SMTP_PORT', '587'))
|
SMTP_PORT = int(os.getenv('SMTP_PORT', '25'))
|
||||||
SMTP_USERNAME = os.getenv('SMTP_USERNAME', '')
|
SMTP_USERNAME = os.getenv('SMTP_USERNAME', '')
|
||||||
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', '')
|
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', '')
|
||||||
MAIL_SENDER = os.getenv('MAIL_SENDER', 'noreply@example.com')
|
MAIL_SENDER = os.getenv('MAIL_SENDER', 'admin@kolb.cc')
|
||||||
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'true').lower() == 'true'
|
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
441
db.py
@ -52,3 +52,444 @@ def execute_returning(query, params=None):
|
|||||||
with get_cursor(commit=True) as cur:
|
with get_cursor(commit=True) as cur:
|
||||||
cur.execute(query, params or ())
|
cur.execute(query, params or ())
|
||||||
return cur.fetchone()
|
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,),
|
||||||
|
)
|
||||||
@ -1,38 +1,23 @@
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import session, redirect, url_for, flash
|
from flask import flash, redirect, session, url_for
|
||||||
from db import fetch_one
|
|
||||||
|
|
||||||
|
|
||||||
def login_required(view_func):
|
def login_required(func):
|
||||||
@wraps(view_func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if not session.get('user_id'):
|
if not session.get("user_id"):
|
||||||
flash('Bitte zuerst anmelden.', 'warning')
|
flash("Bitte zuerst anmelden.", "warning")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("login"))
|
||||||
return view_func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(func):
|
||||||
def admin_required(view_func):
|
@wraps(func)
|
||||||
@wraps(view_func)
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
user_id = session.get('user_id')
|
groups = session.get("groups", [])
|
||||||
if not user_id:
|
if "Admins" not in groups:
|
||||||
flash('Bitte zuerst anmelden.', 'warning')
|
flash("Keine Berechtigung.", "danger")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("dashboard"))
|
||||||
|
return func(*args, **kwargs)
|
||||||
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
|
return wrapper
|
||||||
@ -9,52 +9,107 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: linear-gradient(180deg, #fbf7f1, var(--bg));
|
background: linear-gradient(180deg, #fbf7f1, var(--bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.container, .page-wrap {
|
.container, .page-wrap {
|
||||||
width: min(1100px, calc(100% - 32px));
|
width: min(1100px, calc(100% - 32px));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
.page-wrap { padding: 24px 0 48px; }
|
|
||||||
|
.page-wrap {
|
||||||
|
padding: 24px 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
.site-header, .site-footer {
|
.site-header, .site-footer {
|
||||||
background: rgba(255,255,255,0.6);
|
background: rgba(255,255,255,0.6);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
border-bottom: 1px solid var(--border);
|
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 {
|
.nav-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 18px 0;
|
padding: 18px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-dark);
|
color: var(--accent-dark);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.main-nav { display: flex; gap: 16px; align-items: center; }
|
|
||||||
.main-nav a, .user-menu span { color: var(--text); text-decoration: none; }
|
.main-nav {
|
||||||
.user-menu { position: relative; padding: 10px 14px; background: var(--panel); border-radius: 999px; border: 1px solid var(--border); }
|
display: flex;
|
||||||
.user-menu:hover .dropdown { display: block; }
|
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 {
|
.dropdown {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 8px);
|
top: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
min-width: 180px;
|
margin-top: 0;
|
||||||
|
min-width: 220px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 18px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
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 {
|
.card {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@ -63,9 +118,19 @@ body {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
box-shadow: 0 15px 40px rgba(80, 58, 34, 0.08);
|
box-shadow: 0 15px 40px rgba(80, 58, 34, 0.08);
|
||||||
}
|
}
|
||||||
.hero-card { padding: 40px 32px; }
|
|
||||||
.form-card { max-width: 560px; }
|
.hero-card {
|
||||||
input[type="text"], input[type="email"], input[type="password"] {
|
padding: 40px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="password"],
|
||||||
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 8px 0 18px;
|
margin: 8px 0 18px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
@ -73,35 +138,92 @@ input[type="text"], input[type="email"], input[type="password"] {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 12px 18px;
|
padding: 10px 18px;
|
||||||
border-radius: 16px;
|
border: 1px solid #cdb693;
|
||||||
background: var(--accent);
|
border-radius: 999px;
|
||||||
color: #fff;
|
background: #efe3d1;
|
||||||
|
color: #3f342c;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
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 {
|
.topic-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
.topic-box, .question-box, .contact-box {
|
|
||||||
|
.topic-box,
|
||||||
|
.question-box,
|
||||||
|
.contact-box {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
.question-box { margin-bottom: 16px; }
|
|
||||||
.radio-row { display: flex; gap: 18px; }
|
.topic-box {
|
||||||
.result-chart { width: 100%; max-width: 1000px; border-radius: 18px; border: 1px solid var(--border); }
|
text-decoration: none;
|
||||||
.result-table { width: 100%; border-collapse: collapse; margin-top: 18px; }
|
color: var(--text);
|
||||||
.result-table th, .result-table td { padding: 10px; border-bottom: 1px solid var(--border); text-align: left; }
|
}
|
||||||
.flash-wrapper { margin-bottom: 16px; }
|
|
||||||
|
.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 {
|
.flash {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@ -109,23 +231,87 @@ input[type="text"], input[type="email"], input[type="password"] {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flash.success { border-color: #9fc89f; }
|
.flash.success { border-color: #9fc89f; }
|
||||||
.flash.warning { border-color: #e0b86d; }
|
.flash.warning { border-color: #e0b86d; }
|
||||||
.flash.danger { border-color: #d89f9f; }
|
.flash.danger,
|
||||||
.muted { color: #786a5d; }
|
.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) {
|
@media (max-width: 700px) {
|
||||||
.nav-bar { flex-direction: column; gap: 12px; }
|
.nav-bar {
|
||||||
}
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.user-menu {
|
.page-header {
|
||||||
position: relative;
|
flex-direction: column;
|
||||||
display: inline-block;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-menu-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%; /* direkt unter dem Button */
|
|
||||||
left: 0;
|
|
||||||
margin-top: 0; /* GANZ WICHTIG */
|
|
||||||
padding-top: 0; /* falls vorhanden */
|
|
||||||
}
|
}
|
||||||
35
templates/admin/contact_form.html
Normal file
35
templates/admin/contact_form.html
Normal 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 %}
|
||||||
@ -1,13 +1,46 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Admin Ansprechpartner{% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="card">
|
<div class="content-card">
|
||||||
<h1>Ansprechpartner</h1>
|
<div class="page-header">
|
||||||
<p>CRUD folgt im nächsten Schritt. Aktuell Listenansicht als Platzhalter.</p>
|
<h1>Ansprechpartner</h1>
|
||||||
<ul>
|
<a class="btn" href="{{ url_for('admin_contact_new') }}">Neuen Ansprechpartner erstellen</a>
|
||||||
{% for item in contacts %}
|
</div>
|
||||||
<li>{{ item.name }} – {{ item.email }}</li>
|
|
||||||
{% endfor %}
|
{% if contacts %}
|
||||||
</ul>
|
<table class="admin-table">
|
||||||
</section>
|
<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 %}
|
{% endblock %}
|
||||||
@ -1,12 +1,32 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Admin{% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="card">
|
<div class="content-card">
|
||||||
<h1>Admin Bereich</h1>
|
<h1>Admin</h1>
|
||||||
<div class="button-row">
|
<p class="muted">Wähle einen Bereich für die Verwaltung.</p>
|
||||||
<a class="btn" href="{{ url_for('admin_topics') }}">Themen</a>
|
|
||||||
<a class="btn" href="{{ url_for('admin_questions') }}">Fragen</a>
|
<div class="topic-grid" style="margin-top: 24px;">
|
||||||
<a class="btn" href="{{ url_for('admin_contacts') }}">Ansprechpartner</a>
|
|
||||||
|
<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>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
40
templates/admin/question_form.html
Normal file
40
templates/admin/question_form.html
Normal 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 %}
|
||||||
@ -1,13 +1,41 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Admin Fragen{% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="card">
|
<div class="content-card">
|
||||||
<h1>Fragen</h1>
|
<div class="page-header">
|
||||||
<p>CRUD folgt im nächsten Schritt. Aktuell Listenansicht als Platzhalter.</p>
|
<h1>Fragen</h1>
|
||||||
<ul>
|
<a class="btn" href="{{ url_for('admin_question_new') }}">Neue Frage</a>
|
||||||
{% for item in fragen %}
|
</div>
|
||||||
<li>{{ item.kurztitel }} – {{ item.text }}</li>
|
|
||||||
{% endfor %}
|
<table class="admin-table">
|
||||||
</ul>
|
<thead>
|
||||||
</section>
|
<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 %}
|
{% endblock %}
|
||||||
57
templates/admin/thema_form.html
Normal file
57
templates/admin/thema_form.html
Normal 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 %}
|
||||||
41
templates/admin/themen_list.html
Normal file
41
templates/admin/themen_list.html
Normal 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 %}
|
||||||
57
templates/admin/users.html
Normal file
57
templates/admin/users.html
Normal 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 %}
|
||||||
@ -1,19 +1,24 @@
|
|||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="container nav-bar">
|
<div class="container nav-bar">
|
||||||
<a href="{{ url_for('index') }}" class="brand">dasunternehmen</a>
|
<a href="{{ url_for('index') }}" class="brand">dasunternehmen</a>
|
||||||
|
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
{% if current_user %}
|
{% if current_user %}
|
||||||
<a href="{{ url_for('dashboard') }}">Themen</a>
|
<a href="{{ url_for('dashboard') }}">Themen</a>
|
||||||
{% if session.get('user_id') %}
|
|
||||||
<div class="user-menu">
|
<div class="user-menu">
|
||||||
<span>{{ current_user.name }}</span>
|
<span>{{ current_user.name }}</span>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a href="{{ url_for('profile') }}">Profil</a>
|
<a href="{{ url_for('profile') }}">Profil</a>
|
||||||
{% if is_admin %}<a href="{{ url_for('admin_index') }}">Admin</a>{% endif %}
|
{% if is_admin %}
|
||||||
<a href="{{ url_for('logout') }}">Logout</a>
|
<a href="{{ url_for('admin_index') }}">Admin</a>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('login') }}">Login</a>
|
<a href="{{ url_for('login') }}">Login</a>
|
||||||
<a href="{{ url_for('register') }}">Registrieren</a>
|
<a href="{{ url_for('register') }}">Registrieren</a>
|
||||||
|
|||||||
4
tools.py
4
tools.py
@ -29,6 +29,7 @@ def send_mail(to_address, subject, body):
|
|||||||
msg['To'] = to_address
|
msg['To'] = to_address
|
||||||
msg.set_content(body)
|
msg.set_content(body)
|
||||||
|
|
||||||
|
# SMTPS
|
||||||
if Config.SMTP_PORT == 465:
|
if Config.SMTP_PORT == 465:
|
||||||
context = ssl.create_default_context()
|
context = ssl.create_default_context()
|
||||||
with smtplib.SMTP_SSL(Config.SMTP_SERVER, Config.SMTP_PORT, context=context) as server:
|
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)
|
server.send_message(msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Normales SMTP, z. B. Port 25
|
||||||
with smtplib.SMTP(Config.SMTP_SERVER, Config.SMTP_PORT) as server:
|
with smtplib.SMTP(Config.SMTP_SERVER, Config.SMTP_PORT) as server:
|
||||||
if Config.MAIL_USE_TLS:
|
if Config.MAIL_USE_TLS:
|
||||||
server.starttls(context=ssl.create_default_context())
|
server.starttls(context=ssl.create_default_context())
|
||||||
|
|
||||||
if Config.SMTP_USERNAME:
|
if Config.SMTP_USERNAME:
|
||||||
server.login(Config.SMTP_USERNAME, Config.SMTP_PASSWORD)
|
server.login(Config.SMTP_USERNAME, Config.SMTP_PASSWORD)
|
||||||
|
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user