Compare commits
No commits in common. "f5d9873de7aa25abb60f86cf2f6ec8f9bdb5c9a9" and "471bec87fb4532690cb5f007ffb2c2aec42ff92b" have entirely different histories.
f5d9873de7
...
471bec87fb
16 changed files with 35 additions and 173 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
|
|
||||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
14
flake.nix
14
flake.nix
|
|
@ -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;
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -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"
|
||||||
|
|
@ -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")),
|
||||||
]
|
]
|
||||||
59
module.nix
59
module.nix
|
|
@ -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
2
result
|
|
@ -1 +1 @@
|
||||||
/nix/store/jsjajfw1bbm7h28fh8q64zignm6z09yn-fragdenrat-0.1.0
|
/nix/store/wqsr5qqw8lj49hrags3pzairq70fwwb6-fragdenrat-0.1.0
|
||||||
|
|
@ -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">© {{ 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>
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue