init meinantrag

This commit is contained in:
Jonas Heinrich 2025-11-21 11:42:19 +01:00
parent a26fa2c64a
commit fa8f03a450
11 changed files with 779 additions and 3355 deletions

View file

@ -1,10 +1,10 @@
# Fragify
# MeinAntrag
Eine einfache Web-Anwendung, um vorausgefüllte Links für Anfragen bei [FragDenStaat.de](https://fragdenstaat.de) zu generieren, die du an Freund:innen schicken kannst.
## Was ist Fragify?
## Was ist MeinAntrag?
Fragify ist ein webbasiertes Tool, das es dir ermöglicht, schnell und einfach Anfragen bei deutschen Behörden über das Informationsfreiheitsportal FragDenStaat.de zu erstellen. Du kannst:
MeinAntrag ist ein webbasiertes Tool, das es dir ermöglicht, schnell und einfach Anfragen bei deutschen Behörden über das Informationsfreiheitsportal FragDenStaat.de zu erstellen. Du kannst:
- Nach Behörden suchen und auswählen
- Betreff und Inhalt der Anfrage vorausfüllen
@ -20,7 +20,7 @@ Füge das Modul zu deiner `flake.nix` hinzu:
```nix
{
inputs = {
fragify.url = "git+https://git.project-insanity.org/onny/fragify.git";
meinantrag.url = "git+https://git.project-insanity.org/onny/meinantrag.git";
[...]
};
@ -30,12 +30,12 @@ Füge das Modul zu deiner `flake.nix` hinzu:
system = "x86_64-linux";
specialArgs.inputs = inputs;
modules = [
inputs.fragify.nixosModule
inputs.meinantrag.nixosModule
({ pkgs, ... }:{
nixpkgs.overlays = [
inputs.fragify.overlay
inputs.meinantrag.overlay
];
})
@ -51,7 +51,7 @@ Füge das Modul zu deiner `flake.nix` hinzu:
Füge dies zu deiner `configuration.nix` hinzu:
```nix
services.fragify = {
services.meinantrag = {
enable = true;
};
```
@ -59,7 +59,7 @@ services.fragify = {
### Von der Quelle
```bash
cd fragify
cd meinantrag
nix develop
nix run
```
@ -106,18 +106,18 @@ Die gebauten Dateien landen in `assets/` und werden vom Server unter `/static/..
## Deployment mit Nix/uWSGI
- Das Nix-Paket installiert Templates und (falls vorhanden) `assets/` nach `$out/share/fragify/...`.
- Das NixOS-Modul startet uWSGI und erzeugt einen UNIX-Socket unter `unix:${config.services.uwsgi.runDir}/fragify.sock`.
- Das Nix-Paket installiert Templates und (falls vorhanden) `assets/` nach `$out/share/meinantrag/...`.
- Das NixOS-Modul startet uWSGI und erzeugt einen UNIX-Socket unter `unix:${config.services.uwsgi.runDir}/meinantrag.sock`.
- Die App respektiert folgende Umgebungsvariablen:
- `FRAGIFY_TEMPLATES_DIR` Pfad zu den Templates
- `FRAGIFY_STATIC_DIR` Pfad zu den statischen Assets (`assets/`)
- `MEINANTRAG_TEMPLATES_DIR` Pfad zu den Templates
- `MEINANTRAG_STATIC_DIR` Pfad zu den statischen Assets (`assets/`)
Beispiel (im uWSGI-Instance Block):
```nix
services.uwsgi.instance.fragify = {
services.uwsgi.instance.meinantrag = {
env = {
FRAGIFY_TEMPLATES_DIR = "${pkgs.fragify}/share/fragify/templates";
FRAGIFY_STATIC_DIR = "${pkgs.fragify}/share/fragify/assets";
MEINANTRAG_TEMPLATES_DIR = "${pkgs.meinantrag}/share/meinantrag/templates";
MEINANTRAG_STATIC_DIR = "${pkgs.meinantrag}/share/meinantrag/assets";
};
};
```
@ -131,7 +131,7 @@ services.uwsgi.instance.fragify = {
nix develop
# Anwendung starten
python fragify.py
python meinantrag.py
```
### Abhängigkeiten

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1755471983,
"narHash": "sha256-axUoWcm4cNQ36jOlnkD9D40LTfSQgk8ExfHSRm3rTtg=",
"lastModified": 1763622513,
"narHash": "sha256-1jQnuyu82FpiSxowrF/iFK6Toh9BYprfDqfs4BB+19M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "48f4c982de68d966421d2b6f1ddbeb6227cc5ceb",
"rev": "c58bc7f5459328e4afac201c5c4feb7c818d604b",
"type": "github"
},
"original": {

View file

@ -1,5 +1,5 @@
{
description = "fragify package and service";
description = "meinantrag package and service";
inputs.nixpkgs.url = "nixpkgs/nixos-25.05";
@ -16,8 +16,8 @@
);
in {
overlay = final: prev: {
fragify = with final; python3Packages.buildPythonApplication rec {
pname = "fragify";
meinantrag = with final; python3Packages.buildPythonApplication rec {
pname = "meinantrag";
version = "0.0.1";
format = "other";
@ -28,28 +28,28 @@
dependencies = with python3Packages; [ falcon requests jinja2 ];
installPhase = ''
install -Dm755 ${./fragify.py} $out/bin/fragify
mkdir -p $out/share/fragify
cp -r ${./templates} $out/share/fragify/templates
install -Dm755 ${./meinantrag.py} $out/bin/meinantrag
mkdir -p $out/share/meinantrag
cp -r ${./templates} $out/share/meinantrag/templates
# Provide a WSGI entry file for uWSGI to load
install -Dm644 ${./fragify.py} $out/share/fragify/fragify_wsgi.py
install -Dm644 ${./meinantrag.py} $out/share/meinantrag/meinantrag_wsgi.py
# Install built assets if present
if [ -d ./assets ]; then
cp -r ./assets $out/share/fragify/
cp -r ./assets $out/share/meinantrag/
fi
'';
passthru.pythonPath = python3Packages.makePythonPath dependencies;
meta.mainProgram = "fragify";
meta.mainProgram = "meinantrag";
};
};
packages = forAllSystems (system: {
inherit (nixpkgsFor.${system}) fragify;
inherit (nixpkgsFor.${system}) meinantrag;
});
defaultPackage = forAllSystems (system: self.packages.${system}.fragify);
defaultPackage = forAllSystems (system: self.packages.${system}.meinantrag);
devShells = forAllSystems (system: let
pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; };
@ -61,7 +61,7 @@
];
});
# fragify service module
# meinantrag service module
nixosModule = (import ./module.nix);
};
}

View file

@ -1,250 +0,0 @@
#!/usr/bin/env python3
"""
Fragify - A web application to generate prefilled FragDenStaat.de request links
"""
import falcon
import json
import requests
from urllib.parse import urlencode
import os
import sys
from jinja2 import Environment, FileSystemLoader
SITE_BASE_URL = os.environ.get('FRAGIFY_BASE_URL', 'http://localhost:8000')
class BaseTemplateResource:
"""Base class for resources that need template rendering"""
def _get_template_dir(self):
"""Get the template directory path, handling both development and installed environments"""
# Allow overriding via environment variable (for packaged deployments)
env_dir = os.environ.get('FRAGIFY_TEMPLATES_DIR')
if env_dir and os.path.exists(env_dir):
return env_dir
# Get the directory where this script is located
script_dir = os.path.dirname(os.path.abspath(__file__))
# Try development templates first
dev_template_dir = os.path.join(script_dir, 'templates')
if os.path.exists(dev_template_dir):
return dev_template_dir
# Try to find templates relative to the executable
try:
# If we're running from a Nix store, look for templates in share/fragify
if '/nix/store/' in script_dir:
# Go up from bin to share/fragify/templates
share_dir = os.path.join(script_dir, '..', 'share', 'fragify', 'templates')
if os.path.exists(share_dir):
return share_dir
# Alternative: look for templates in the same store path
store_root = script_dir.split('/nix/store/')[1].split('/')[0]
store_path = f"/nix/store/{store_root}"
alt_share_dir = os.path.join(store_path, 'share', 'fragify', 'templates')
if os.path.exists(alt_share_dir):
return alt_share_dir
except Exception:
pass
# Last resort: try to find any templates directory
for root, dirs, files in os.walk('/nix/store'):
if 'templates' in dirs and 'index.html' in os.listdir(os.path.join(root, 'templates')):
return os.path.join(root, 'templates')
# Fallback to current directory
return dev_template_dir
class FragifyApp(BaseTemplateResource):
def __init__(self):
self.fragdenstaat_api = "https://fragdenstaat.de/api/v1"
# Setup Jinja2 template environment
template_dir = self._get_template_dir()
print(f"Using template directory: {template_dir}")
self.jinja_env = Environment(loader=FileSystemLoader(template_dir))
def on_get(self, req, resp):
"""Serve the main page"""
template = self.jinja_env.get_template('index.html')
resp.content_type = 'text/html; charset=utf-8'
resp.text = template.render(
meta_title='Fragify Anfragelinks für FragDenStaat',
meta_description='Erstelle vorausgefüllte Anfragelinks für FragDenStaat.de, suche Behörden, füge Betreff und Text hinzu und teile den Link.',
canonical_url=f"{SITE_BASE_URL}/"
)
def on_post(self, req, resp):
"""Handle form submission and generate link"""
try:
# Parse form data - use get_param for form fields
publicbody_id = req.get_param('publicbody_id', default='')
subject = req.get_param('subject', default='')
body = req.get_param('body', default='')
# Generate FragDenStaat.de link
base_url = "https://fragdenstaat.de/anfrage-stellen/"
if publicbody_id:
base_url += f"an/{publicbody_id}/"
params = {}
if subject:
params['subject'] = subject
if body:
params['body'] = body
if params:
base_url += "?" + urlencode(params)
resp.content_type = 'application/json'
resp.text = json.dumps({
'success': True,
'link': base_url
})
except Exception as e:
resp.status = falcon.HTTP_500
resp.content_type = 'application/json'
resp.text = json.dumps({
'success': False,
'error': str(e)
})
class ImpressumResource(BaseTemplateResource):
def __init__(self):
template_dir = self._get_template_dir()
self.jinja_env = Environment(loader=FileSystemLoader(template_dir))
def on_get(self, req, resp):
"""Serve the Impressum page"""
template = self.jinja_env.get_template('impressum.html')
resp.content_type = 'text/html; charset=utf-8'
resp.text = template.render(
meta_title='Impressum Fragify',
meta_description='Impressum für Fragify.',
canonical_url=f"{SITE_BASE_URL}/impressum",
noindex=True
)
class DatenschutzResource(BaseTemplateResource):
def __init__(self):
template_dir = self._get_template_dir()
self.jinja_env = Environment(loader=FileSystemLoader(template_dir))
def on_get(self, req, resp):
"""Serve the Datenschutz page"""
template = self.jinja_env.get_template('datenschutz.html')
resp.content_type = 'text/html; charset=utf-8'
resp.text = template.render(
meta_title='Datenschutz Fragify',
meta_description='Datenschutzerklärung für Fragify. Keine Cookies, es werden nur Anfragen an die FragDenStaat-API gestellt.',
canonical_url=f"{SITE_BASE_URL}/datenschutz",
noindex=True
)
class PublicBodiesResource:
def __init__(self):
self.fragdenstaat_api = "https://fragdenstaat.de/api/v1"
def on_get(self, req, resp):
"""API endpoint to search public bodies"""
try:
search = req.get_param('search', default='')
page = req.get_param('page', default=1)
# Build API URL
url = f"{self.fragdenstaat_api}/publicbody/"
params = {
'limit': 20,
'offset': (int(page) - 1) * 20
}
if search:
params['q'] = search
# Make request to FragDenStaat API
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
resp.content_type = 'application/json'
resp.text = json.dumps(data)
except Exception as e:
resp.status = falcon.HTTP_500
resp.content_type = 'application/json'
resp.text = json.dumps({
'error': str(e),
'results': [],
'next': None
})
class RobotsResource:
def on_get(self, req, resp):
resp.content_type = 'text/plain; charset=utf-8'
resp.text = f"""User-agent: *
Allow: /
Sitemap: {SITE_BASE_URL}/sitemap.xml
"""
class SitemapResource:
def on_get(self, req, resp):
resp.content_type = 'application/xml; charset=utf-8'
resp.text = f"""<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>{SITE_BASE_URL}/</loc></url>
<url><loc>{SITE_BASE_URL}/impressum</loc></url>
<url><loc>{SITE_BASE_URL}/datenschutz</loc></url>
</urlset>
"""
# Create Falcon application
app = falcon.App()
# Discover static assets directory
STATIC_DIR = os.environ.get('FRAGIFY_STATIC_DIR')
if not STATIC_DIR:
# Prefer local assets folder in development (relative to this file)
script_dir = os.path.dirname(os.path.abspath(__file__))
candidate = os.path.join(script_dir, 'assets')
if os.path.isdir(candidate):
STATIC_DIR = candidate
else:
# Try current working directory (useful when running packaged binary from project root)
cwd_candidate = os.path.join(os.getcwd(), 'assets')
if os.path.isdir(cwd_candidate):
STATIC_DIR = cwd_candidate
else:
# Fallback to packaged location under share
STATIC_DIR = os.path.join(script_dir, '..', 'share', 'fragify', 'assets')
# Add routes
fragify = FragifyApp()
impressum = ImpressumResource()
datenschutz = DatenschutzResource()
publicbodies = PublicBodiesResource()
robots = RobotsResource()
sitemap = SitemapResource()
app.add_route('/', fragify)
app.add_route('/impressum', impressum)
app.add_route('/datenschutz', datenschutz)
app.add_route('/api/publicbodies', publicbodies)
app.add_route('/robots.txt', robots)
app.add_route('/sitemap.xml', sitemap)
# Static file route
if STATIC_DIR and os.path.isdir(STATIC_DIR):
app.add_static_route('/static', STATIC_DIR)
if __name__ == '__main__':
import wsgiref.simple_server
print("Starting Fragify web application...")
print("Open your browser and navigate to: http://localhost:8000")
print(f"Serving static assets from: {STATIC_DIR}")
httpd = wsgiref.simple_server.make_server('localhost', 8000, app)
httpd.serve_forever()

View file

@ -6,15 +6,15 @@
}:
let
cfg = config.services.fragify;
cfg = config.services.meinantrag;
in
{
options = {
services.fragify = {
services.meinantrag = {
enable = lib.mkEnableOption "Fragify web app";
enable = lib.mkEnableOption "MeinAntrag web app";
};
};
@ -28,13 +28,13 @@ in
instance = {
type = "emperor";
vassals = {
fragify = {
meinantrag = {
type = "normal";
chdir = "/";
module = "fragify_wsgi:app";
module = "meinantrag_wsgi:app";
socket = "${config.services.uwsgi.runDir}/fragify.sock";
socket = "${config.services.uwsgi.runDir}/meinantrag.sock";
"chmod-socket" = "660";
umask = "0077";
@ -48,27 +48,27 @@ in
"no-orphans" = true;
env = [
"PYTHONPATH=${pkgs.fragify}/share/fragify:${pkgs.fragify.pythonPath}"
"FRAGIFY_TEMPLATES_DIR=${pkgs.fragify}/share/fragify/templates"
"FRAGIFY_STATIC_DIR=${pkgs.fragify}/share/fragify/assets"
"PYTHONPATH=${pkgs.meinantrag}/share/meinantrag:${pkgs.meinantrag.pythonPath}"
"MEINANTRAG_TEMPLATES_DIR=${pkgs.meinantrag}/share/meinantrag/templates"
"MEINANTRAG_STATIC_DIR=${pkgs.meinantrag}/share/meinantrag/assets"
];
settings = {
"static-map" = "/static=${pkgs.fragify}/share/fragify/assets";
"static-map" = "/static=${pkgs.meinantrag}/share/meinantrag/assets";
};
};
};
};
};
# Ensure fragify user and group exist
users.users.fragify = {
# Ensure meinantrag user and group exist
users.users.meinantrag = {
isSystemUser = true;
group = "fragify";
description = "fragify web application user";
group = "meinantrag";
description = "meinantrag web application user";
};
users.groups.fragify = { };
users.groups.meinantrag = { };
};
meta = {

3758
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
{
"name": "fragify-assets",
"name": "meinantrag-assets",
"version": "0.0.1",
"private": true,
"dependencies": {
"bootstrap": "^5.3.3",
"bootstrap": "^5.3.8",
"jquery": "^3.7.1",
"select2": "^4.1.0-rc.0",
"select2-bootstrap-5-theme": "^1.3.0",
"gulp": "^4.0.2",
"gulp-copy": "^4.0.1"
"gulp": "^5.0.1",
"gulp-copy": "^5.0.0"
},
"scripts": {
"build": "gulp"

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Fragify{% endblock %}</title>
<title>{% block title %}MeinAntrag{% endblock %}</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/select2.min.css" rel="stylesheet">
<link href="/static/css/select2-bootstrap-5-theme.min.css" rel="stylesheet">
@ -13,14 +13,14 @@
<link rel="alternate" hreflang="de" href="{{ canonical_url | default('/') }}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Fragify">
<meta property="og:title" content="{{ meta_title | default('Fragify') }}">
<meta property="og:site_name" content="MeinAntrag">
<meta property="og:title" content="{{ meta_title | default('MeinAntrag') }}">
<meta property="og:description" content="{{ meta_description | default('Erstelle vorausgefüllte FragDenStaat.de-Anfragelinks und teile sie mit Freund:innen.') }}">
<meta property="og:locale" content="de_DE">
<meta property="og:url" content="{{ canonical_url | default('/') }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ meta_title | default('Fragify') }}">
<meta name="twitter:title" content="{{ meta_title | default('MeinAntrag') }}">
<meta name="twitter:description" content="{{ meta_description | default('Erstelle vorausgefüllte FragDenStaat.de-Anfragelinks und teile sie mit Freund:innen.') }}">
{% if noindex %}<meta name="robots" content="noindex,follow">{% endif %}
@ -175,7 +175,7 @@
<div class="footer-links mb-2">
<a href="/impressum" class="text-muted text-decoration-none me-3">Impressum</a>
<a href="/datenschutz" class="text-muted text-decoration-none me-3">Datenschutz</a>
<a href="https://git.project-insanity.org/onny/fragify" class="text-muted text-decoration-none" target="_blank">Source</a>
<a href="https://git.project-insanity.org/onny/MeinAntrag" class="text-muted text-decoration-none" target="_blank">Source</a>
</div>
<div class="footer-text">
<small class="text-muted">

View file

@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block title %}Datenschutz - Fragify{% endblock %}
{% block title %}Datenschutz - MeinAntrag{% endblock %}
{% block content %}
<div class="text-center">
<h1 class="title display-4">
<a href="/" class="text-decoration-none text-dark">Fragify</a>
<a href="/" class="text-decoration-none text-dark">MeinAntrag</a>
</h1>
<h2 class="text-muted h4 mt-3">Datenschutzerklärung</h2>
</div>

View file

@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block title %}Impressum - Fragify{% endblock %}
{% block title %}Impressum - MeinAntrag{% endblock %}
{% block content %}
<div class="text-center">
<h1 class="title display-4">
<a href="/" class="text-decoration-none text-dark">Fragify</a>
<a href="/" class="text-decoration-none text-dark">MeinAntrag</a>
</h1>
<h2 class="text-muted h4 mt-3">Impressum</h2>
</div>

View file

@ -1,17 +1,17 @@
{% extends "base.html" %}
{% block title %}Fragify{% endblock %}
{% block title %}MeinAntrag{% endblock %}
{% block content %}
<div class="text-center">
<h1 class="title display-4">Fragify</h1>
<h1 class="title display-4">MeinAntrag</h1>
<p class="description">
Erstelle einfach Links für Anfragen bei dem Portal
<a href="https://fragdenstaat.de" target="_blank" class="text-decoration-none">FragDenStaat.de</a>,
welche du vorausfüllen und an Freund:innen schicken kannst!
</p>
<form id="fragifyForm" class="text-start">
<form id="meinantragForm" class="text-start">
<div class="mb-4">
<label for="publicbody" class="form-label fw-bold">Behörde</label>
<select class="form-select" id="publicbody" name="publicbody_id" required>
@ -87,7 +87,7 @@
});
// Handle form submission (client-side)
$('#fragifyForm').on('submit', function(e) {
$('#meinantragForm').on('submit', function(e) {
e.preventDefault();
const publicbodyId = $('#publicbody').val() || '';