feat(v3): Bürger:innen-Modus-Sandbox unter /v3/antrag/{drs}
Skelett für Issues #184 + #185 — minimal, nicht-disruptiv: - v3/base.html extendet v2/base.html (Topbar/Sidebar/Footer geteilt) - v3/screens/antrag_detail.html extendet vorerst v2-Screen 1:1 und injiziert nur Beta-Pill + Toggle "→ Profi-Modus" - v2/screens/antrag_detail.html bekommt Topbar-Link "→ Bürger:innen- Modus (v3 Beta)" → /v3/antrag/<drs> - _render_antrag_detail() teilt DB-Reads/Context zwischen v2 + v3 — Datenbasis garantiert in Sync, Unterschied ist nur template_name - _MATRIX_EXPLANATIONS auf Modul-Ebene ausgelagert (war bisher inline im v2-Route, jetzt von beiden Modi referenziert) - v3.css als Add-On nach v2.css (lädt im v3/base head) Was v3 noch NICHT tut: Score-Hero-Vereinfachung, Matrix→5-Werte, Glossar-Tooltips, Default-Collapsing der Profi-Blöcke (Verbesserungen, Kommentare). Diese Iterationen folgen pro PR — v2 bleibt unberührt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c4f8ce398a
commit
c4750d3274
62
app/main.py
62
app/main.py
@ -285,13 +285,23 @@ async def index(request: Request, current_user: Optional[dict] = Depends(get_cur
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/antrag/{drucksache:path}", response_class=HTMLResponse)
|
async def _render_antrag_detail(
|
||||||
async def antrag_detail(request: Request, drucksache: str, current_user: Optional[dict] = Depends(get_current_user)):
|
request: Request,
|
||||||
"""v2-Antragsdetail-Route — Server-Side-Rendering mit echten DB-Daten."""
|
drucksache: str,
|
||||||
|
current_user: Optional[dict],
|
||||||
|
template_name: str,
|
||||||
|
) -> HTMLResponse:
|
||||||
|
"""Gemeinsame Render-Logik fuer v2- und v3-Antrag-Detail-Routes.
|
||||||
|
|
||||||
|
Der einzige Unterschied zwischen den Modi ist `template_name` — die
|
||||||
|
DB-Reads, _row_to_detail-Aufbereitung, Plenum-Votes-Anreicherung und
|
||||||
|
der Template-Context sind identisch. So bleibt die Datenbasis fuer
|
||||||
|
beide Modi automatisch in Sync.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
drucksache = validate_drucksache(drucksache)
|
drucksache = validate_drucksache(drucksache)
|
||||||
except Exception:
|
except Exception:
|
||||||
return templates.TemplateResponse("v2/screens/antrag_detail.html", {
|
return templates.TemplateResponse(template_name, {
|
||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "durchsuchen",
|
"v2_active_nav": "durchsuchen",
|
||||||
"error": f"Ungültige Drucksachen-ID: {drucksache}",
|
"error": f"Ungültige Drucksachen-ID: {drucksache}",
|
||||||
@ -300,7 +310,7 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona
|
|||||||
|
|
||||||
row = await get_assessment(drucksache)
|
row = await get_assessment(drucksache)
|
||||||
if not row:
|
if not row:
|
||||||
return templates.TemplateResponse("v2/screens/antrag_detail.html", {
|
return templates.TemplateResponse(template_name, {
|
||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "durchsuchen",
|
"v2_active_nav": "durchsuchen",
|
||||||
"error": f"Antrag {drucksache} wurde nicht gefunden.",
|
"error": f"Antrag {drucksache} wurde nicht gefunden.",
|
||||||
@ -308,13 +318,11 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona
|
|||||||
}, status_code=404)
|
}, status_code=404)
|
||||||
|
|
||||||
antrag = _row_to_detail(row)
|
antrag = _row_to_detail(row)
|
||||||
# #106 Phase 1: namentliche Abstimmungsdaten ergänzen (optional, kann None sein)
|
|
||||||
try:
|
try:
|
||||||
antrag["abstimmungsverhalten"] = await get_abstimmungsverhalten(drucksache)
|
antrag["abstimmungsverhalten"] = await get_abstimmungsverhalten(drucksache)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Fehler beim Laden von Abstimmungsverhalten für %s", drucksache)
|
logger.exception("Fehler beim Laden von Abstimmungsverhalten für %s", drucksache)
|
||||||
antrag["abstimmungsverhalten"] = None
|
antrag["abstimmungsverhalten"] = None
|
||||||
# #106 Phase 2: fraktions-aggregierte Plenum-Abstimmungen aus Plenarprotokollen
|
|
||||||
try:
|
try:
|
||||||
from .database import get_plenum_votes as _gpv
|
from .database import get_plenum_votes as _gpv
|
||||||
antrag["plenum_votes"] = await _gpv(antrag.get("bundesland") or "NRW", drucksache)
|
antrag["plenum_votes"] = await _gpv(antrag.get("bundesland") or "NRW", drucksache)
|
||||||
@ -322,12 +330,22 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona
|
|||||||
logger.exception("Fehler beim Laden plenum_vote_results für %s", drucksache)
|
logger.exception("Fehler beim Laden plenum_vote_results für %s", drucksache)
|
||||||
antrag["plenum_votes"] = []
|
antrag["plenum_votes"] = []
|
||||||
from .models import MATRIX_LABELS
|
from .models import MATRIX_LABELS
|
||||||
return templates.TemplateResponse("v2/screens/antrag_detail.html", {
|
return templates.TemplateResponse(template_name, {
|
||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "durchsuchen",
|
"v2_active_nav": "durchsuchen",
|
||||||
"antrag": antrag,
|
"antrag": antrag,
|
||||||
"assessment_count": None,
|
"assessment_count": None,
|
||||||
"matrix_explanations": {
|
"matrix_explanations": _MATRIX_EXPLANATIONS,
|
||||||
|
"matrix_labels": MATRIX_LABELS,
|
||||||
|
**_v2_template_context(current_user),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# Allgemeine Felderklärungen für die GWÖ-Matrix — alltagssprachlich, nicht
|
||||||
|
# antragsspezifisch (die antragsspezifische Begründung kommt aus dem
|
||||||
|
# LLM-Output via _row_to_detail/matrix_dict). Geteilt zwischen / (Index)
|
||||||
|
# und /antrag/ /v3/antrag/.
|
||||||
|
_MATRIX_EXPLANATIONS = {
|
||||||
"A1": "Wenn Ihre Stadt Büromöbel kauft: Wurden die unter menschenwürdigen Bedingungen hergestellt? Oder in einer Fabrik, in der Arbeiter:innen ausgebeutet werden? Hier geht es darum, ob die öffentliche Hand beim Einkauf auf Menschenrechte achtet.",
|
"A1": "Wenn Ihre Stadt Büromöbel kauft: Wurden die unter menschenwürdigen Bedingungen hergestellt? Oder in einer Fabrik, in der Arbeiter:innen ausgebeutet werden? Hier geht es darum, ob die öffentliche Hand beim Einkauf auf Menschenrechte achtet.",
|
||||||
"A2": "Beauftragt die Stadt den Handwerksbetrieb aus dem Ort — oder den billigsten Konzern aus dem Ausland? Bleibt das Geld in der Region und schafft Arbeitsplätze vor Ort?",
|
"A2": "Beauftragt die Stadt den Handwerksbetrieb aus dem Ort — oder den billigsten Konzern aus dem Ausland? Bleibt das Geld in der Region und schafft Arbeitsplätze vor Ort?",
|
||||||
"A3": "Werden bei öffentlichen Aufträgen Klimastandards verlangt? Kommt das Schulessen von regionalen Bauernhöfen oder wird es quer durch Europa gekarrt?",
|
"A3": "Werden bei öffentlichen Aufträgen Klimastandards verlangt? Kommt das Schulessen von regionalen Bauernhöfen oder wird es quer durch Europa gekarrt?",
|
||||||
@ -353,10 +371,28 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona
|
|||||||
"E3": "Denkt Ihre Kommune beim Einkauf auch an den CO₂-Fußabdruck? An die Abholzung von Regenwäldern für billiges Papier? An den Wasserverbrauch in Dürregebieten?",
|
"E3": "Denkt Ihre Kommune beim Einkauf auch an den CO₂-Fußabdruck? An die Abholzung von Regenwäldern für billiges Papier? An den Wasserverbrauch in Dürregebieten?",
|
||||||
"E4": "Unterstützt Ihre Stadt strukturschwache Regionen? Gibt es Partnerschaften mit Kommunen im globalen Süden?",
|
"E4": "Unterstützt Ihre Stadt strukturschwache Regionen? Gibt es Partnerschaften mit Kommunen im globalen Süden?",
|
||||||
"E5": "Setzt sich Ihre Kommune für mehr Demokratie ein — auch auf Landes- und Bundesebene? Werden internationale Abkommen unterstützt?",
|
"E5": "Setzt sich Ihre Kommune für mehr Demokratie ein — auch auf Landes- und Bundesebene? Werden internationale Abkommen unterstützt?",
|
||||||
},
|
}
|
||||||
"matrix_labels": MATRIX_LABELS,
|
|
||||||
**_v2_template_context(current_user),
|
|
||||||
})
|
@app.get("/antrag/{drucksache:path}", response_class=HTMLResponse)
|
||||||
|
async def antrag_detail(request: Request, drucksache: str, current_user: Optional[dict] = Depends(get_current_user)):
|
||||||
|
"""v2-Antrag-Detail (Profi-Modus): volles GWÖ-Dashboard pro Drucksache."""
|
||||||
|
return await _render_antrag_detail(
|
||||||
|
request, drucksache, current_user, "v2/screens/antrag_detail.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v3/antrag/{drucksache:path}", response_class=HTMLResponse)
|
||||||
|
async def antrag_detail_v3(request: Request, drucksache: str, current_user: Optional[dict] = Depends(get_current_user)):
|
||||||
|
"""v3-Antrag-Detail (Bürger:innen-Modus, Beta): vereinfachte Vorschau.
|
||||||
|
|
||||||
|
Sandbox fuer Issues #184 (CD-Konformitaet) und #185 (Bürgerinnen-
|
||||||
|
Perspektive). Initial identisch zu v2; iterative Vereinfachungen
|
||||||
|
folgen pro PR. v2-Endpoint bleibt unangetastet.
|
||||||
|
"""
|
||||||
|
return await _render_antrag_detail(
|
||||||
|
request, drucksache, current_user, "v3/screens/antrag_detail.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _v2_template_context(current_user=None) -> dict:
|
def _v2_template_context(current_user=None) -> dict:
|
||||||
|
|||||||
46
app/static/v3/v3.css
Normal file
46
app/static/v3/v3.css
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* v3.css — Bürgerinnen-Modus-Erweiterung über v2.css
|
||||||
|
*
|
||||||
|
* v3 = Sandbox für Issues #184 (CD-Konformität) und #185 (Bürgerinnen-
|
||||||
|
* Perspektive). Lädt nach v2.css und überschreibt selektiv. v2-Endpoints
|
||||||
|
* bleiben unverändert.
|
||||||
|
*
|
||||||
|
* Konvention:
|
||||||
|
* - Klassen mit Präfix `v3-` sind v3-spezifisch.
|
||||||
|
* - v2-Klassen werden nur überschrieben, wenn das Citizen-Bedürfnis es
|
||||||
|
* verlangt (Wort-Etiketten statt Symbol, weniger Tiefe als Default).
|
||||||
|
* - Inline-Styles in v3-Templates sind verboten (Lint-Hook folgt in #184).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ── v3-Beta-Indikator in der Topbar ────────────────────────────────── */
|
||||||
|
.v3-beta-badge {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--ecg-green);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Modus-Toggle: zwischen Profi (/antrag/...) und Bürgerin (/v3/...) ── */
|
||||||
|
.v3-modus-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--ecg-dark);
|
||||||
|
opacity: 0.85;
|
||||||
|
border-bottom: 1px solid rgba(0, 157, 165, 0.35);
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v3-modus-toggle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--ecg-blue);
|
||||||
|
}
|
||||||
@ -649,6 +649,21 @@ document.addEventListener('keydown', function (e) {
|
|||||||
history.back();
|
history.back();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* v3-Toggle in der Topbar: nur bei vorhandenem Antrag, Link auf
|
||||||
|
/v3/antrag/<drs>. Injektion via JS, damit v2/base.html unangetastet bleibt. */
|
||||||
|
{% if antrag is defined and antrag and antrag.drucksache %}
|
||||||
|
(function () {
|
||||||
|
var bar = document.querySelector('.v2-topbar');
|
||||||
|
if (!bar) return;
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.href = '/v3/antrag/' + encodeURIComponent({{ antrag.drucksache | tojson }});
|
||||||
|
link.textContent = '→ Bürger:innen-Modus · v3 Beta';
|
||||||
|
link.title = 'Vereinfachte Vorschau für Erst-Leser:innen (v3 Beta).';
|
||||||
|
link.style.marginLeft = 'auto';
|
||||||
|
bar.appendChild(link);
|
||||||
|
})();
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{# Matrix-Erklärungen als JSON in den Browser übertragen #}
|
{# Matrix-Erklärungen als JSON in den Browser übertragen #}
|
||||||
|
|||||||
17
app/templates/v3/base.html
Normal file
17
app/templates/v3/base.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{# ─────────────────────────────────────────────────────────────────────
|
||||||
|
v3/base.html — Bürgerinnen-Modus-Shell
|
||||||
|
|
||||||
|
Erweitert v2/base.html und kann perspektivisch Sidebar/Topbar/Footer
|
||||||
|
ueberschreiben. Initial: ein zusaetzliches v3.css fuer Bürgerinnen-
|
||||||
|
spezifische Komponenten + ein Beta-Indikator in der Topbar.
|
||||||
|
|
||||||
|
Der v3-Endpoint (Routes: /v3/antrag/...) ist die Sandbox fuer Issue
|
||||||
|
#184 (CD-Konformitaet) und #185 (Buergerinnen-Perspektive). v2 bleibt
|
||||||
|
als Profi-Modus unangetastet.
|
||||||
|
───────────────────────────────────────────────────────────────────── #}
|
||||||
|
{% extends "v2/base.html" %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/static/v3/v3.css?v={{ app_version|default('1') }}">
|
||||||
|
{% endblock %}
|
||||||
47
app/templates/v3/screens/antrag_detail.html
Normal file
47
app/templates/v3/screens/antrag_detail.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{# ─────────────────────────────────────────────────────────────────────
|
||||||
|
v3/screens/antrag_detail.html — Bürgerinnen-Modus, Schritt 0
|
||||||
|
|
||||||
|
Initial extendet diese Datei das v2-Antrag-Detail unverändert und
|
||||||
|
fügt nur den v3-Beta-Indikator + Modus-Toggle in die Topbar ein.
|
||||||
|
Folgende Iterationen ersetzen Profi-Blöcke durch Bürgerinnen-
|
||||||
|
Varianten (siehe #185):
|
||||||
|
- Score-Hero: Wort-Etikett statt 0–10-Zahl
|
||||||
|
- Matrix 5×5 → 5 Werte (Berührungsgruppen kollabiert)
|
||||||
|
- Heuchelei/Opportunismus-Marker mit Klartext-Tooltip
|
||||||
|
- Verbesserungsvorschläge default kollabiert
|
||||||
|
- Glossar-Tooltips auf Schlüsselbegriffen
|
||||||
|
───────────────────────────────────────────────────────────────────── #}
|
||||||
|
{% extends "v2/screens/antrag_detail.html" %}
|
||||||
|
|
||||||
|
{# v3-Indikator + Toggle: erscheint im topbar-Slot, der in v2/base.html
|
||||||
|
aktuell nicht als Block exponiert ist. Wir nutzen daher head_extra
|
||||||
|
als Eingangstor und injizieren via JS einen kleinen Topbar-Pill.
|
||||||
|
Das vermeidet ein Refactor von v2/base.html, der v2 beruehren wuerde. #}
|
||||||
|
{% block head_extra %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/static/v3/v3.css?v={{ app_version|default('1') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body_scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script>
|
||||||
|
/* v3-Topbar-Pill + Toggle zurueck zu Profi-Modus injizieren */
|
||||||
|
(function () {
|
||||||
|
var bar = document.querySelector('.v2-topbar');
|
||||||
|
if (!bar) return;
|
||||||
|
var pill = document.createElement('span');
|
||||||
|
pill.className = 'v3-beta-badge';
|
||||||
|
pill.textContent = 'Bürger:innen-Modus · Beta';
|
||||||
|
pill.title = 'Vereinfachte Ansicht für Erst-Leser:innen. v3 ist eine frühe Vorschau.';
|
||||||
|
|
||||||
|
var toggle = document.createElement('a');
|
||||||
|
toggle.className = 'v3-modus-toggle';
|
||||||
|
toggle.href = '/antrag/' + encodeURIComponent({{ antrag.drucksache | tojson }});
|
||||||
|
toggle.textContent = '→ Profi-Modus';
|
||||||
|
toggle.title = 'Volle GWÖ-Detailansicht (v2) öffnen';
|
||||||
|
|
||||||
|
bar.appendChild(pill);
|
||||||
|
bar.appendChild(toggle);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Loading…
Reference in New Issue
Block a user