Compare commits
No commits in common. "ecaf5ab897801d37e3e8831281e40e6c6c1cff0b" and "8ae984060115951343fdc9b8fbb172150fc80159" have entirely different histories.
ecaf5ab897
...
8ae9840601
9 changed files with 821 additions and 954 deletions
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1772047000,
|
"lastModified": 1766473571,
|
||||||
"narHash": "sha256-7DaQVv4R97cii/Qdfy4tmDZMB2xxtyIvNGSwXBBhSmo=",
|
"narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "1267bb4920d0fc06ea916734c11b0bf004bbe17e",
|
"rev": "76701a179d3a98b07653e2b0409847499b2a07d3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
overlay = final: prev: {
|
overlay = final: prev: {
|
||||||
meinantrag = with final; python3Packages.buildPythonApplication rec {
|
meinantrag = with final; python3Packages.buildPythonApplication rec {
|
||||||
pname = "meinantrag";
|
pname = "meinantrag";
|
||||||
version = "0.0.3";
|
version = "0.0.2";
|
||||||
format = "other";
|
format = "other";
|
||||||
|
|
||||||
src = self;
|
src = self;
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
requests
|
requests
|
||||||
jinja2
|
jinja2
|
||||||
google-generativeai # Dependency for Gemini API
|
google-generativeai # Dependency for Gemini API
|
||||||
grpcio # Required by google-generativeai
|
reportlab # Dependency for PDF generation
|
||||||
python-docx # Dependency for Word document generation
|
python-docx # Dependency for Word document generation
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
1062
meinantrag.py
1062
meinantrag.py
File diff suppressed because it is too large
Load diff
28
module.nix
28
module.nix
|
|
@ -16,31 +16,16 @@ in
|
||||||
|
|
||||||
enable = lib.mkEnableOption "MeinAntrag web app";
|
enable = lib.mkEnableOption "MeinAntrag web app";
|
||||||
|
|
||||||
settings = lib.mkOption {
|
environment = lib.mkOption {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = lib.types.attrsOf lib.types.str;
|
||||||
default = { };
|
default = { };
|
||||||
example = {
|
example = {
|
||||||
|
GOOGLE_GEMINI_API_KEY = "your-api-key-here";
|
||||||
MEINANTRAG_BASE_URL = "https://example.com";
|
MEINANTRAG_BASE_URL = "https://example.com";
|
||||||
GOOGLE_GEMINI_API_KEY = "file:/run/secrets/gemini_api_key";
|
|
||||||
};
|
};
|
||||||
description = ''
|
description = ''
|
||||||
Additional environment variables to pass to the MeinAntrag service.
|
Additional environment variables to pass to the MeinAntrag service.
|
||||||
Values starting with "file:" will be read from the specified path.
|
For example, set GOOGLE_GEMINI_API_KEY for Gemini API integration.
|
||||||
For secrets with systemd LoadCredential, use the credentials option instead.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
credentials = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf lib.types.str;
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
GOOGLE_GEMINI_API_KEY = "/run/secrets/gemini_api_key";
|
|
||||||
};
|
|
||||||
description = ''
|
|
||||||
Credentials to pass to the MeinAntrag service.
|
|
||||||
Maps environment variable names to file paths containing the secret values.
|
|
||||||
These are loaded via systemd's LoadCredential mechanism.
|
|
||||||
The Python app will automatically read the value from the file.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -79,8 +64,7 @@ in
|
||||||
"PYTHONPATH=${pkgs.meinantrag}/share/meinantrag:${pkgs.meinantrag.pythonPath}"
|
"PYTHONPATH=${pkgs.meinantrag}/share/meinantrag:${pkgs.meinantrag.pythonPath}"
|
||||||
"MEINANTRAG_TEMPLATES_DIR=${pkgs.meinantrag}/share/meinantrag/templates"
|
"MEINANTRAG_TEMPLATES_DIR=${pkgs.meinantrag}/share/meinantrag/templates"
|
||||||
"MEINANTRAG_STATIC_DIR=${pkgs.meinantrag}/share/meinantrag/assets"
|
"MEINANTRAG_STATIC_DIR=${pkgs.meinantrag}/share/meinantrag/assets"
|
||||||
] ++ (lib.mapAttrsToList (name: value: "${name}=${value}") cfg.settings)
|
] ++ (lib.mapAttrsToList (name: value: "${name}=${value}") cfg.environment);
|
||||||
++ (lib.mapAttrsToList (name: _: "${name}=file:/run/credentials/uwsgi.service/${name}") cfg.credentials);
|
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
"static-map" = "/static=${pkgs.meinantrag}/share/meinantrag/assets";
|
"static-map" = "/static=${pkgs.meinantrag}/share/meinantrag/assets";
|
||||||
|
|
@ -90,10 +74,6 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Load credentials via systemd's LoadCredential mechanism
|
|
||||||
systemd.services.uwsgi.serviceConfig.LoadCredential =
|
|
||||||
lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials;
|
|
||||||
|
|
||||||
# Ensure meinantrag user and group exist
|
# Ensure meinantrag user and group exist
|
||||||
users.users.meinantrag = {
|
users.users.meinantrag = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,24 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}MeinAntrag{% endblock %}</title>
|
<title>{% block title %}MeinAntrag{% endblock %}</title>
|
||||||
<link type="text/css" href="/static/css/pages.css" media="all" rel="stylesheet">
|
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="/static/css/select2.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">
|
<link href="/static/css/select2-bootstrap-5-theme.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<meta name="description" content="{{ meta_description | default('Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!') }}">
|
<meta name="description" content="{{ meta_description | default('Erstelle vorausgefüllte FragDenStaat.de-Anfragelinks und teile sie mit Freund:innen. Suche Behörden, füge Betreff und Text hinzu und generiere einen teilbaren Link.') }}">
|
||||||
<link rel="canonical" href="{{ canonical_url | default('/') }}">
|
<link rel="canonical" href="{{ canonical_url | default('/') }}">
|
||||||
<link rel="alternate" hreflang="de" href="{{ canonical_url | default('/') }}">
|
<link rel="alternate" hreflang="de" href="{{ canonical_url | default('/') }}">
|
||||||
|
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:site_name" content="MeinAntrag">
|
<meta property="og:site_name" content="MeinAntrag">
|
||||||
<meta property="og:title" content="{{ meta_title | default('MeinAntrag') }}">
|
<meta property="og:title" content="{{ meta_title | default('MeinAntrag') }}">
|
||||||
<meta property="og:description" content="{{ meta_description | default('Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!') }}">
|
<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:locale" content="de_DE">
|
||||||
<meta property="og:url" content="{{ canonical_url | default('/') }}">
|
<meta property="og:url" content="{{ canonical_url | default('/') }}">
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="{{ meta_title | default('MeinAntrag') }}">
|
<meta name="twitter:title" content="{{ meta_title | default('MeinAntrag') }}">
|
||||||
<meta name="twitter:description" content="{{ meta_description | default('Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!') }}">
|
<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 %}
|
{% if noindex %}<meta name="robots" content="noindex,follow">{% endif %}
|
||||||
<meta name="theme-color" content="#667eea">
|
<meta name="theme-color" content="#667eea">
|
||||||
|
|
@ -30,211 +30,61 @@
|
||||||
|
|
||||||
{% block meta_extra %}{% endblock %}
|
{% block meta_extra %}{% endblock %}
|
||||||
<style>
|
<style>
|
||||||
h2 {
|
body {
|
||||||
max-width: 700px !important;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
}
|
min-height: 100vh;
|
||||||
select, textarea, input {
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
max-width: 700px;
|
}
|
||||||
margin-bottom: 1rem;
|
.main-container {
|
||||||
}
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
.tutorial-list li {
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
margin-bottom: 30px;
|
padding: 3rem;
|
||||||
}
|
margin: 2rem auto;
|
||||||
|
max-width: 800px;
|
||||||
.tutorial-list li h4 {
|
}
|
||||||
margin-bottom: 10px;
|
.title {
|
||||||
}
|
color: #2c3e50;
|
||||||
|
font-weight: 700;
|
||||||
.tutorial-list li p {
|
margin-bottom: 1rem;
|
||||||
margin: 10px 0;
|
}
|
||||||
}
|
.description {
|
||||||
|
color: #7f8c8d;
|
||||||
/* Responsive design for mobile devices */
|
font-size: 1.1rem;
|
||||||
@media (max-width: 768px) {
|
margin-bottom: 2.5rem;
|
||||||
h2 {
|
line-height: 1.6;
|
||||||
max-width: 100% !important;
|
}
|
||||||
font-size: 1.2rem;
|
.form-control, .form-select {
|
||||||
padding: 0 1rem;
|
border-radius: 10px;
|
||||||
}
|
border: 2px solid #e9ecef;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
select, textarea, input {
|
transition: all 0.3s ease;
|
||||||
max-width: 100%;
|
}
|
||||||
width: 100%;
|
.form-control:focus, .form-select:focus {
|
||||||
margin-bottom: 1rem;
|
border-color: #667eea;
|
||||||
}
|
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||||
|
}
|
||||||
.tutorial-list li {
|
.btn-primary {
|
||||||
padding: 1rem;
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||||
}
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
.tutorial-list li h4 {
|
padding: 1rem 2rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
margin-bottom: 0.5rem;
|
font-weight: 600;
|
||||||
}
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
.tutorial-list li p {
|
.btn-primary:hover {
|
||||||
font-size: 0.9rem;
|
transform: translateY(-2px);
|
||||||
margin-bottom: 0.5rem;
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
.result-link {
|
||||||
.form-control, .form-select {
|
background: #f8f9fa;
|
||||||
font-size: 16px; /* Prevents zoom on iOS */
|
border: 2px solid #e9ecef;
|
||||||
padding: 0.75rem;
|
border-radius: 10px;
|
||||||
}
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
textarea.form-control {
|
word-break: break-all;
|
||||||
min-height: 150px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#resultFields .btn-primary {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#resultFields .btn-primary:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list li form {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list li form > div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix wrapper width on mobile */
|
|
||||||
.wrapper {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer {
|
|
||||||
width: 100% !important;
|
|
||||||
padding: 20px 1rem;
|
|
||||||
margin: 40px auto 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer .site-footer-links {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
padding: 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer .site-footer-links.right {
|
|
||||||
float: none;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer .site-footer-links li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list p {
|
|
||||||
margin: 15px 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-spot {
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-spot h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-spot h2 {
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial {
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
padding: 0 1rem;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list li {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list li:after {
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
top: -29px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.hero-spot h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-spot h2 {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list li h4 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer .site-footer-links {
|
|
||||||
display: block;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer .site-footer-links.right {
|
|
||||||
float: none;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer .site-footer-links li {
|
|
||||||
display: block;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list li {
|
|
||||||
padding: 1rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list li h4 {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-list li p {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Ensure Select2 scales properly */
|
/* Ensure Select2 scales properly */
|
||||||
.select2-container {
|
.select2-container {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
|
|
@ -250,58 +100,52 @@
|
||||||
.select2-container .select2-selection__arrow {
|
.select2-container .select2-selection__arrow {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
.loading {
|
/* Mobile adjustments */
|
||||||
display: none;
|
@media (max-width: 576px) {
|
||||||
}
|
.main-container {
|
||||||
.form-control, .form-select {
|
padding: 1.25rem;
|
||||||
width: 100%;
|
margin: 1rem auto 4rem auto;
|
||||||
padding: 0.5rem;
|
}
|
||||||
margin-top: 0.5rem;
|
.title {
|
||||||
border: 1px solid #ddd;
|
font-size: 2rem;
|
||||||
border-radius: 3px;
|
}
|
||||||
font-size: 14px;
|
.description {
|
||||||
}
|
font-size: 1rem;
|
||||||
.form-control:focus, .form-select:focus {
|
}
|
||||||
outline: none;
|
|
||||||
border-color: #4078c0;
|
|
||||||
box-shadow: 0 0 0 2px rgba(64, 120, 192, 0.2);
|
|
||||||
}
|
|
||||||
textarea.form-control {
|
|
||||||
min-height: 200px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #4078c0;
|
|
||||||
border-color: #4078c0;
|
|
||||||
color: white;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: #3669a3;
|
|
||||||
border-color: #3669a3;
|
|
||||||
}
|
|
||||||
.btn-primary:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
.loading {
|
.loading {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.loading.active {
|
.footer {
|
||||||
display: inline-block;
|
background: transparent;
|
||||||
margin-left: 0.5rem;
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
.footer-links a {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: #667eea !important;
|
||||||
|
}
|
||||||
|
.footer-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.footer-text a:hover {
|
||||||
|
color: #667eea !important;
|
||||||
|
}
|
||||||
|
.footer-text a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.footer-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
.legal-content {
|
.legal-content {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
.legal-content h2 {
|
.legal-content h2 {
|
||||||
|
color: #2c3e50;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -311,27 +155,44 @@
|
||||||
.legal-content ul {
|
.legal-content ul {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
.main-container {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="home">
|
<body>
|
||||||
{% block content %}{% endblock %}
|
<div class="container">
|
||||||
|
<div class="main-container">
|
||||||
<footer class="page-footer">
|
{% block content %}{% endblock %}
|
||||||
<ul class="site-footer-links right">
|
</div>
|
||||||
<li><a href="/impressum">Impressum</a></li>
|
|
||||||
<li><a href="/datenschutz">Datenschutz</a></li>
|
<!-- Footer -->
|
||||||
<li><a href="https://git.project-insanity.org/onny/MeinAntrag" target="_blank">Source</a></li>
|
<footer class="footer mt-5 pt-4">
|
||||||
</ul>
|
<div class="container">
|
||||||
<ul class="site-footer-links">
|
<div class="row">
|
||||||
<li>© 2025 <span>Project-Insanity.org</span></li>
|
<div class="col-12 text-center">
|
||||||
<li><a href="https://social.project-insanity.org/@pi_crew" target="_blank">Mastodon</a></li>
|
<div class="footer-links mb-2">
|
||||||
</ul>
|
<a href="/impressum" class="text-muted text-decoration-none me-3">Impressum</a>
|
||||||
</footer>
|
<a href="/datenschutz" class="text-muted text-decoration-none me-3">Datenschutz</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">
|
||||||
|
Projekt von <a href="https://project-insanity.org" class="text-muted text-decoration-none" target="_blank">Project-Insanity.org</a>,
|
||||||
|
follow us on <a href="https://social.project-insanity.org/@pi_crew" class="text-muted text-decoration-none" target="_blank">Mastodon</a> :)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/pages-jquery.js"></script>
|
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="/static/js/jquery.min.js"></script>
|
||||||
<script src="/static/js/select2.min.js"></script>
|
<script src="/static/js/select2.min.js"></script>
|
||||||
|
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -3,90 +3,90 @@
|
||||||
{% block title %}MeinAntrag{% endblock %}
|
{% block title %}MeinAntrag{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section id="hero-spot" class="hero-spot">
|
<div class="text-center">
|
||||||
<h1>MeinAntrag</h1>
|
<h1 class="title display-4">MeinAntrag</h1>
|
||||||
<h2>Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!</h2>
|
<p class="description">
|
||||||
</section>
|
Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung
|
||||||
|
zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!
|
||||||
<section id="tutorial" class="tutorial">
|
</p>
|
||||||
|
|
||||||
<ul id="project-site" class="tutorial-list wrapper active">
|
<form id="meinantragForm" class="text-start">
|
||||||
<li class="question">
|
<div id="inputFields">
|
||||||
|
<div class="mb-4">
|
||||||
<form id="meinantragForm">
|
<label for="party" class="form-label fw-bold">Fraktion</label>
|
||||||
<div id="inputFields">
|
<select class="form-select" id="party" name="party_id" required>
|
||||||
<li>
|
<option value="">Fraktion auswählen...</option>
|
||||||
<h4>Fraktion auswählen</h4>
|
<option value="SPD">SPD</option>
|
||||||
<p>Wähle die Fraktion, an die der Antrag gerichtet ist:</p>
|
<option value="GRÜNEN">GRÜNEN</option>
|
||||||
<select class="form-select" id="party" name="party_id" required>
|
<option value="CDU">CDU</option>
|
||||||
<option value="">Fraktion auswählen...</option>
|
<option value="FDP/FW">FDP/FW</option>
|
||||||
<option value="SPD">SPD</option>
|
<option value="Volt">Volt</option>
|
||||||
<option value="GRÜNEN">GRÜNEN</option>
|
<option value="DIE LINKE">DIE LINKE</option>
|
||||||
<option value="CDU">CDU</option>
|
<option value="KAL">KAL</option>
|
||||||
<option value="FDP/FW">FDP/FW</option>
|
</select>
|
||||||
<option value="Volt">Volt</option>
|
|
||||||
<option value="DIE LINKE">DIE LINKE</option>
|
|
||||||
<option value="KAL">KAL</option>
|
|
||||||
</select>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<h4>Dein Anliegen beschreiben</h4>
|
|
||||||
<p>Beschreibe hier, welche Anfrage oder Antrag du an die Stadtverwaltung stellen möchtest:</p>
|
|
||||||
<textarea class="form-control" id="anliegen" name="anliegen" rows="5" required></textarea>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<span class="btn-text">Antrag generieren</span>
|
|
||||||
<span class="loading" role="status" style="display: none;">...</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="resultFields" style="display: none;">
|
<div class="mb-4">
|
||||||
<li>
|
<label for="anliegen" class="form-label fw-bold">Mein Anliegen:</label>
|
||||||
<p><a href="#" id="backLink">← Zurück zum Formular</a></p>
|
<textarea class="form-control" id="anliegen" name="anliegen" rows="5"
|
||||||
<br><br>
|
placeholder="Beschreibe hier, welche Anfrage oder Antrag du an die Stadtverwaltung stellen möchtest ..." required></textarea>
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<h4>Antragstitel</h4>
|
|
||||||
<p></p>
|
|
||||||
<input type="text" class="form-control" id="antragstitel" name="antragstitel">
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<h4>Forderung</h4>
|
|
||||||
<p></p>
|
|
||||||
<textarea class="form-control" id="forderung" name="forderung" rows="5"></textarea>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<h4>Begründung/Sachverhalt</h4>
|
|
||||||
<p></p>
|
|
||||||
<textarea class="form-control" id="begruendung" name="begruendung" rows="8"></textarea>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<h4>Aktionen</h4>
|
|
||||||
<p>Du kannst den Antrag jetzt per E-Mail senden oder als Word-Datei herunterladen:</p>
|
|
||||||
<button type="button" class="btn btn-primary" id="mailBtn">
|
|
||||||
<span id="mailBtnText">Mail an Fraktion senden</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="wordBtn">
|
|
||||||
Word-Datei herunterladen
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
</p>
|
<span class="btn-text">Antrag generieren</span>
|
||||||
</li>
|
<span class="loading spinner-border spinner-border-sm ms-2" role="status"></span>
|
||||||
</ul>
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
|
<div id="resultFields" class="text-start" style="display: none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<a href="#" id="backLink" class="text-decoration-none">
|
||||||
|
<span>← Zurück</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="antragstitel" class="form-label fw-bold">Antragstitel</label>
|
||||||
|
<input type="text" class="form-control" id="antragstitel" name="antragstitel">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="forderung" class="form-label fw-bold">Forderung</label>
|
||||||
|
<textarea class="form-control" id="forderung" name="forderung" rows="5"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="begruendung" class="form-label fw-bold">Begründung/Sachverhalt</label>
|
||||||
|
<textarea class="form-control" id="begruendung" name="begruendung" rows="8"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 d-flex gap-2 flex-wrap justify-content-center">
|
||||||
|
<button type="button" class="btn btn-primary" id="mailBtn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-envelope me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1zm13 2.383-4.708 2.825L15 11.105zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741M1 11.105l4.708-2.897L1 5.383z"/>
|
||||||
|
</svg>
|
||||||
|
<span id="mailBtnText">Mail an Fraktion senden</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="pdfBtn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-pdf me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.5 6a.5.5 0 0 0-1 0v1.5H6a.5.5 0 0 0 0 1h1.5V10a.5.5 0 0 0 1 0V8.5H10a.5.5 0 0 0 0-1H8.5z"/>
|
||||||
|
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||||
|
</svg>
|
||||||
|
PDF anzeigen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="wordBtn">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-word me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.485 6.879a.5.5 0 1 0-.97.242l1.5 6a.5.5 0 0 0 .539.314l1.5-.5a.5.5 0 0 0 .186-.596l-.737-2.945 2.679-3.42a.5.5 0 1 0-.758-.652L6.978 8.616l-1.493-.5z"/>
|
||||||
|
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||||
|
</svg>
|
||||||
|
Word-Datei herunterladen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
|
@ -109,8 +109,8 @@
|
||||||
const $btnText = $button.find('.btn-text');
|
const $btnText = $button.find('.btn-text');
|
||||||
const $loading = $button.find('.loading');
|
const $loading = $button.find('.loading');
|
||||||
$button.prop('disabled', true);
|
$button.prop('disabled', true);
|
||||||
$btnText.text('Generiere Antrag');
|
$btnText.text('Generiere Antrag...');
|
||||||
$loading.css('display', 'inline');
|
$loading.css('display', 'inline-block');
|
||||||
|
|
||||||
// Prepare form data as URL-encoded
|
// Prepare form data as URL-encoded
|
||||||
const formData = new URLSearchParams();
|
const formData = new URLSearchParams();
|
||||||
|
|
@ -142,9 +142,8 @@
|
||||||
$('#forderung').val(data.demand || '');
|
$('#forderung').val(data.demand || '');
|
||||||
$('#begruendung').val(data.justification || '');
|
$('#begruendung').val(data.justification || '');
|
||||||
|
|
||||||
// Store party name and email body for mail button
|
// Store party name for mail button
|
||||||
$('#resultFields').data('party-name', data.party_name || '');
|
$('#resultFields').data('party-name', data.party_name || '');
|
||||||
$('#resultFields').data('email-body', data.email_body || '');
|
|
||||||
|
|
||||||
// Update mail button text
|
// Update mail button text
|
||||||
if (data.party_name) {
|
if (data.party_name) {
|
||||||
|
|
@ -161,7 +160,7 @@
|
||||||
// Reset button state
|
// Reset button state
|
||||||
$button.prop('disabled', false);
|
$button.prop('disabled', false);
|
||||||
$btnText.text('Antrag generieren');
|
$btnText.text('Antrag generieren');
|
||||||
$loading.css('display', 'none');
|
$loading.hide();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
|
@ -170,7 +169,7 @@
|
||||||
// Reset button state
|
// Reset button state
|
||||||
$button.prop('disabled', false);
|
$button.prop('disabled', false);
|
||||||
$btnText.text('Antrag generieren');
|
$btnText.text('Antrag generieren');
|
||||||
$loading.css('display', 'none');
|
$loading.hide();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -183,13 +182,13 @@
|
||||||
|
|
||||||
// Mail-Kontakte für Fraktionen
|
// Mail-Kontakte für Fraktionen
|
||||||
const partyContacts = {
|
const partyContacts = {
|
||||||
'SPD': 'spd@fraktion.karlsruhe.de',
|
'SPD': 'spd@karlsruhe.de',
|
||||||
'GRÜNEN': 'gruene@fraktion.karlsruhe.de',
|
'GRÜNEN': 'gruene@karlsruhe.de',
|
||||||
'CDU': 'cdu@fraktion.karlsruhe.de',
|
'CDU': 'cdu@karlsruhe.de',
|
||||||
'FDP/FW': 'fdp-fw@fraktion.karlsruhe.de',
|
'FDP/FW': 'fdp@karlsruhe.de',
|
||||||
'Volt': 'volt@fraktion.karlsruhe.de',
|
'Volt': 'volt@karlsruhe.de',
|
||||||
'DIE LINKE': 'dielinke@gr.karlsruhe.de',
|
'DIE LINKE': 'dielinke@karlsruhe.de',
|
||||||
'KAL': 'kal@fraktion.karlsruhe.de'
|
'KAL': 'kal@karlsruhe.de'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle mail button click
|
// Handle mail button click
|
||||||
|
|
@ -197,9 +196,18 @@
|
||||||
const partyName = $('#resultFields').data('party-name') || '';
|
const partyName = $('#resultFields').data('party-name') || '';
|
||||||
const email = partyContacts[partyName] || '';
|
const email = partyContacts[partyName] || '';
|
||||||
const subject = encodeURIComponent($('#antragstitel').val() || '');
|
const subject = encodeURIComponent($('#antragstitel').val() || '');
|
||||||
const emailBody = $('#resultFields').data('email-body') || '';
|
const title = $('#antragstitel').val() || '';
|
||||||
|
const demand = $('#forderung').val() || '';
|
||||||
|
const justification = $('#begruendung').val() || '';
|
||||||
|
|
||||||
const bodyEncoded = encodeURIComponent(emailBody);
|
// Build email body
|
||||||
|
let body = title + '\n\n';
|
||||||
|
body += 'Der Gemeinderat möge beschließen:\n';
|
||||||
|
body += demand + '\n\n';
|
||||||
|
body += 'Begründung/Sachverhalt\n';
|
||||||
|
body += justification;
|
||||||
|
|
||||||
|
const bodyEncoded = encodeURIComponent(body);
|
||||||
|
|
||||||
// Open mail client
|
// Open mail client
|
||||||
if (email) {
|
if (email) {
|
||||||
|
|
@ -209,37 +217,45 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function to create a valid filename from title
|
// Handle PDF button click
|
||||||
function createFilename(title) {
|
$('#pdfBtn').on('click', function() {
|
||||||
if (!title || !title.trim()) {
|
const title = $('#antragstitel').val() || '';
|
||||||
return 'antrag.docx';
|
const demand = $('#forderung').val() || '';
|
||||||
|
const justification = $('#begruendung').val() || '';
|
||||||
|
const partyName = $('#resultFields').data('party-name') || '';
|
||||||
|
|
||||||
|
// Prepare form data
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
formData.append('title', title);
|
||||||
|
formData.append('demand', demand);
|
||||||
|
formData.append('justification', justification);
|
||||||
|
if (partyName) {
|
||||||
|
formData.append('party_name', partyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove or replace special characters
|
// Open PDF in new window
|
||||||
let filename = title
|
fetch('/api/generate-pdf', {
|
||||||
.trim()
|
method: 'POST',
|
||||||
.toLowerCase()
|
headers: {
|
||||||
.replace(/[ä]/g, 'ae')
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
.replace(/[ö]/g, 'oe')
|
},
|
||||||
.replace(/[ü]/g, 'ue')
|
body: formData.toString()
|
||||||
.replace(/[ß]/g, 'ss')
|
})
|
||||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars except spaces and hyphens
|
.then(response => {
|
||||||
.replace(/\s+/g, '_') // Replace spaces with underscores
|
if (!response.ok) {
|
||||||
.replace(/_+/g, '_') // Replace multiple underscores with single
|
throw new Error('Fehler beim Generieren des PDFs');
|
||||||
.replace(/^_+|_+$/g, ''); // Remove leading/trailing underscores
|
}
|
||||||
|
return response.blob();
|
||||||
// Limit length to 100 characters
|
})
|
||||||
if (filename.length > 100) {
|
.then(blob => {
|
||||||
filename = filename.substring(0, 100);
|
const url = window.URL.createObjectURL(blob);
|
||||||
}
|
window.open(url, '_blank');
|
||||||
|
})
|
||||||
// If empty after cleaning, use default
|
.catch(error => {
|
||||||
if (!filename) {
|
console.error('Error:', error);
|
||||||
return 'antrag.docx';
|
alert('Fehler beim Generieren des PDFs: ' + error.message);
|
||||||
}
|
});
|
||||||
|
});
|
||||||
return filename + '.docx';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Word button click
|
// Handle Word button click
|
||||||
$('#wordBtn').on('click', function() {
|
$('#wordBtn').on('click', function() {
|
||||||
|
|
@ -257,9 +273,6 @@
|
||||||
formData.append('party_name', partyName);
|
formData.append('party_name', partyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate filename from title
|
|
||||||
const filename = createFilename(title);
|
|
||||||
|
|
||||||
// Download Word file
|
// Download Word file
|
||||||
fetch('/api/generate-word', {
|
fetch('/api/generate-word', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -278,7 +291,7 @@
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename;
|
a.download = 'antrag.docx';
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
@ -291,4 +304,4 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue