diff --git a/council/__pycache__/models.cpython-312.pyc b/council/__pycache__/models.cpython-312.pyc index 3476787..ba5165b 100644 Binary files a/council/__pycache__/models.cpython-312.pyc and b/council/__pycache__/models.cpython-312.pyc differ diff --git a/council/__pycache__/urls.cpython-312.pyc b/council/__pycache__/urls.cpython-312.pyc index 73546aa..b6d3000 100644 Binary files a/council/__pycache__/urls.cpython-312.pyc and b/council/__pycache__/urls.cpython-312.pyc differ diff --git a/council/__pycache__/views.cpython-312.pyc b/council/__pycache__/views.cpython-312.pyc index c138571..d85b83d 100644 Binary files a/council/__pycache__/views.cpython-312.pyc and b/council/__pycache__/views.cpython-312.pyc differ diff --git a/council/models.py b/council/models.py index 40aa047..0ecbc6e 100644 --- a/council/models.py +++ b/council/models.py @@ -55,6 +55,11 @@ class Question(models.Model): body = models.TextField() member = models.ForeignKey(Member, related_name="questions", on_delete=models.CASCADE) 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: verbose_name = "Frage" diff --git a/council/urls.py b/council/urls.py index b97155f..798a6a7 100644 --- a/council/urls.py +++ b/council/urls.py @@ -7,6 +7,7 @@ urlpatterns = [ path("gemeinden//", views.public_body_detail, name="public_body_detail"), path("mitglieder/", views.members, name="members"), path("mitglieder//", views.member_detail, name="member_detail"), + path("mitglieder//frage/stellen", views.ask_question, name="ask_question"), path("parteien/", views.parties, name="parties"), path("parteien//", views.party_detail, name="party_detail"), path("fragen/", views.questions, name="questions"), diff --git a/council/views.py b/council/views.py index 0161474..644bf8b 100644 --- a/council/views.py +++ b/council/views.py @@ -1,4 +1,5 @@ -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib import messages from .models import PublicBody, Party, Member, Question, Vote @@ -39,6 +40,34 @@ 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): items = Party.objects.all() return render(request, "council/parties.html", {"items": items}) diff --git a/db.sqlite3 b/db.sqlite3 index 7ed9b0f..ac4eee1 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/flake.nix b/flake.nix index a1fa7bc..4f8a751 100644 --- a/flake.nix +++ b/flake.nix @@ -28,18 +28,16 @@ dependencies = with python3Packages; [ django requests jinja2 ]; installPhase = '' - mkdir -p $out/share + mkdir -p $out/share/fragdenrat # 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 install -Dm755 ${./manage.py} $out/bin/fragdenrat-manage # Templates and static assets - if [ -d ./templates ]; then - cp -r ./templates $out/share/fragdenrat/ - fi - if [ -d ./assets ]; then - cp -r ./assets $out/share/fragdenrat/ - fi + cp -r ./templates $out/share/fragdenrat/ + cp -r ./assets $out/share/fragdenrat/ ''; passthru.pythonPath = python3Packages.makePythonPath dependencies; diff --git a/fragdenrat/__pycache__/settings.cpython-312.pyc b/fragdenrat/__pycache__/settings.cpython-312.pyc index b3ae2f5..84fad05 100644 Binary files a/fragdenrat/__pycache__/settings.cpython-312.pyc and b/fragdenrat/__pycache__/settings.cpython-312.pyc differ diff --git a/fragdenrat/__pycache__/urls.cpython-312.pyc b/fragdenrat/__pycache__/urls.cpython-312.pyc index 8c58cdf..6cafa8b 100644 Binary files a/fragdenrat/__pycache__/urls.cpython-312.pyc and b/fragdenrat/__pycache__/urls.cpython-312.pyc differ diff --git a/fragdenrat/settings.py b/fragdenrat/settings.py index fd08fb6..c1525c0 100644 --- a/fragdenrat/settings.py +++ b/fragdenrat/settings.py @@ -2,6 +2,7 @@ from pathlib import Path import os 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))) SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key-change-me") @@ -33,7 +34,10 @@ ROOT_URLCONF = "fragdenrat.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "templates"], + "DIRS": [ + BASE_DIR / "templates", + PROJECT_DIR / "templates", + ], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -61,6 +65,9 @@ USE_I18N = True USE_TZ = True STATIC_URL = "/static/" -STATICFILES_DIRS = [BASE_DIR / "assets"] +STATICFILES_DIRS = [ + BASE_DIR / "assets", + PROJECT_DIR / "assets", +] STATIC_ROOT = DATA_DIR / "staticfiles" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" \ No newline at end of file diff --git a/fragdenrat/urls.py b/fragdenrat/urls.py index de090f4..264b370 100644 --- a/fragdenrat/urls.py +++ b/fragdenrat/urls.py @@ -1,7 +1,10 @@ from django.contrib import admin from django.urls import path, include +from django.views.generic import TemplateView urlpatterns = [ 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")), ] \ No newline at end of file diff --git a/module.nix b/module.nix index 390bfaa..6e95863 100644 --- a/module.nix +++ b/module.nix @@ -5,35 +5,64 @@ ... }: let - 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 { - options = { services.fragdenrat = { - enable = lib.mkEnableOption "FragDenRat web app"; - }; }; 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 = { enable = true; plugins = [ "python3" ]; - instance = { type = "emperor"; vassals = { fragdenrat = { type = "normal"; - chdir = "${pkgs.fragdenrat}/share/fragdenrat"; + # Use data dir as working directory + chdir = dataDir; - # Use absolute wsgi entrypoint to avoid module resolution issues - wsgi-file = "${pkgs.fragdenrat}/share/fragdenrat/fragdenrat/wsgi.py"; + # Absolute WSGI entrypoint in Nix store + wsgi-file = "${pkgs.fragdenrat}/share/fragdenrat/wsgi.py"; callable = "application"; socket = "${config.services.uwsgi.runDir}/fragdenrat.sock"; @@ -49,15 +78,13 @@ in need-app = true; "no-orphans" = true; - env = [ - # Ensure python sees the app and dependencies - "PYTHONPATH=${pkgs.fragdenrat}/share/fragdenrat:${pkgs.fragdenrat.pythonPath}" - "DJANGO_SETTINGS_MODULE=fragdenrat.settings" - ]; + env = envVars; settings = { - "static-map" = "/static=${pkgs.fragdenrat}/share/fragdenrat/assets"; - pythonpath = "${pkgs.fragdenrat}/share/fragdenrat"; + # Serve collected static files from dataDir + "static-map" = "/static=${dataDir}/staticfiles"; + # Python import path for the app and its deps + pythonpath = "${pkgs.fragdenrat}/share"; }; }; }; diff --git a/result b/result index 8440bcc..430629b 120000 --- a/result +++ b/result @@ -1 +1 @@ -/nix/store/wqsr5qqw8lj49hrags3pzairq70fwwb6-fragdenrat-0.1.0 \ No newline at end of file +/nix/store/jsjajfw1bbm7h28fh8q64zignm6z09yn-fragdenrat-0.1.0 \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 2e3eaca..327f623 100644 --- a/templates/base.html +++ b/templates/base.html @@ -13,7 +13,7 @@ .navbar-brand img { height: 36px; } - + -
+
{% block content %}{% endblock %}
+
+ {% block extra_js %}{% endblock %} diff --git a/templates/council/member_detail.html b/templates/council/member_detail.html index 4d78d99..16b1645 100644 --- a/templates/council/member_detail.html +++ b/templates/council/member_detail.html @@ -1,11 +1,26 @@ {% extends "base.html" %} {% block title %}{{ member }} – Stadträt:in{% endblock %} {% block content %} -

{{ member.first_name }} {{ member.last_name }}

-

- {% if member.public_body %}in {{ member.public_body.name }}{% endif %} - {% if member.party %} • {{ member.party }} (Profil){% endif %} -

+
+
+

{{ member.first_name }} {{ member.last_name }}

+

+ {% if member.public_body %}in {{ member.public_body.name }}{% endif %} + {% if member.party %} • {{ member.party }} (Profil){% endif %} +

+
+
+ +
+
+ +{% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+{% endif %}

Fragen an {{ member.first_name }}

    @@ -33,4 +48,47 @@
  • Keine Abstimmungen.
  • {% endfor %}
+ + + {% endblock %} \ No newline at end of file