Wartung Fragen
This commit is contained in:
parent
b4ea78ec3b
commit
8a9823e9de
88
app.py
88
app.py
@ -50,6 +50,8 @@ from db import (
|
||||
get_themen_for_branche,
|
||||
get_next_thema_id_for_branche,
|
||||
get_thema_for_branche,
|
||||
get_question_count_for_thema,
|
||||
get_all_themen_with_question_count,
|
||||
)
|
||||
from permissions import admin_required, login_required
|
||||
from tools import create_assessment_chart, generate_activation_token, send_mail, verify_activation_token
|
||||
@ -355,7 +357,7 @@ def admin_users():
|
||||
@app.route("/admin/themen")
|
||||
@admin_required
|
||||
def admin_themen():
|
||||
themen = get_all_themen()
|
||||
themen = get_all_themen_with_question_count()
|
||||
return render_template("admin/themen_list.html", themen=themen)
|
||||
|
||||
|
||||
@ -562,7 +564,8 @@ def admin_contact_delete(ansprechpartner_id):
|
||||
def admin_questions():
|
||||
fragen = get_all_questions_with_thema()
|
||||
themen = get_all_themen()
|
||||
return render_template("admin/questions.html", fragen=fragen, themen=themen)
|
||||
branchen = get_all_branchen()
|
||||
return render_template("admin/questions.html", fragen=fragen, themen=themen, branchen=branchen)
|
||||
|
||||
@app.route("/admin/fragen/new", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
@ -755,5 +758,86 @@ def admin_branche_delete(branche_id):
|
||||
flash("Branche wurde gelöscht.", "success")
|
||||
return redirect(url_for("admin_branchen"))
|
||||
|
||||
@app.route("/admin/themen/<int:thema_id>/fragen/new", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def admin_question_new_for_thema(thema_id):
|
||||
thema = get_thema_by_id(thema_id)
|
||||
if not thema:
|
||||
flash("Thema nicht gefunden.", "error")
|
||||
return redirect(url_for("admin_themen"))
|
||||
|
||||
question_count = get_question_count_for_thema(thema_id)
|
||||
|
||||
if request.method == "POST":
|
||||
text = request.form.get("text", "").strip()
|
||||
|
||||
if not text:
|
||||
flash("Der Fragetext ist ein Pflichtfeld.", "error")
|
||||
return render_template(
|
||||
"admin/question_form_thema.html",
|
||||
mode="new",
|
||||
thema=thema,
|
||||
frage=request.form,
|
||||
question_count=question_count,
|
||||
min_questions=8,
|
||||
)
|
||||
|
||||
create_question(thema_id, text)
|
||||
flash("Frage wurde erstellt.", "success")
|
||||
return redirect(url_for("admin_question_new_for_thema", thema_id=thema_id))
|
||||
|
||||
return render_template(
|
||||
"admin/question_form_thema.html",
|
||||
mode="new",
|
||||
thema=thema,
|
||||
frage={},
|
||||
question_count=question_count,
|
||||
min_questions=8,
|
||||
)
|
||||
|
||||
@app.route("/admin/themen/<int:thema_id>/fragen/<int:frage_id>/edit", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def admin_question_edit_for_thema(thema_id, frage_id):
|
||||
thema = get_thema_by_id(thema_id)
|
||||
frage = get_question_by_id(frage_id)
|
||||
|
||||
if not thema or not frage or int(frage["thema_id"]) != int(thema_id):
|
||||
flash("Frage oder Thema nicht gefunden.", "error")
|
||||
return redirect(url_for("admin_themen"))
|
||||
|
||||
question_count = get_question_count_for_thema(thema_id)
|
||||
|
||||
if request.method == "POST":
|
||||
text = request.form.get("text", "").strip()
|
||||
|
||||
if not text:
|
||||
frage_form = {
|
||||
"id": frage_id,
|
||||
"thema_id": thema_id,
|
||||
"text": text,
|
||||
}
|
||||
flash("Der Fragetext ist ein Pflichtfeld.", "error")
|
||||
return render_template(
|
||||
"admin/question_form_thema.html",
|
||||
mode="edit",
|
||||
thema=thema,
|
||||
frage=frage_form,
|
||||
question_count=question_count,
|
||||
min_questions=8,
|
||||
)
|
||||
|
||||
update_question(frage_id, thema_id, text)
|
||||
flash("Frage wurde gespeichert.", "success")
|
||||
return redirect(url_for("admin_question_new_for_thema", thema_id=thema_id))
|
||||
|
||||
return render_template(
|
||||
"admin/question_form_thema.html",
|
||||
mode="edit",
|
||||
thema=thema,
|
||||
frage=frage,
|
||||
question_count=question_count,
|
||||
min_questions=8,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
52
db.py
52
db.py
@ -352,9 +352,28 @@ def get_thema_questions(thema_id):
|
||||
def get_all_questions_with_thema():
|
||||
return fetch_all(
|
||||
"""
|
||||
SELECT f.id, f.text, f.thema_id, t.kurztitel, t.titel
|
||||
SELECT
|
||||
f.id,
|
||||
f.text,
|
||||
f.thema_id,
|
||||
t.id AS thema_sort_id,
|
||||
t.kurztitel,
|
||||
t.titel,
|
||||
COALESCE(STRING_AGG(b.branchenname, ' | '), '') AS branchen
|
||||
FROM fragen f
|
||||
JOIN thema t ON t.id = f.thema_id
|
||||
JOIN thema t
|
||||
ON t.id = f.thema_id
|
||||
LEFT JOIN branchenthemen bt
|
||||
ON bt.thema_id = t.id
|
||||
LEFT JOIN branche b
|
||||
ON b.id = bt.branche_id
|
||||
GROUP BY
|
||||
f.id,
|
||||
f.text,
|
||||
f.thema_id,
|
||||
t.id,
|
||||
t.kurztitel,
|
||||
t.titel
|
||||
ORDER BY t.id, f.id
|
||||
"""
|
||||
)
|
||||
@ -712,3 +731,32 @@ def get_thema_for_branche(thema_id, branche_id):
|
||||
""",
|
||||
(thema_id, branche_id),
|
||||
)
|
||||
|
||||
def get_question_count_for_thema(thema_id):
|
||||
row = fetch_one(
|
||||
"""
|
||||
SELECT COUNT(*) AS anzahl
|
||||
FROM fragen
|
||||
WHERE thema_id = %s
|
||||
""",
|
||||
(thema_id,),
|
||||
)
|
||||
return int(row["anzahl"]) if row else 0
|
||||
|
||||
|
||||
def get_all_themen_with_question_count():
|
||||
return fetch_all(
|
||||
"""
|
||||
SELECT
|
||||
t.id,
|
||||
t.kurztitel,
|
||||
t.titel,
|
||||
t.infotext,
|
||||
t.zusatztext,
|
||||
COUNT(f.id) AS fragen_anzahl
|
||||
FROM thema t
|
||||
LEFT JOIN fragen f ON f.thema_id = t.id
|
||||
GROUP BY t.id, t.kurztitel, t.titel, t.infotext, t.zusatztext
|
||||
ORDER BY t.id
|
||||
"""
|
||||
)
|
||||
@ -331,3 +331,122 @@ select {
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.progress-bar-wrap {
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
background: #f1e8dc;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.question-group {
|
||||
background: #fcf8f2;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
padding: 20px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.question-group:nth-child(odd) {
|
||||
background: #fffaf4;
|
||||
}
|
||||
|
||||
.question-group:nth-child(even) {
|
||||
background: #f8f1e7;
|
||||
}
|
||||
|
||||
.question-group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.question-group-header h2 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.question-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.question-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
background: #fff;
|
||||
border: 1px solid #eadfce;
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.question-item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.question-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.question-id {
|
||||
font-weight: 700;
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
|
||||
.question-branch {
|
||||
color: #6f5d4f;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.question-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.question-group-header,
|
||||
.question-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.question-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
57
templates/admin/question_form_thema.html
Normal file
57
templates/admin/question_form_thema.html
Normal file
@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-card">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>
|
||||
{% if mode == "edit" %}
|
||||
Frage bearbeiten
|
||||
{% else %}
|
||||
Neue Frage für Thema
|
||||
{% endif %}
|
||||
</h1>
|
||||
<p class="muted">{{ thema.kurztitel }} - {{ thema.titel }}</p>
|
||||
</div>
|
||||
<a class="btn btn-secondary" href="{{ url_for('admin_themen') }}">Zur Themenübersicht</a>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 24px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
||||
<strong>Fortschritt Fragen</strong>
|
||||
<span>{{ question_count }}/{{ min_questions }}</span>
|
||||
</div>
|
||||
|
||||
{% set progress = (question_count * 100 / min_questions) %}
|
||||
{% if progress > 100 %}
|
||||
{% set progress = 100 %}
|
||||
{% endif %}
|
||||
|
||||
<div class="progress-bar-wrap">
|
||||
<div class="progress-bar-fill" style="width: {{ progress }}%;"></div>
|
||||
</div>
|
||||
|
||||
{% if question_count < min_questions %}
|
||||
<p class="muted" style="margin-top: 10px;">
|
||||
Für dieses Thema sollten mindestens {{ min_questions }} Fragen gepflegt werden.
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="muted" style="margin-top: 10px;">
|
||||
Mindestanzahl erreicht.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post" class="admin-form">
|
||||
<div class="form-group">
|
||||
<label for="text">Frage</label>
|
||||
<textarea id="text" 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_question_new_for_thema', thema_id=thema.id) }}">Neu laden</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -7,35 +7,138 @@
|
||||
<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">
|
||||
<div class="filter-bar">
|
||||
<div class="form-group filter-item">
|
||||
<label for="filter-thema">Thema</label>
|
||||
<select id="filter-thema">
|
||||
<option value="">Alle Themen</option>
|
||||
{% for t in themen %}
|
||||
<option value="{{ t.id }}">{{ t.kurztitel }} - {{ t.titel }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group filter-item">
|
||||
<label for="filter-branche">Branche</label>
|
||||
<select id="filter-branche">
|
||||
<option value="">Alle Branchen</option>
|
||||
{% for b in branchen %}
|
||||
<option value="{{ b.branchenname }}">{{ b.branchenname }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group filter-item">
|
||||
<label for="filter-text">Suche</label>
|
||||
<input type="text" id="filter-text" placeholder="Frage durchsuchen">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="question-groups">
|
||||
{% for thema in themen %}
|
||||
{% set group_questions = fragen | selectattr("thema_id", "equalto", thema.id) | list %}
|
||||
{% if group_questions %}
|
||||
<div class="question-group"
|
||||
data-thema-id="{{ thema.id }}"
|
||||
data-thema-name="{{ thema.kurztitel }} {{ thema.titel }}">
|
||||
|
||||
<div class="question-group-header">
|
||||
<div>
|
||||
<h2>{{ thema.kurztitel }} - {{ thema.titel }}</h2>
|
||||
<p class="muted">Fragen: {{ group_questions|length }}</p>
|
||||
</div>
|
||||
<a class="btn btn-small btn-secondary"
|
||||
href="{{ url_for('admin_question_new_for_thema', thema_id=thema.id) }}">
|
||||
Frage zu diesem Thema
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="question-list">
|
||||
{% for f in group_questions %}
|
||||
<div class="question-item"
|
||||
data-thema-id="{{ f.thema_id }}"
|
||||
data-branchen="{{ f.branchen|lower }}"
|
||||
data-text="{{ f.text|lower }}">
|
||||
<div class="question-item-main">
|
||||
<div class="question-meta">
|
||||
<span class="question-id">#{{ f.id }}</span>
|
||||
{% if f.branchen %}
|
||||
<span class="question-branch">{{ f.branchen }}</span>
|
||||
{% else %}
|
||||
<span class="question-branch muted">Keine Branche zugeordnet</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="question-text">
|
||||
{{ f.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="question-actions">
|
||||
<a class="btn btn-small"
|
||||
href="{{ url_for('admin_question_edit', frage_id=f.id) }}">Bearbeiten</a>
|
||||
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>
|
||||
<button type="submit" class="btn btn-small btn-danger">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const themaFilter = document.getElementById("filter-thema");
|
||||
const brancheFilter = document.getElementById("filter-branche");
|
||||
const textFilter = document.getElementById("filter-text");
|
||||
|
||||
const groups = document.querySelectorAll(".question-group");
|
||||
|
||||
function applyFilters() {
|
||||
const selectedThema = themaFilter.value.trim();
|
||||
const selectedBranche = brancheFilter.value.trim().toLowerCase();
|
||||
const searchText = textFilter.value.trim().toLowerCase();
|
||||
|
||||
groups.forEach(group => {
|
||||
const questionItems = group.querySelectorAll(".question-item");
|
||||
let visibleCount = 0;
|
||||
|
||||
questionItems.forEach(item => {
|
||||
const itemThemaId = item.dataset.themaId;
|
||||
const itemBranchen = item.dataset.branchen || "";
|
||||
const itemText = item.dataset.text || "";
|
||||
|
||||
const themaMatch = !selectedThema || itemThemaId === selectedThema;
|
||||
const brancheMatch = !selectedBranche || itemBranchen.includes(selectedBranche);
|
||||
const textMatch = !searchText || itemText.includes(searchText);
|
||||
|
||||
const visible = themaMatch && brancheMatch && textMatch;
|
||||
item.style.display = visible ? "" : "none";
|
||||
|
||||
if (visible) {
|
||||
visibleCount++;
|
||||
}
|
||||
});
|
||||
|
||||
group.style.display = visibleCount > 0 ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
themaFilter.addEventListener("change", applyFilters);
|
||||
brancheFilter.addEventListener("change", applyFilters);
|
||||
textFilter.addEventListener("input", applyFilters);
|
||||
|
||||
applyFilters();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -14,6 +14,7 @@
|
||||
<th>ID</th>
|
||||
<th>Kurztitel</th>
|
||||
<th>Titel</th>
|
||||
<th>Fragen</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -23,10 +24,20 @@
|
||||
<td>{{ thema.id }}</td>
|
||||
<td>{{ thema.kurztitel }}</td>
|
||||
<td>{{ thema.titel }}</td>
|
||||
<td>
|
||||
{{ thema.fragen_anzahl }}
|
||||
{% if thema.fragen_anzahl < 8 %}
|
||||
<span class="muted">/ 8</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a class="btn btn-small" href="{{ url_for('admin_thema_edit', thema_id=thema.id) }}">Bearbeiten</a>
|
||||
<a class="btn btn-small btn-secondary" href="{{ url_for('admin_question_new_for_thema', thema_id=thema.id) }}">Fragen pflegen</a>
|
||||
|
||||
<form method="post" action="{{ url_for('admin_thema_delete', thema_id=thema.id) }}" style="display:inline;" onsubmit="return confirm('Thema wirklich löschen?');">
|
||||
<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>
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
<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_themen') }}">Themen</a>
|
||||
<a href="{{ url_for('admin_users') }}">User</a>
|
||||
<a href="{{ url_for('admin_contacts') }}">Ansprechpartner</a>
|
||||
<a href="{{ url_for('admin_contacts') }}">Ansprechpartner</a> -->
|
||||
{% endif %}
|
||||
<a href="{{ url_for('logout') }}">Logout</a>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user