generate antrag
This commit is contained in:
parent
6c46660d0c
commit
8ae9840601
5 changed files with 719 additions and 149 deletions
8
flake.lock
generated
8
flake.lock
generated
|
|
@ -2,16 +2,16 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763622513,
|
"lastModified": 1766473571,
|
||||||
"narHash": "sha256-1jQnuyu82FpiSxowrF/iFK6Toh9BYprfDqfs4BB+19M=",
|
"narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c58bc7f5459328e4afac201c5c4feb7c818d604b",
|
"rev": "76701a179d3a98b07653e2b0409847499b2a07d3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"id": "nixpkgs",
|
||||||
"ref": "nixos-25.05",
|
"ref": "nixos-25.11",
|
||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
13
flake.nix
13
flake.nix
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
description = "meinantrag package and service";
|
description = "meinantrag package and service";
|
||||||
|
|
||||||
inputs.nixpkgs.url = "nixpkgs/nixos-25.05";
|
inputs.nixpkgs.url = "nixpkgs/nixos-25.11";
|
||||||
|
|
||||||
outputs = { self, nixpkgs }:
|
outputs = { self, nixpkgs }:
|
||||||
let
|
let
|
||||||
|
|
@ -18,14 +18,21 @@
|
||||||
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.1";
|
version = "0.0.2";
|
||||||
format = "other";
|
format = "other";
|
||||||
|
|
||||||
src = self;
|
src = self;
|
||||||
|
|
||||||
dontBuild = true;
|
dontBuild = true;
|
||||||
|
|
||||||
dependencies = with python3Packages; [ falcon requests jinja2 ];
|
dependencies = with python3Packages; [
|
||||||
|
falcon
|
||||||
|
requests
|
||||||
|
jinja2
|
||||||
|
google-generativeai # Dependency for Gemini API
|
||||||
|
reportlab # Dependency for PDF generation
|
||||||
|
python-docx # Dependency for Word document generation
|
||||||
|
];
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
install -Dm755 ${./meinantrag.py} $out/bin/meinantrag
|
install -Dm755 ${./meinantrag.py} $out/bin/meinantrag
|
||||||
|
|
|
||||||
486
meinantrag.py
486
meinantrag.py
|
|
@ -6,10 +6,25 @@ MeinAntrag - A web application to generate prefilled government requests
|
||||||
import falcon
|
import falcon
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode, parse_qs
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
import google.generativeai as genai
|
||||||
|
import re
|
||||||
|
from io import BytesIO
|
||||||
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from reportlab.lib.units import cm
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
||||||
|
from reportlab.lib.enums import TA_LEFT, TA_JUSTIFY
|
||||||
|
try:
|
||||||
|
from docx import Document
|
||||||
|
from docx.shared import Pt, Inches
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
|
DOCX_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
DOCX_AVAILABLE = False
|
||||||
|
|
||||||
SITE_BASE_URL = os.environ.get('MEINANTRAG_BASE_URL', 'http://localhost:8000')
|
SITE_BASE_URL = os.environ.get('MEINANTRAG_BASE_URL', 'http://localhost:8000')
|
||||||
|
|
||||||
|
|
@ -59,7 +74,6 @@ class BaseTemplateResource:
|
||||||
|
|
||||||
class MeinAntragApp(BaseTemplateResource):
|
class MeinAntragApp(BaseTemplateResource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.fragdenstaat_api = "https://fragdenstaat.de/api/v1"
|
|
||||||
# Setup Jinja2 template environment
|
# Setup Jinja2 template environment
|
||||||
template_dir = self._get_template_dir()
|
template_dir = self._get_template_dir()
|
||||||
print(f"Using template directory: {template_dir}")
|
print(f"Using template directory: {template_dir}")
|
||||||
|
|
@ -74,42 +88,6 @@ class MeinAntragApp(BaseTemplateResource):
|
||||||
meta_description='Erstelle vorausgefüllte Anfragelinks für FragDenStaat.de, suche Behörden, füge Betreff und Text hinzu und teile den Link.',
|
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}/"
|
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):
|
class ImpressumResource(BaseTemplateResource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -143,42 +121,422 @@ class DatenschutzResource(BaseTemplateResource):
|
||||||
noindex=True
|
noindex=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class PublicBodiesResource:
|
class GenerateAntragResource:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.fragdenstaat_api = "https://fragdenstaat.de/api/v1"
|
# Initialize Gemini API
|
||||||
|
api_key = os.environ.get('GOOGLE_GEMINI_API_KEY')
|
||||||
|
if api_key:
|
||||||
|
genai.configure(api_key=api_key)
|
||||||
|
self.model = genai.GenerativeModel('gemini-flash-latest')
|
||||||
|
else:
|
||||||
|
self.model = None
|
||||||
|
|
||||||
def on_get(self, req, resp):
|
def _remove_markdown(self, text):
|
||||||
"""API endpoint to search public bodies"""
|
"""Remove markdown formatting from text"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
|
||||||
|
# Remove bold/italic markdown: **text** or *text* or __text__ or _text_
|
||||||
|
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
||||||
|
text = re.sub(r'\*(.+?)\*', r'\1', text)
|
||||||
|
text = re.sub(r'__(.+?)__', r'\1', text)
|
||||||
|
text = re.sub(r'_(.+?)_', r'\1', text)
|
||||||
|
|
||||||
|
# Remove heading markdown: /Heading or # Heading
|
||||||
|
text = re.sub(r'^/\s*', '', text, flags=re.MULTILINE)
|
||||||
|
text = re.sub(r'^#+\s*', '', text, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Remove other markdown elements
|
||||||
|
text = re.sub(r'`(.+?)`', r'\1', text) # Code
|
||||||
|
text = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', text) # Links
|
||||||
|
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
def _parse_gemini_response(self, text):
|
||||||
|
"""Parse the response from Gemini into title, demand, and justification"""
|
||||||
|
# Remove markdown formatting first
|
||||||
|
text = self._remove_markdown(text)
|
||||||
|
|
||||||
|
# Split by "Begründung/Sachverhalt" or similar patterns
|
||||||
|
parts = re.split(r'(begründung|sachverhalt|begründung/sachverhalt)', text, maxsplit=1, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
if len(parts) >= 3:
|
||||||
|
# We have a split at "Begründung/Sachverhalt"
|
||||||
|
before_justification = parts[0].strip()
|
||||||
|
justification = parts[2].strip() if len(parts) > 2 else ""
|
||||||
|
|
||||||
|
# Remove "Begründung/Sachverhalt" or "Sachverhalt" from the beginning of justification
|
||||||
|
justification = re.sub(r'^(begründung\s*/?\s*sachverhalt|sachverhalt|begründung)\s*:?\s*\n?', '', justification, flags=re.IGNORECASE).strip()
|
||||||
|
else:
|
||||||
|
# Try to split by paragraphs
|
||||||
|
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
|
||||||
|
if len(paragraphs) >= 3:
|
||||||
|
before_justification = '\n\n'.join(paragraphs[:-1])
|
||||||
|
justification = paragraphs[-1]
|
||||||
|
# Remove "Begründung/Sachverhalt" or "Sachverhalt" from the beginning
|
||||||
|
justification = re.sub(r'^(begründung\s*/?\s*sachverhalt|sachverhalt|begründung)\s*:?\s*\n?', '', justification, flags=re.IGNORECASE).strip()
|
||||||
|
else:
|
||||||
|
before_justification = text
|
||||||
|
justification = ""
|
||||||
|
|
||||||
|
# Extract title (first line or first paragraph)
|
||||||
|
title_match = re.match(r'^(.+?)(?:\n\n|\n|$)', before_justification)
|
||||||
|
if title_match:
|
||||||
|
title = title_match.group(1).strip()
|
||||||
|
demand = before_justification[len(title):].strip()
|
||||||
|
else:
|
||||||
|
lines = before_justification.split('\n', 1)
|
||||||
|
title = lines[0].strip()
|
||||||
|
demand = lines[1].strip() if len(lines) > 1 else ""
|
||||||
|
|
||||||
|
# Remove title from demand if it's duplicated
|
||||||
|
if demand.startswith(title):
|
||||||
|
demand = demand[len(title):].strip()
|
||||||
|
|
||||||
|
# Remove markdown from each part
|
||||||
|
title = self._remove_markdown(title)
|
||||||
|
demand = self._remove_markdown(demand)
|
||||||
|
justification = self._remove_markdown(justification)
|
||||||
|
|
||||||
|
# Final cleanup: remove any remaining "Sachverhalt" or "Begründung/Sachverhalt" at the start
|
||||||
|
justification = re.sub(r'^(begründung\s*/?\s*sachverhalt|sachverhalt|begründung)\s*:?\s*\n?\s*', '', justification, flags=re.IGNORECASE).strip()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'demand': demand,
|
||||||
|
'justification': justification
|
||||||
|
}
|
||||||
|
|
||||||
|
def on_post(self, req, resp):
|
||||||
|
"""Generate text from user input using Gemini API"""
|
||||||
try:
|
try:
|
||||||
search = req.get_param('search', default='')
|
if not self.model:
|
||||||
page = req.get_param('page', default=1)
|
resp.status = falcon.HTTP_500
|
||||||
|
resp.content_type = 'application/json'
|
||||||
|
resp.text = json.dumps({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Gemini API key not configured'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
# Build API URL
|
# Get form data - try multiple methods for Falcon compatibility
|
||||||
url = f"{self.fragdenstaat_api}/publicbody/"
|
anliegen = ''
|
||||||
params = {
|
party_id = ''
|
||||||
'limit': 20,
|
|
||||||
'offset': (int(page) - 1) * 20
|
|
||||||
}
|
|
||||||
|
|
||||||
if search:
|
# Method 1: Try get_param (works for URL-encoded form data)
|
||||||
params['q'] = search
|
anliegen = req.get_param('anliegen', default='') or ''
|
||||||
|
party_id = req.get_param('party_id', default='') or ''
|
||||||
|
|
||||||
# Make request to FragDenStaat API
|
# Method 2: If empty, try to read from stream and parse manually
|
||||||
response = requests.get(url, params=params, timeout=10)
|
if not anliegen:
|
||||||
response.raise_for_status()
|
try:
|
||||||
|
# Read the raw body - use bounded_stream if available, otherwise stream
|
||||||
|
stream = getattr(req, 'bounded_stream', req.stream)
|
||||||
|
raw_body = stream.read().decode('utf-8')
|
||||||
|
# Parse URL-encoded data manually
|
||||||
|
parsed = parse_qs(raw_body)
|
||||||
|
anliegen = parsed.get('anliegen', [''])[0]
|
||||||
|
party_id = parsed.get('party_id', [''])[0]
|
||||||
|
except Exception as e:
|
||||||
|
# Log the exception for debugging
|
||||||
|
print(f"Error parsing form data: {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
data = response.json()
|
# Remove any whitespace and check if actually empty
|
||||||
|
anliegen = anliegen.strip() if anliegen else ''
|
||||||
|
party_id = party_id.strip() if party_id else ''
|
||||||
|
|
||||||
|
if not anliegen:
|
||||||
|
resp.status = falcon.HTTP_400
|
||||||
|
resp.content_type = 'application/json'
|
||||||
|
resp.text = json.dumps({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Anliegen-Feld ist erforderlich'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create prompt for Gemini
|
||||||
|
prompt = """Erzeuge aus dem folgenden Anliegen-Text je nach Anliegen eine Anfrage oder einen Antrag an die Karlsruher Stadtverwaltung im Namen einer Stadtratsfraktion.
|
||||||
|
|
||||||
|
Der Antrag soll im sachlichen, offiziellen Ton einer Fraktion verfasst sein - KEINE persönliche Anrede, KEINE "ich" oder "wir" Formulierungen. Verwende die dritte Person oder Passiv-Formulierungen.
|
||||||
|
|
||||||
|
Struktur:
|
||||||
|
- Die erste Zeile ist der Antragstitel. Der Titel soll PRÄGNANT, EINFACH und EINPRÄGSAM sein - maximal 8-10 Wörter. Vermeide komplizierte Formulierungen, technische Fachbegriffe oder zu lange Titel. Der Titel soll eine gute Außenwirkung haben und das Anliegen klar und verständlich kommunizieren. Beispiele für gute Titel: "Nachtabsenkung der öffentlichen Straßenbeleuchtung", "Vielfalt in Bewegung – Kulturelle Begleitmaßnahmen World Games 2029", "Prüfung digitaler Zahlungsdienstleister und WERO-Alternative"
|
||||||
|
- Der zweite Absatz ist der Forderungsteil ("Der Gemeinderat möge beschließen:"). Hier können nach einem kurzen Satz auch Stichpunkte verwendet werden, wenn dies sinnvoll ist.
|
||||||
|
- Der letzte Teil ist Begründung/Sachverhalt (ohne diesen Titel im Text)
|
||||||
|
|
||||||
|
WICHTIG:
|
||||||
|
- Verwende KEINE Markdown-Formatierung. Keine **fett**, keine *kursiv*, keine /Überschriften, keine # Hashtags, keine Links oder andere Formatierung.
|
||||||
|
- Schreibe nur reinen Text ohne jegliche Markdown-Syntax.
|
||||||
|
- Sachlicher, offizieller Ton einer Fraktion, keine persönlichen Formulierungen.
|
||||||
|
- Der Antragstitel muss prägnant, einfach verständlich und einprägsam sein - keine komplizierten Formulierungen!
|
||||||
|
|
||||||
|
"""
|
||||||
|
prompt += anliegen
|
||||||
|
|
||||||
|
# Call Gemini API
|
||||||
|
response = self.model.generate_content(prompt)
|
||||||
|
generated_text = response.text
|
||||||
|
|
||||||
|
# Parse the response
|
||||||
|
parsed = self._parse_gemini_response(generated_text)
|
||||||
|
|
||||||
|
# Return JSON with the generated text parts
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = 'application/json'
|
||||||
resp.text = json.dumps(data)
|
resp.text = json.dumps({
|
||||||
|
'success': True,
|
||||||
|
'title': parsed['title'],
|
||||||
|
'demand': parsed['demand'],
|
||||||
|
'justification': parsed['justification'],
|
||||||
|
'party_name': party_id if party_id else ""
|
||||||
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
resp.status = falcon.HTTP_500
|
resp.status = falcon.HTTP_500
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = 'application/json'
|
||||||
resp.text = json.dumps({
|
resp.text = json.dumps({
|
||||||
'error': str(e),
|
'success': False,
|
||||||
'results': [],
|
'error': str(e)
|
||||||
'next': None
|
})
|
||||||
|
|
||||||
|
class GeneratePDFResource:
|
||||||
|
def _generate_pdf(self, title, demand, justification, party_name=""):
|
||||||
|
"""Generate a PDF that looks like a city council proposal"""
|
||||||
|
buffer = BytesIO()
|
||||||
|
doc = SimpleDocTemplate(buffer, pagesize=A4,
|
||||||
|
rightMargin=2.5*cm, leftMargin=2.5*cm,
|
||||||
|
topMargin=2.5*cm, bottomMargin=2.5*cm)
|
||||||
|
|
||||||
|
# Container for the 'Flowable' objects
|
||||||
|
story = []
|
||||||
|
|
||||||
|
# Define styles
|
||||||
|
styles = getSampleStyleSheet()
|
||||||
|
|
||||||
|
# Custom styles for the document
|
||||||
|
title_style = ParagraphStyle(
|
||||||
|
'CustomTitle',
|
||||||
|
parent=styles['Heading1'],
|
||||||
|
fontSize=16,
|
||||||
|
textColor='black',
|
||||||
|
spaceAfter=30,
|
||||||
|
alignment=TA_LEFT,
|
||||||
|
fontName='Helvetica-Bold'
|
||||||
|
)
|
||||||
|
|
||||||
|
heading_style = ParagraphStyle(
|
||||||
|
'CustomHeading',
|
||||||
|
parent=styles['Heading2'],
|
||||||
|
fontSize=12,
|
||||||
|
textColor='black',
|
||||||
|
spaceAfter=12,
|
||||||
|
spaceBefore=20,
|
||||||
|
alignment=TA_LEFT,
|
||||||
|
fontName='Helvetica-Bold'
|
||||||
|
)
|
||||||
|
|
||||||
|
body_style = ParagraphStyle(
|
||||||
|
'CustomBody',
|
||||||
|
parent=styles['Normal'],
|
||||||
|
fontSize=11,
|
||||||
|
textColor='black',
|
||||||
|
spaceAfter=12,
|
||||||
|
alignment=TA_JUSTIFY,
|
||||||
|
fontName='Helvetica'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Header with party name if provided
|
||||||
|
if party_name:
|
||||||
|
party_para = Paragraph(f"<b>Antrag der {party_name}</b>", body_style)
|
||||||
|
story.append(party_para)
|
||||||
|
story.append(Spacer(1, 0.5*cm))
|
||||||
|
|
||||||
|
# Title
|
||||||
|
if title:
|
||||||
|
title_para = Paragraph(f"<b>{title}</b>", title_style)
|
||||||
|
story.append(title_para)
|
||||||
|
|
||||||
|
# Demand section
|
||||||
|
if demand:
|
||||||
|
story.append(Spacer(1, 0.3*cm))
|
||||||
|
demand_heading = Paragraph("<b>Der Gemeinderat möge beschließen:</b>", heading_style)
|
||||||
|
story.append(demand_heading)
|
||||||
|
|
||||||
|
# Process demand text - replace newlines with proper breaks
|
||||||
|
demand_lines = demand.split('\n')
|
||||||
|
for line in demand_lines:
|
||||||
|
if line.strip():
|
||||||
|
demand_para = Paragraph(line.strip(), body_style)
|
||||||
|
story.append(demand_para)
|
||||||
|
|
||||||
|
# Justification section
|
||||||
|
if justification:
|
||||||
|
story.append(Spacer(1, 0.5*cm))
|
||||||
|
justification_heading = Paragraph("<b>Begründung/Sachverhalt</b>", heading_style)
|
||||||
|
story.append(justification_heading)
|
||||||
|
|
||||||
|
# Process justification text
|
||||||
|
justification_lines = justification.split('\n')
|
||||||
|
for line in justification_lines:
|
||||||
|
if line.strip():
|
||||||
|
justification_para = Paragraph(line.strip(), body_style)
|
||||||
|
story.append(justification_para)
|
||||||
|
|
||||||
|
# Build PDF
|
||||||
|
doc.build(story)
|
||||||
|
buffer.seek(0)
|
||||||
|
return buffer
|
||||||
|
|
||||||
|
def on_post(self, req, resp):
|
||||||
|
"""Generate PDF from form data"""
|
||||||
|
try:
|
||||||
|
# Get form data
|
||||||
|
title = req.get_param('title', default='') or ''
|
||||||
|
demand = req.get_param('demand', default='') or ''
|
||||||
|
justification = req.get_param('justification', default='') or ''
|
||||||
|
party_name = req.get_param('party_name', default='') or ''
|
||||||
|
|
||||||
|
# If empty, try to read from stream
|
||||||
|
if not title:
|
||||||
|
try:
|
||||||
|
stream = getattr(req, 'bounded_stream', req.stream)
|
||||||
|
raw_body = stream.read().decode('utf-8')
|
||||||
|
parsed = parse_qs(raw_body)
|
||||||
|
title = parsed.get('title', [''])[0]
|
||||||
|
demand = parsed.get('demand', [''])[0]
|
||||||
|
justification = parsed.get('justification', [''])[0]
|
||||||
|
party_name = parsed.get('party_name', [''])[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Generate PDF
|
||||||
|
pdf_buffer = self._generate_pdf(title, demand, justification, party_name)
|
||||||
|
|
||||||
|
# Return PDF
|
||||||
|
resp.content_type = 'application/pdf'
|
||||||
|
resp.set_header('Content-Disposition', 'inline; filename="antrag.pdf"')
|
||||||
|
resp.data = pdf_buffer.read()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
resp.status = falcon.HTTP_500
|
||||||
|
resp.content_type = 'application/json'
|
||||||
|
resp.text = json.dumps({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
class GenerateWordResource:
|
||||||
|
def _generate_word(self, title, demand, justification, party_name=""):
|
||||||
|
"""Generate a Word document that looks like a city council proposal"""
|
||||||
|
doc = Document()
|
||||||
|
|
||||||
|
# Set default font
|
||||||
|
style = doc.styles['Normal']
|
||||||
|
font = style.font
|
||||||
|
font.name = 'Arial'
|
||||||
|
font.size = Pt(11)
|
||||||
|
|
||||||
|
# Header with party name if provided
|
||||||
|
if party_name:
|
||||||
|
party_para = doc.add_paragraph(f"Antrag der {party_name}")
|
||||||
|
party_para.runs[0].bold = True
|
||||||
|
party_para.runs[0].font.size = Pt(11)
|
||||||
|
doc.add_paragraph()
|
||||||
|
|
||||||
|
# Title
|
||||||
|
if title:
|
||||||
|
title_para = doc.add_paragraph(title)
|
||||||
|
title_para.runs[0].bold = True
|
||||||
|
title_para.runs[0].font.size = Pt(16)
|
||||||
|
title_para.paragraph_format.space_after = Pt(30)
|
||||||
|
|
||||||
|
# Demand section
|
||||||
|
if demand:
|
||||||
|
doc.add_paragraph()
|
||||||
|
demand_heading = doc.add_paragraph("Der Gemeinderat möge beschließen:")
|
||||||
|
demand_heading.runs[0].bold = True
|
||||||
|
demand_heading.runs[0].font.size = Pt(12)
|
||||||
|
demand_heading.paragraph_format.space_before = Pt(20)
|
||||||
|
demand_heading.paragraph_format.space_after = Pt(12)
|
||||||
|
|
||||||
|
# Process demand text
|
||||||
|
demand_lines = demand.split('\n')
|
||||||
|
for line in demand_lines:
|
||||||
|
if line.strip():
|
||||||
|
doc.add_paragraph(line.strip())
|
||||||
|
|
||||||
|
# Justification section
|
||||||
|
if justification:
|
||||||
|
doc.add_paragraph()
|
||||||
|
justification_heading = doc.add_paragraph("Begründung/Sachverhalt")
|
||||||
|
justification_heading.runs[0].bold = True
|
||||||
|
justification_heading.runs[0].font.size = Pt(12)
|
||||||
|
justification_heading.paragraph_format.space_before = Pt(20)
|
||||||
|
justification_heading.paragraph_format.space_after = Pt(12)
|
||||||
|
|
||||||
|
# Process justification text
|
||||||
|
justification_lines = justification.split('\n')
|
||||||
|
for line in justification_lines:
|
||||||
|
if line.strip():
|
||||||
|
doc.add_paragraph(line.strip())
|
||||||
|
|
||||||
|
# Save to buffer
|
||||||
|
buffer = BytesIO()
|
||||||
|
doc.save(buffer)
|
||||||
|
buffer.seek(0)
|
||||||
|
return buffer
|
||||||
|
|
||||||
|
def on_post(self, req, resp):
|
||||||
|
"""Generate Word document from form data"""
|
||||||
|
try:
|
||||||
|
if not DOCX_AVAILABLE:
|
||||||
|
resp.status = falcon.HTTP_500
|
||||||
|
resp.content_type = 'application/json'
|
||||||
|
resp.text = json.dumps({
|
||||||
|
'success': False,
|
||||||
|
'error': 'python-docx not installed'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get form data
|
||||||
|
title = req.get_param('title', default='') or ''
|
||||||
|
demand = req.get_param('demand', default='') or ''
|
||||||
|
justification = req.get_param('justification', default='') or ''
|
||||||
|
party_name = req.get_param('party_name', default='') or ''
|
||||||
|
|
||||||
|
# If empty, try to read from stream
|
||||||
|
if not title:
|
||||||
|
try:
|
||||||
|
stream = getattr(req, 'bounded_stream', req.stream)
|
||||||
|
raw_body = stream.read().decode('utf-8')
|
||||||
|
parsed = parse_qs(raw_body)
|
||||||
|
title = parsed.get('title', [''])[0]
|
||||||
|
demand = parsed.get('demand', [''])[0]
|
||||||
|
justification = parsed.get('justification', [''])[0]
|
||||||
|
party_name = parsed.get('party_name', [''])[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Generate Word document
|
||||||
|
word_buffer = self._generate_word(title, demand, justification, party_name)
|
||||||
|
|
||||||
|
# Return Word document
|
||||||
|
resp.content_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||||
|
resp.set_header('Content-Disposition', 'attachment; filename="antrag.docx"')
|
||||||
|
resp.data = word_buffer.read()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
resp.status = falcon.HTTP_500
|
||||||
|
resp.content_type = 'application/json'
|
||||||
|
resp.text = json.dumps({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
class RobotsResource:
|
class RobotsResource:
|
||||||
|
|
@ -224,14 +582,18 @@ if not STATIC_DIR:
|
||||||
meinantrag = MeinAntragApp()
|
meinantrag = MeinAntragApp()
|
||||||
impressum = ImpressumResource()
|
impressum = ImpressumResource()
|
||||||
datenschutz = DatenschutzResource()
|
datenschutz = DatenschutzResource()
|
||||||
publicbodies = PublicBodiesResource()
|
generate_antrag = GenerateAntragResource()
|
||||||
|
generate_pdf = GeneratePDFResource()
|
||||||
|
generate_word = GenerateWordResource()
|
||||||
robots = RobotsResource()
|
robots = RobotsResource()
|
||||||
sitemap = SitemapResource()
|
sitemap = SitemapResource()
|
||||||
|
|
||||||
app.add_route('/', meinantrag)
|
app.add_route('/', meinantrag)
|
||||||
app.add_route('/impressum', impressum)
|
app.add_route('/impressum', impressum)
|
||||||
app.add_route('/datenschutz', datenschutz)
|
app.add_route('/datenschutz', datenschutz)
|
||||||
app.add_route('/api/publicbodies', publicbodies)
|
app.add_route('/api/generate-antrag', generate_antrag)
|
||||||
|
app.add_route('/api/generate-pdf', generate_pdf)
|
||||||
|
app.add_route('/api/generate-word', generate_word)
|
||||||
app.add_route('/robots.txt', robots)
|
app.add_route('/robots.txt', robots)
|
||||||
app.add_route('/sitemap.xml', sitemap)
|
app.add_route('/sitemap.xml', sitemap)
|
||||||
|
|
||||||
|
|
|
||||||
15
module.nix
15
module.nix
|
|
@ -16,6 +16,19 @@ in
|
||||||
|
|
||||||
enable = lib.mkEnableOption "MeinAntrag web app";
|
enable = lib.mkEnableOption "MeinAntrag web app";
|
||||||
|
|
||||||
|
environment = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf lib.types.str;
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
GOOGLE_GEMINI_API_KEY = "your-api-key-here";
|
||||||
|
MEINANTRAG_BASE_URL = "https://example.com";
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Additional environment variables to pass to the MeinAntrag service.
|
||||||
|
For example, set GOOGLE_GEMINI_API_KEY for Gemini API integration.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -51,7 +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.environment);
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
"static-map" = "/static=${pkgs.meinantrag}/share/meinantrag/assets";
|
"static-map" = "/static=${pkgs.meinantrag}/share/meinantrag/assets";
|
||||||
|
|
|
||||||
|
|
@ -11,107 +11,295 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form id="meinantragForm" class="text-start">
|
<form id="meinantragForm" class="text-start">
|
||||||
<div class="mb-4">
|
<div id="inputFields">
|
||||||
<label for="party" class="form-label fw-bold">Fraktion</label>
|
<div class="mb-4">
|
||||||
<select class="form-select" id="party" name="party_id" required>
|
<label for="party" class="form-label fw-bold">Fraktion</label>
|
||||||
<option value="">Fraktion auswählen...</option>
|
<select class="form-select" id="party" name="party_id" required>
|
||||||
</select>
|
<option value="">Fraktion auswählen...</option>
|
||||||
|
<option value="SPD">SPD</option>
|
||||||
|
<option value="GRÜNEN">GRÜNEN</option>
|
||||||
|
<option value="CDU">CDU</option>
|
||||||
|
<option value="FDP/FW">FDP/FW</option>
|
||||||
|
<option value="Volt">Volt</option>
|
||||||
|
<option value="DIE LINKE">DIE LINKE</option>
|
||||||
|
<option value="KAL">KAL</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="anliegen" class="form-label fw-bold">Mein Anliegen:</label>
|
||||||
|
<textarea class="form-control" id="anliegen" name="anliegen" rows="5"
|
||||||
|
placeholder="Beschreibe hier, welche Anfrage oder Antrag du an die Stadtverwaltung stellen möchtest ..." required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
|
<span class="btn-text">Antrag generieren</span>
|
||||||
|
<span class="loading spinner-border spinner-border-sm ms-2" role="status"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div id="resultFields" class="text-start" style="display: none;">
|
||||||
<label for="body" class="form-label fw-bold">Mein Anliegen:</label>
|
<div class="mb-3">
|
||||||
<textarea class="form-control" id="body" name="body" rows="5"
|
<a href="#" id="backLink" class="text-decoration-none">
|
||||||
placeholder="Beschreibe hier, welche Anfrage oder Antrag du an die Stadtverwaltung stellen möchtest ..." required></textarea>
|
<span>← Zurück</span>
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
<div class="text-center">
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg">
|
<div class="mb-4">
|
||||||
<span class="btn-text">Antrag generieren</span>
|
<label for="antragstitel" class="form-label fw-bold">Antragstitel</label>
|
||||||
<span class="loading spinner-border spinner-border-sm ms-2" role="status"></span>
|
<input type="text" class="form-control" id="antragstitel" name="antragstitel">
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="result" class="mt-4" style="display: none;">
|
|
||||||
<h5 class="text-success mb-3">Link erfolgreich generiert!</h5>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" id="generatedLinkInput" readonly>
|
|
||||||
<button class="btn btn-outline-primary" type="button" id="copyBtn">In Zwischenablage</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Initialize Select2 for public bodies
|
|
||||||
$('#publicbody').select2({
|
|
||||||
theme: 'bootstrap-5',
|
|
||||||
placeholder: 'Behörde auswählen...',
|
|
||||||
allowClear: true,
|
|
||||||
width: '100%',
|
|
||||||
ajax: {
|
|
||||||
url: '/api/publicbodies',
|
|
||||||
dataType: 'json',
|
|
||||||
delay: 250,
|
|
||||||
data: function(params) {
|
|
||||||
return {
|
|
||||||
search: params.term,
|
|
||||||
page: params.page || 1
|
|
||||||
};
|
|
||||||
},
|
|
||||||
processResults: function(data, params) {
|
|
||||||
params.page = params.page || 1;
|
|
||||||
return {
|
|
||||||
results: data.objects.map(function(item) {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
text: item.name + ' (' + item.jurisdiction.name + ')'
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
pagination: {
|
|
||||||
more: data.meta.next !== null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
cache: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission (client-side)
|
// Handle form submission (client-side)
|
||||||
$('#meinantragForm').on('submit', function(e) {
|
$('#meinantragForm').on('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const publicbodyId = $('#publicbody').val() || '';
|
const partyId = $('#party').val() || '';
|
||||||
const subject = $('#subject').val() || '';
|
const anliegen = $('#anliegen').val() || '';
|
||||||
const body = $('#body').val() || '';
|
|
||||||
|
|
||||||
let link = 'https://fragdenstaat.de/anfrage-stellen/';
|
if (!anliegen.trim()) {
|
||||||
if (publicbodyId) {
|
alert('Bitte geben Sie ein Anliegen ein.');
|
||||||
link += `an/${publicbodyId}/`;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
// Show loading state
|
||||||
if (subject) params.set('subject', subject);
|
const $button = $(this).find('button[type="submit"]');
|
||||||
if (body) params.set('body', body);
|
const $btnText = $button.find('.btn-text');
|
||||||
|
const $loading = $button.find('.loading');
|
||||||
|
$button.prop('disabled', true);
|
||||||
|
$btnText.text('Generiere Antrag...');
|
||||||
|
$loading.css('display', 'inline-block');
|
||||||
|
|
||||||
if ([...params].length > 0) {
|
// Prepare form data as URL-encoded
|
||||||
link += `?${params.toString()}`;
|
const formData = new URLSearchParams();
|
||||||
|
formData.append('anliegen', anliegen);
|
||||||
|
if (partyId) {
|
||||||
|
formData.append('party_id', partyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#generatedLinkInput').val(link);
|
// Send request to generate text
|
||||||
$('#result').show();
|
fetch('/api/generate-antrag', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: formData.toString()
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.json().then(err => {
|
||||||
|
throw new Error(err.error || 'Fehler beim Generieren des Antrags');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Fill in the result fields
|
||||||
|
$('#antragstitel').val(data.title || '');
|
||||||
|
$('#forderung').val(data.demand || '');
|
||||||
|
$('#begruendung').val(data.justification || '');
|
||||||
|
|
||||||
|
// Store party name for mail button
|
||||||
|
$('#resultFields').data('party-name', data.party_name || '');
|
||||||
|
|
||||||
|
// Update mail button text
|
||||||
|
if (data.party_name) {
|
||||||
|
$('#mailBtnText').text('Mail an ' + data.party_name + ' senden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide input fields and show result fields
|
||||||
|
$('#inputFields').hide();
|
||||||
|
$('#resultFields').show();
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || 'Fehler beim Generieren des Antrags');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button state
|
||||||
|
$button.prop('disabled', false);
|
||||||
|
$btnText.text('Antrag generieren');
|
||||||
|
$loading.hide();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Fehler beim Generieren des Antrags: ' + error.message);
|
||||||
|
|
||||||
|
// Reset button state
|
||||||
|
$button.prop('disabled', false);
|
||||||
|
$btnText.text('Antrag generieren');
|
||||||
|
$loading.hide();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy to clipboard
|
// Handle back link click
|
||||||
$(document).on('click', '#copyBtn', function() {
|
$('#backLink').on('click', function(e) {
|
||||||
const link = $('#generatedLinkInput').val();
|
e.preventDefault();
|
||||||
navigator.clipboard.writeText(link).then(function() {
|
$('#resultFields').hide();
|
||||||
const btn = $('#copyBtn');
|
$('#inputFields').show();
|
||||||
const originalText = btn.text();
|
});
|
||||||
btn.text('Kopiert!');
|
|
||||||
setTimeout(() => btn.text(originalText), 1500);
|
// Mail-Kontakte für Fraktionen
|
||||||
|
const partyContacts = {
|
||||||
|
'SPD': 'spd@karlsruhe.de',
|
||||||
|
'GRÜNEN': 'gruene@karlsruhe.de',
|
||||||
|
'CDU': 'cdu@karlsruhe.de',
|
||||||
|
'FDP/FW': 'fdp@karlsruhe.de',
|
||||||
|
'Volt': 'volt@karlsruhe.de',
|
||||||
|
'DIE LINKE': 'dielinke@karlsruhe.de',
|
||||||
|
'KAL': 'kal@karlsruhe.de'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle mail button click
|
||||||
|
$('#mailBtn').on('click', function() {
|
||||||
|
const partyName = $('#resultFields').data('party-name') || '';
|
||||||
|
const email = partyContacts[partyName] || '';
|
||||||
|
const subject = encodeURIComponent($('#antragstitel').val() || '');
|
||||||
|
const title = $('#antragstitel').val() || '';
|
||||||
|
const demand = $('#forderung').val() || '';
|
||||||
|
const justification = $('#begruendung').val() || '';
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (email) {
|
||||||
|
window.location.href = `mailto:${email}?subject=${subject}&body=${bodyEncoded}`;
|
||||||
|
} else {
|
||||||
|
alert('Keine E-Mail-Adresse für diese Fraktion hinterlegt.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle PDF button click
|
||||||
|
$('#pdfBtn').on('click', function() {
|
||||||
|
const title = $('#antragstitel').val() || '';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open PDF in new window
|
||||||
|
fetch('/api/generate-pdf', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: formData.toString()
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Generieren des PDFs');
|
||||||
|
}
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Fehler beim Generieren des PDFs: ' + error.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Word button click
|
||||||
|
$('#wordBtn').on('click', function() {
|
||||||
|
const title = $('#antragstitel').val() || '';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download Word file
|
||||||
|
fetch('/api/generate-word', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: formData.toString()
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Generieren der Word-Datei');
|
||||||
|
}
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'antrag.docx';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Fehler beim Generieren der Word-Datei: ' + error.message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue