Compare commits

..

No commits in common. "f5d9873de7aa25abb60f86cf2f6ec8f9bdb5c9a9" and "471bec87fb4532690cb5f007ffb2c2aec42ff92b" have entirely different histories.

16 changed files with 35 additions and 173 deletions

View file

@ -55,11 +55,6 @@ class Question(models.Model):
body = models.TextField() body = models.TextField()
member = models.ForeignKey(Member, related_name="questions", on_delete=models.CASCADE) member = models.ForeignKey(Member, related_name="questions", on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
# asker metadata
asker_first_name = models.CharField(max_length=150, blank=True)
asker_last_name = models.CharField(max_length=150, blank=True)
asker_city = models.CharField(max_length=150, blank=True)
asker_email = models.EmailField(blank=True)
class Meta: class Meta:
verbose_name = "Frage" verbose_name = "Frage"

View file

@ -7,7 +7,6 @@ urlpatterns = [
path("gemeinden/<slug:slug>/", views.public_body_detail, name="public_body_detail"), path("gemeinden/<slug:slug>/", views.public_body_detail, name="public_body_detail"),
path("mitglieder/", views.members, name="members"), path("mitglieder/", views.members, name="members"),
path("mitglieder/<int:pk>/", views.member_detail, name="member_detail"), path("mitglieder/<int:pk>/", views.member_detail, name="member_detail"),
path("mitglieder/<int:pk>/frage/stellen", views.ask_question, name="ask_question"),
path("parteien/", views.parties, name="parties"), path("parteien/", views.parties, name="parties"),
path("parteien/<int:pk>/", views.party_detail, name="party_detail"), path("parteien/<int:pk>/", views.party_detail, name="party_detail"),
path("fragen/", views.questions, name="questions"), path("fragen/", views.questions, name="questions"),

View file

@ -1,5 +1,4 @@
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404
from django.contrib import messages
from .models import PublicBody, Party, Member, Question, Vote from .models import PublicBody, Party, Member, Question, Vote
@ -40,34 +39,6 @@ def member_detail(request, pk: int):
) )
def ask_question(request, pk: int):
member = get_object_or_404(Member, pk=pk)
if request.method != "POST":
return redirect("member_detail", pk=member.pk)
title = request.POST.get("title", "").strip()
asker_first_name = request.POST.get("asker_first_name", "").strip()
asker_last_name = request.POST.get("asker_last_name", "").strip()
asker_city = request.POST.get("asker_city", "").strip()
asker_email = request.POST.get("asker_email", "").strip()
if not title or not asker_first_name or not asker_last_name or not asker_city or not asker_email:
messages.error(request, "Bitte alle Pflichtfelder ausfüllen.")
return redirect("member_detail", pk=member.pk)
Question.objects.create(
title=title,
body=title,
member=member,
asker_first_name=asker_first_name,
asker_last_name=asker_last_name,
asker_city=asker_city,
asker_email=asker_email,
)
messages.success(request, "Frage wurde eingereicht.")
return redirect("member_detail", pk=member.pk)
def parties(request): def parties(request):
items = Party.objects.all() items = Party.objects.all()
return render(request, "council/parties.html", {"items": items}) return render(request, "council/parties.html", {"items": items})

Binary file not shown.

View file

@ -28,16 +28,18 @@
dependencies = with python3Packages; [ django requests jinja2 ]; dependencies = with python3Packages; [ django requests jinja2 ];
installPhase = '' installPhase = ''
mkdir -p $out/share/fragdenrat mkdir -p $out/share
# Project code # Project code
cp -r "${./fragdenrat}/." $out/share/fragdenrat/ cp -r ${./fragdenrat} $out/share/fragdenrat
# Django app: council
cp -r ${./council} $out/share/fragdenrat/council
# Django manage helper # Django manage helper
install -Dm755 ${./manage.py} $out/bin/fragdenrat-manage install -Dm755 ${./manage.py} $out/bin/fragdenrat-manage
# Templates and static assets # Templates and static assets
cp -r ./templates $out/share/fragdenrat/ if [ -d ./templates ]; then
cp -r ./assets $out/share/fragdenrat/ cp -r ./templates $out/share/fragdenrat/
fi
if [ -d ./assets ]; then
cp -r ./assets $out/share/fragdenrat/
fi
''; '';
passthru.pythonPath = python3Packages.makePythonPath dependencies; passthru.pythonPath = python3Packages.makePythonPath dependencies;

View file

@ -2,7 +2,6 @@ from pathlib import Path
import os import os
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = Path(__file__).resolve().parent
DATA_DIR = Path(os.environ.get("FRAGDENRAT_DATA_DIR", str(BASE_DIR))) DATA_DIR = Path(os.environ.get("FRAGDENRAT_DATA_DIR", str(BASE_DIR)))
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key-change-me") SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key-change-me")
@ -34,10 +33,7 @@ ROOT_URLCONF = "fragdenrat.urls"
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [ "DIRS": [BASE_DIR / "templates"],
BASE_DIR / "templates",
PROJECT_DIR / "templates",
],
"APP_DIRS": True, "APP_DIRS": True,
"OPTIONS": { "OPTIONS": {
"context_processors": [ "context_processors": [
@ -65,9 +61,6 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATICFILES_DIRS = [ STATICFILES_DIRS = [BASE_DIR / "assets"]
BASE_DIR / "assets",
PROJECT_DIR / "assets",
]
STATIC_ROOT = DATA_DIR / "staticfiles" STATIC_ROOT = DATA_DIR / "staticfiles"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

View file

@ -1,10 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.views.generic import TemplateView
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("impressum", TemplateView.as_view(template_name="impressum.html"), name="impressum"),
path("datenschutz", TemplateView.as_view(template_name="datenschutz.html"), name="datenschutz"),
path("", include("council.urls")), path("", include("council.urls")),
] ]

View file

@ -5,64 +5,35 @@
... ...
}: }:
let let
cfg = config.services.fragdenrat; cfg = config.services.fragdenrat;
dataDir = "/var/lib/fragdenrat";
manageCmd = "${pkgs.fragdenrat}/bin/fragdenrat-manage";
pythonPath = "${pkgs.fragdenrat}/share:${pkgs.fragdenrat.pythonPath}";
envVars = [
"PYTHONPATH=${pythonPath}"
"DJANGO_SETTINGS_MODULE=fragdenrat.settings"
"FRAGDENRAT_DATA_DIR=${dataDir}"
];
in in
{ {
options = { options = {
services.fragdenrat = { services.fragdenrat = {
enable = lib.mkEnableOption "FragDenRat web app"; enable = lib.mkEnableOption "FragDenRat web app";
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# Ensure data dir exists with proper perms
systemd.tmpfiles.rules = [
"d ${dataDir} 0750 fragdenrat fragdenrat -"
"d ${dataDir}/staticfiles 0750 fragdenrat fragdenrat -"
];
# One-shot setup: migrate DB and collect static into dataDir
systemd.services.fragdenrat-setup = {
description = "Initialize FragDenRat database and static files";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
User = "fragdenrat";
Group = "fragdenrat";
WorkingDirectory = dataDir;
Environment = envVars;
};
script = ''
set -euo pipefail
${manageCmd} migrate --noinput
${manageCmd} collectstatic --noinput
'';
};
# uWSGI app
services.uwsgi = { services.uwsgi = {
enable = true; enable = true;
plugins = [ "python3" ]; plugins = [ "python3" ];
instance = { instance = {
type = "emperor"; type = "emperor";
vassals = { vassals = {
fragdenrat = { fragdenrat = {
type = "normal"; type = "normal";
# Use data dir as working directory chdir = "${pkgs.fragdenrat}/share/fragdenrat";
chdir = dataDir;
# Absolute WSGI entrypoint in Nix store # Use absolute wsgi entrypoint to avoid module resolution issues
wsgi-file = "${pkgs.fragdenrat}/share/fragdenrat/wsgi.py"; wsgi-file = "${pkgs.fragdenrat}/share/fragdenrat/fragdenrat/wsgi.py";
callable = "application"; callable = "application";
socket = "${config.services.uwsgi.runDir}/fragdenrat.sock"; socket = "${config.services.uwsgi.runDir}/fragdenrat.sock";
@ -78,13 +49,15 @@ in
need-app = true; need-app = true;
"no-orphans" = true; "no-orphans" = true;
env = envVars; env = [
# Ensure python sees the app and dependencies
"PYTHONPATH=${pkgs.fragdenrat}/share/fragdenrat:${pkgs.fragdenrat.pythonPath}"
"DJANGO_SETTINGS_MODULE=fragdenrat.settings"
];
settings = { settings = {
# Serve collected static files from dataDir "static-map" = "/static=${pkgs.fragdenrat}/share/fragdenrat/assets";
"static-map" = "/static=${dataDir}/staticfiles"; pythonpath = "${pkgs.fragdenrat}/share/fragdenrat";
# Python import path for the app and its deps
pythonpath = "${pkgs.fragdenrat}/share";
}; };
}; };
}; };

2
result
View file

@ -1 +1 @@
/nix/store/jsjajfw1bbm7h28fh8q64zignm6z09yn-fragdenrat-0.1.0 /nix/store/wqsr5qqw8lj49hrags3pzairq70fwwb6-fragdenrat-0.1.0

View file

@ -13,7 +13,7 @@
.navbar-brand img { height: 36px; } .navbar-brand img { height: 36px; }
</style> </style>
</head> </head>
<body class="d-flex flex-column min-vh-100"> <body>
<nav class="navbar navbar-expand-lg bg-white border-bottom"> <nav class="navbar navbar-expand-lg bg-white border-bottom">
<div class="container"> <div class="container">
<a class="navbar-brand d-flex align-items-center" href="/"> <a class="navbar-brand d-flex align-items-center" href="/">
@ -35,20 +35,10 @@
</div> </div>
</nav> </nav>
<main class="container py-4 flex-grow-1"> <main class="container py-4">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<footer class="border-top bg-white py-3 mt-auto">
<div class="container d-flex justify-content-between">
<span class="text-muted">&copy; {{ now|date:'Y' }} FragDenRat</span>
<nav>
<a href="{% url 'impressum' %}" class="text-muted me-3">Impressum</a>
<a href="{% url 'datenschutz' %}" class="text-muted">Datenschutz</a>
</nav>
</div>
</footer>
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script> <script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>

View file

@ -1,26 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ member }} Stadträt:in{% endblock %} {% block title %}{{ member }} Stadträt:in{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-start"> <h2 class="mb-1">{{ member.first_name }} {{ member.last_name }}</h2>
<div> <p class="text-muted mb-3">
<h2 class="mb-1">{{ member.first_name }} {{ member.last_name }}</h2> {% if member.public_body %}in <a href="{% url 'public_body_detail' slug=member.public_body.slug %}">{{ member.public_body.name }}</a>{% endif %}
<p class="text-muted mb-3"> {% if member.party %} • {{ member.party }} (<a href="{% url 'party_detail' pk=member.party.id %}">Profil</a>){% endif %}
{% if member.public_body %}in <a href="{% url 'public_body_detail' slug=member.public_body.slug %}">{{ member.public_body.name }}</a>{% endif %} </p>
{% if member.party %} • {{ member.party }} (<a href="{% url 'party_detail' pk=member.party.id %}">Profil</a>){% endif %}
</p>
</div>
<div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#askQuestionModal">Frage stellen</button>
</div>
</div>
{% if messages %}
<div class="mt-2">
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<h3 class="mt-4">Fragen an {{ member.first_name }}</h3> <h3 class="mt-4">Fragen an {{ member.first_name }}</h3>
<ul class="list-group mb-4"> <ul class="list-group mb-4">
@ -48,47 +33,4 @@
<li class="list-group-item">Keine Abstimmungen.</li> <li class="list-group-item">Keine Abstimmungen.</li>
{% endfor %} {% endfor %}
</ul> </ul>
<!-- Ask Question Modal -->
<div class="modal fade" id="askQuestionModal" tabindex="-1" aria-labelledby="askQuestionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="askQuestionModalLabel">Frage an {{ member.first_name }} {{ member.last_name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<form method="post" action="{% url 'ask_question' pk=member.id %}">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label for="title" class="form-label">Frage</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="row g-2">
<div class="col-6">
<label for="asker_first_name" class="form-label">Vorname</label>
<input type="text" class="form-control" id="asker_first_name" name="asker_first_name" required>
</div>
<div class="col-6">
<label for="asker_last_name" class="form-label">Name</label>
<input type="text" class="form-control" id="asker_last_name" name="asker_last_name" required>
</div>
</div>
<div class="mb-3 mt-2">
<label for="asker_city" class="form-label">Wohnort</label>
<input type="text" class="form-control" id="asker_city" name="asker_city" required>
</div>
<div class="mb-3">
<label for="asker_email" class="form-label">E-Mail</label>
<input type="email" class="form-control" id="asker_email" name="asker_email" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Senden</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}