added debugging, switch to json
This commit is contained in:
parent
fcffccb718
commit
56f1f4e8fb
3 changed files with 509 additions and 523 deletions
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766473571,
|
||||
"narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=",
|
||||
"lastModified": 1772047000,
|
||||
"narHash": "sha256-7DaQVv4R97cii/Qdfy4tmDZMB2xxtyIvNGSwXBBhSmo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "76701a179d3a98b07653e2b0409847499b2a07d3",
|
||||
"rev": "1267bb4920d0fc06ea916734c11b0bf004bbe17e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
overlay = final: prev: {
|
||||
meinantrag = with final; python3Packages.buildPythonApplication rec {
|
||||
pname = "meinantrag";
|
||||
version = "0.0.2";
|
||||
version = "0.0.3";
|
||||
format = "other";
|
||||
|
||||
src = self;
|
||||
|
|
|
|||
424
meinantrag.py
424
meinantrag.py
|
|
@ -14,15 +14,27 @@ import google.generativeai as genai
|
|||
import re
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
|
||||
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')
|
||||
# Setup logging
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
level=os.environ.get('LOG_LEVEL', 'INFO').upper(),
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SITE_BASE_URL = os.environ.get("MEINANTRAG_BASE_URL", "http://localhost:8000")
|
||||
|
||||
|
||||
class BaseTemplateResource:
|
||||
"""Base class for resources that need template rendering"""
|
||||
|
|
@ -30,7 +42,7 @@ class BaseTemplateResource:
|
|||
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('MEINANTRAG_TEMPLATES_DIR')
|
||||
env_dir = os.environ.get("MEINANTRAG_TEMPLATES_DIR")
|
||||
if env_dir and os.path.exists(env_dir):
|
||||
return env_dir
|
||||
|
||||
|
|
@ -38,36 +50,43 @@ class BaseTemplateResource:
|
|||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Try development templates first
|
||||
dev_template_dir = os.path.join(script_dir, 'templates')
|
||||
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/meinantrag
|
||||
if '/nix/store/' in script_dir:
|
||||
if "/nix/store/" in script_dir:
|
||||
# Go up from bin to share/meinantrag/templates
|
||||
share_dir = os.path.join(script_dir, '..', 'share', 'meinantrag', 'templates')
|
||||
share_dir = os.path.join(
|
||||
script_dir, "..", "share", "meinantrag", "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_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', 'meinantrag', 'templates')
|
||||
alt_share_dir = os.path.join(
|
||||
store_path, "share", "meinantrag", "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')
|
||||
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 MeinAntragApp(BaseTemplateResource):
|
||||
def __init__(self):
|
||||
# Setup Jinja2 template environment
|
||||
|
|
@ -77,14 +96,15 @@ class MeinAntragApp(BaseTemplateResource):
|
|||
|
||||
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'
|
||||
template = self.jinja_env.get_template("index.html")
|
||||
resp.content_type = "text/html; charset=utf-8"
|
||||
resp.text = template.render(
|
||||
meta_title='MeinAntrag – Anträge an die Karlsruher Stadtverwaltung',
|
||||
meta_description='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!',
|
||||
canonical_url=f"{SITE_BASE_URL}/"
|
||||
meta_title="MeinAntrag – Anträge an die Karlsruher Stadtverwaltung",
|
||||
meta_description="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!",
|
||||
canonical_url=f"{SITE_BASE_URL}/",
|
||||
)
|
||||
|
||||
|
||||
class ImpressumResource(BaseTemplateResource):
|
||||
def __init__(self):
|
||||
template_dir = self._get_template_dir()
|
||||
|
|
@ -92,15 +112,16 @@ class ImpressumResource(BaseTemplateResource):
|
|||
|
||||
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'
|
||||
template = self.jinja_env.get_template("impressum.html")
|
||||
resp.content_type = "text/html; charset=utf-8"
|
||||
resp.text = template.render(
|
||||
meta_title='Impressum – MeinAntrag',
|
||||
meta_description='Impressum für MeinAntrag.',
|
||||
meta_title="Impressum – MeinAntrag",
|
||||
meta_description="Impressum für MeinAntrag.",
|
||||
canonical_url=f"{SITE_BASE_URL}/impressum",
|
||||
noindex=True
|
||||
noindex=True,
|
||||
)
|
||||
|
||||
|
||||
class DatenschutzResource(BaseTemplateResource):
|
||||
def __init__(self):
|
||||
template_dir = self._get_template_dir()
|
||||
|
|
@ -108,22 +129,23 @@ class DatenschutzResource(BaseTemplateResource):
|
|||
|
||||
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'
|
||||
template = self.jinja_env.get_template("datenschutz.html")
|
||||
resp.content_type = "text/html; charset=utf-8"
|
||||
resp.text = template.render(
|
||||
meta_title='Datenschutz – MeinAntrag',
|
||||
meta_description='Datenschutzerklärung für MeinAntrag. Keine Cookies, es werden nur Anfragen an die FragDenStaat-API gestellt.',
|
||||
meta_title="Datenschutz – MeinAntrag",
|
||||
meta_description="Datenschutzerklärung für MeinAntrag. Keine Cookies, es werden nur Anfragen an die FragDenStaat-API gestellt.",
|
||||
canonical_url=f"{SITE_BASE_URL}/datenschutz",
|
||||
noindex=True
|
||||
noindex=True,
|
||||
)
|
||||
|
||||
|
||||
class GenerateAntragResource:
|
||||
def __init__(self):
|
||||
# Initialize Gemini API
|
||||
api_key = os.environ.get('GOOGLE_GEMINI_API_KEY')
|
||||
api_key = os.environ.get("GOOGLE_GEMINI_API_KEY")
|
||||
if api_key:
|
||||
genai.configure(api_key=api_key)
|
||||
self.model = genai.GenerativeModel('gemini-3-pro-preview')
|
||||
self.model = genai.GenerativeModel("gemini-2.5-pro")
|
||||
else:
|
||||
self.model = None
|
||||
|
||||
|
|
@ -133,122 +155,65 @@ class GenerateAntragResource:
|
|||
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)
|
||||
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)
|
||||
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
|
||||
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:
|
||||
if not self.model:
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.content_type = 'application/json'
|
||||
resp.text = json.dumps({
|
||||
'success': False,
|
||||
'error': 'Gemini API key not configured'
|
||||
})
|
||||
resp.content_type = "application/json"
|
||||
resp.text = json.dumps(
|
||||
{"success": False, "error": "Gemini API key not configured"}
|
||||
)
|
||||
return
|
||||
|
||||
# Get form data - try multiple methods for Falcon compatibility
|
||||
anliegen = ''
|
||||
party_id = ''
|
||||
anliegen = ""
|
||||
party_id = ""
|
||||
|
||||
# Method 1: Try get_param (works for URL-encoded form data)
|
||||
anliegen = req.get_param('anliegen', default='') or ''
|
||||
party_id = req.get_param('party_id', default='') or ''
|
||||
anliegen = req.get_param("anliegen", default="") or ""
|
||||
party_id = req.get_param("party_id", default="") or ""
|
||||
|
||||
# Method 2: If empty, try to read from stream and parse manually
|
||||
if not anliegen:
|
||||
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')
|
||||
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]
|
||||
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
|
||||
|
||||
# Remove any whitespace and check if actually empty
|
||||
anliegen = anliegen.strip() if anliegen else ''
|
||||
party_id = party_id.strip() if party_id else ''
|
||||
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'
|
||||
})
|
||||
resp.content_type = "application/json"
|
||||
resp.text = json.dumps(
|
||||
{"success": False, "error": "Anliegen-Feld ist erforderlich"}
|
||||
)
|
||||
return
|
||||
|
||||
# Create prompt for Gemini
|
||||
|
|
@ -256,74 +221,91 @@ class GenerateAntragResource:
|
|||
|
||||
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. Je nachdem: Entweder Sätze und oder Liste von Forderungen.
|
||||
- Der letzte Teil ist Begründung/Sachverhalt (ohne diesen Titel im Text)
|
||||
Gib das Ergebnis ALS GÜLTIGES JSON mit den folgenden Keys zurück:
|
||||
{
|
||||
"antragstitel": "PRÄGNANTER Titel (max. 8-10 Wörter)",
|
||||
"forderung": "Forderungstext oder Liste von Forderungen",
|
||||
"begruendung": "Begründung/Sachverhalt",
|
||||
"mail_recipient": "Empfänger-E-Mail (z.B. 'fraktion@example.com')",
|
||||
"mail_subject": "Betreff für die E-Mail",
|
||||
"mail_body": "Höflicher E-Mail-Text in der ersten Person",
|
||||
"filename": "Dateiname (z.B. 'antragsentwurf_...docx')"
|
||||
}
|
||||
|
||||
WICHTIG:
|
||||
- Reinen Text, verwende KEINE Markdown-Formatierung oder sonstige Formatierungen, ausgenommen Listen und Aufzählungen.
|
||||
- Sachlicher, offizieller Ton einer Fraktion, keine persönlichen Formulierungen.
|
||||
- ANTWORTE NUR MIT GÜLTIGEM JSON, KEINERLEI ERKLÄRUNGEN ODER ZUSÄTZLICHEN TEXT!
|
||||
- Der Titel soll PRÄGNANT, EINFACH und EINPRÄGSAM sein - maximal 8-10 Wörter.
|
||||
- Der Dateiname soll knapp sein, z.B. 'antragsentwurf_...docx'.
|
||||
- Keine Markdown-Formatierung im Text.
|
||||
- Sachlicher, offizieller Ton einer Fraktion, keine persönlichen Formulierungen im Antrag.
|
||||
- Der E-Mail-Text soll persönlich sein (ich-Form).
|
||||
|
||||
"""
|
||||
Anliegen: """
|
||||
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)
|
||||
# Log the raw response for debugging
|
||||
logger.debug(f"Gemini raw response: {generated_text}")
|
||||
|
||||
# Generate email text
|
||||
email_prompt = f"""Erstelle einen kurzen, höflichen E-Mail-Text in der ERSTEN PERSON (persönlich, ich-rede) an eine Fraktion.
|
||||
Die E-Mail soll:
|
||||
- Mit "Guten Tag," beginnen
|
||||
- Das Anliegen kurz erklären und prägnant
|
||||
- Erwähnen, dass eine Antragsvorlage im Anhang beigefügt ist
|
||||
- Mit "Mit freundlichen Grüßen," enden
|
||||
- Verwende KEINE Markdown-Formatierung
|
||||
- Schreibe keinen Betreff-Entwurf dazu
|
||||
# Extract JSON from markdown code block if present
|
||||
if generated_text.startswith('```json'):
|
||||
generated_text = generated_text[7:].strip() # Remove ```json
|
||||
if generated_text.endswith('```'):
|
||||
generated_text = generated_text[:-3].strip() # Remove trailing ```
|
||||
elif generated_text.startswith('```'):
|
||||
generated_text = generated_text[3:].strip() # Remove leading ```
|
||||
if generated_text.endswith('```'):
|
||||
generated_text = generated_text[:-3].strip() # Remove trailing ```
|
||||
|
||||
Anliegen: {anliegen}
|
||||
"""
|
||||
|
||||
email_response = self.model.generate_content(email_prompt)
|
||||
email_text = self._remove_markdown(email_response.text)
|
||||
# Parse the JSON response
|
||||
gemini_data = json.loads(generated_text)
|
||||
logger.debug(f"Parsed Gemini data: {gemini_data}")
|
||||
parsed = {
|
||||
"title": gemini_data.get("antragstitel", ""),
|
||||
"demand": gemini_data.get("forderung", ""),
|
||||
"justification": gemini_data.get("begruendung", ""),
|
||||
"mail_recipient": gemini_data.get("mail_recipient", ""),
|
||||
"mail_subject": gemini_data.get("mail_subject", ""),
|
||||
"mail_body": gemini_data.get("mail_body", ""),
|
||||
"filename": gemini_data.get("filename", ""),
|
||||
}
|
||||
email_text = parsed["mail_body"]
|
||||
|
||||
# Ensure proper format - clean up and ensure structure
|
||||
email_text = email_text.strip()
|
||||
|
||||
# Return JSON with the generated text parts
|
||||
resp.content_type = 'application/json'
|
||||
resp.text = json.dumps({
|
||||
'success': True,
|
||||
'title': parsed['title'],
|
||||
'demand': parsed['demand'],
|
||||
'justification': parsed['justification'],
|
||||
'email_body': email_text,
|
||||
'party_name': party_id if party_id else ""
|
||||
})
|
||||
resp.content_type = "application/json"
|
||||
resp.text = json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"title": parsed["title"],
|
||||
"demand": parsed["demand"],
|
||||
"justification": parsed["justification"],
|
||||
"email_body": email_text,
|
||||
"party_name": party_id if party_id else "",
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
logger.error(f"Error generating antrag: {str(e)}", exc_info=True)
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.content_type = 'application/json'
|
||||
resp.text = json.dumps({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
resp.content_type = "application/json"
|
||||
resp.text = json.dumps({"success": False, "error": str(e)})
|
||||
|
||||
|
||||
class GenerateWordResource:
|
||||
def __init__(self):
|
||||
# Get template path
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
self.template_path = os.path.join(script_dir, 'assets', 'antrag_vorlage.docx')
|
||||
self.template_path = os.path.join(script_dir, "assets", "antrag_vorlage.docx")
|
||||
# Fallback if not in assets
|
||||
if not os.path.exists(self.template_path):
|
||||
assets_dir = os.path.join(script_dir, '..', 'assets')
|
||||
self.template_path = os.path.join(assets_dir, 'antrag_vorlage.docx')
|
||||
assets_dir = os.path.join(script_dir, "..", "assets")
|
||||
self.template_path = os.path.join(assets_dir, "antrag_vorlage.docx")
|
||||
|
||||
def _generate_word(self, title, demand, justification, party_name=""):
|
||||
"""Generate a Word document using the template"""
|
||||
|
|
@ -347,42 +329,42 @@ class GenerateWordResource:
|
|||
continue
|
||||
|
||||
# Replace FRAKTION
|
||||
if 'FRAKTION' in full_text:
|
||||
if "FRAKTION" in full_text:
|
||||
for run in paragraph.runs:
|
||||
if 'FRAKTION' in run.text:
|
||||
run.text = run.text.replace('FRAKTION', party_name)
|
||||
if "FRAKTION" in run.text:
|
||||
run.text = run.text.replace("FRAKTION", party_name)
|
||||
|
||||
# Replace XX.XX.XXXX with current date
|
||||
if 'XX.XX.XXXX' in full_text:
|
||||
if "XX.XX.XXXX" in full_text:
|
||||
for run in paragraph.runs:
|
||||
if 'XX.XX.XXXX' in run.text:
|
||||
run.text = run.text.replace('XX.XX.XXXX', current_date)
|
||||
if "XX.XX.XXXX" in run.text:
|
||||
run.text = run.text.replace("XX.XX.XXXX", current_date)
|
||||
|
||||
# Replace ANTRAGSTITEL (bold)
|
||||
if 'ANTRAGSTITEL' in full_text:
|
||||
if "ANTRAGSTITEL" in full_text:
|
||||
paragraph.clear()
|
||||
run = paragraph.add_run(title)
|
||||
run.bold = True
|
||||
|
||||
# Replace ANTRAGSTEXT
|
||||
if 'ANTRAGSTEXT' in full_text:
|
||||
if "ANTRAGSTEXT" in full_text:
|
||||
paragraph.clear()
|
||||
lines = antragtext.split('\n')
|
||||
lines = antragtext.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip():
|
||||
paragraph.add_run(line.strip())
|
||||
if i < len(lines) - 1:
|
||||
paragraph.add_run('\n')
|
||||
paragraph.add_run("\n")
|
||||
|
||||
# Replace BEGRÜNDUNGSTEXT
|
||||
if 'BEGRÜNDUNGSTEXT' in full_text:
|
||||
if "BEGRÜNDUNGSTEXT" in full_text:
|
||||
paragraph.clear()
|
||||
lines = justification.split('\n')
|
||||
lines = justification.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip():
|
||||
paragraph.add_run(line.strip())
|
||||
if i < len(lines) - 1:
|
||||
paragraph.add_run('\n')
|
||||
paragraph.add_run("\n")
|
||||
|
||||
# Check text boxes (shapes) for placeholders
|
||||
# Text boxes are stored in the document's part relationships
|
||||
|
|
@ -405,15 +387,17 @@ class GenerateWordResource:
|
|||
|
||||
# Search in main document body
|
||||
if party_name:
|
||||
replace_in_element(document_part.element, 'FRAKTION', party_name)
|
||||
replace_in_element(document_part.element, "FRAKTION", party_name)
|
||||
|
||||
# Also search in header and footer parts
|
||||
for rel in document_part.rels.values():
|
||||
if 'header' in rel.target_ref or 'footer' in rel.target_ref:
|
||||
if "header" in rel.target_ref or "footer" in rel.target_ref:
|
||||
try:
|
||||
header_footer_part = rel.target_part
|
||||
if party_name:
|
||||
replace_in_element(header_footer_part.element, 'FRAKTION', party_name)
|
||||
replace_in_element(
|
||||
header_footer_part.element, "FRAKTION", party_name
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
|
|
@ -430,38 +414,40 @@ class GenerateWordResource:
|
|||
if not full_text:
|
||||
continue
|
||||
|
||||
if party_name and 'FRAKTION' in full_text:
|
||||
if party_name and "FRAKTION" in full_text:
|
||||
for run in paragraph.runs:
|
||||
if 'FRAKTION' in run.text:
|
||||
run.text = run.text.replace('FRAKTION', party_name)
|
||||
if "FRAKTION" in run.text:
|
||||
run.text = run.text.replace("FRAKTION", party_name)
|
||||
|
||||
if 'XX.XX.XXXX' in full_text:
|
||||
if "XX.XX.XXXX" in full_text:
|
||||
for run in paragraph.runs:
|
||||
if 'XX.XX.XXXX' in run.text:
|
||||
run.text = run.text.replace('XX.XX.XXXX', current_date)
|
||||
if "XX.XX.XXXX" in run.text:
|
||||
run.text = run.text.replace(
|
||||
"XX.XX.XXXX", current_date
|
||||
)
|
||||
|
||||
if 'ANTRAGSTITEL' in full_text:
|
||||
if "ANTRAGSTITEL" in full_text:
|
||||
paragraph.clear()
|
||||
run = paragraph.add_run(title)
|
||||
run.bold = True
|
||||
|
||||
if 'ANTRAGSTEXT' in full_text:
|
||||
if "ANTRAGSTEXT" in full_text:
|
||||
paragraph.clear()
|
||||
lines = antragtext.split('\n')
|
||||
lines = antragtext.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip():
|
||||
paragraph.add_run(line.strip())
|
||||
if i < len(lines) - 1:
|
||||
paragraph.add_run('\n')
|
||||
paragraph.add_run("\n")
|
||||
|
||||
if 'BEGRÜNDUNGSTEXT' in full_text:
|
||||
if "BEGRÜNDUNGSTEXT" in full_text:
|
||||
paragraph.clear()
|
||||
lines = justification.split('\n')
|
||||
lines = justification.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip():
|
||||
paragraph.add_run(line.strip())
|
||||
if i < len(lines) - 1:
|
||||
paragraph.add_run('\n')
|
||||
paragraph.add_run("\n")
|
||||
|
||||
# Save to buffer
|
||||
buffer = BytesIO()
|
||||
|
|
@ -474,29 +460,28 @@ class GenerateWordResource:
|
|||
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'
|
||||
})
|
||||
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 ''
|
||||
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')
|
||||
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]
|
||||
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
|
||||
|
||||
|
|
@ -504,31 +489,31 @@ class GenerateWordResource:
|
|||
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.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)
|
||||
})
|
||||
resp.content_type = "application/json"
|
||||
resp.text = json.dumps({"success": False, "error": str(e)})
|
||||
|
||||
|
||||
class RobotsResource:
|
||||
def on_get(self, req, resp):
|
||||
resp.content_type = 'text/plain; charset=utf-8'
|
||||
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.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>
|
||||
|
|
@ -537,25 +522,26 @@ class SitemapResource:
|
|||
</urlset>
|
||||
"""
|
||||
|
||||
|
||||
# Create Falcon application
|
||||
app = falcon.App()
|
||||
|
||||
# Discover static assets directory
|
||||
STATIC_DIR = os.environ.get('MEINANTRAG_STATIC_DIR')
|
||||
STATIC_DIR = os.environ.get("MEINANTRAG_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')
|
||||
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')
|
||||
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', 'meinantrag', 'assets')
|
||||
STATIC_DIR = os.path.join(script_dir, "..", "share", "meinantrag", "assets")
|
||||
|
||||
# Add routes
|
||||
meinantrag = MeinAntragApp()
|
||||
|
|
@ -566,24 +552,24 @@ generate_word = GenerateWordResource()
|
|||
robots = RobotsResource()
|
||||
sitemap = SitemapResource()
|
||||
|
||||
app.add_route('/', meinantrag)
|
||||
app.add_route('/impressum', impressum)
|
||||
app.add_route('/datenschutz', datenschutz)
|
||||
app.add_route('/api/generate-antrag', generate_antrag)
|
||||
app.add_route('/api/generate-word', generate_word)
|
||||
app.add_route('/robots.txt', robots)
|
||||
app.add_route('/sitemap.xml', sitemap)
|
||||
app.add_route("/", meinantrag)
|
||||
app.add_route("/impressum", impressum)
|
||||
app.add_route("/datenschutz", datenschutz)
|
||||
app.add_route("/api/generate-antrag", generate_antrag)
|
||||
app.add_route("/api/generate-word", generate_word)
|
||||
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)
|
||||
app.add_static_route("/static", STATIC_DIR)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
import wsgiref.simple_server
|
||||
|
||||
print("Starting MeinAntrag 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 = wsgiref.simple_server.make_server("localhost", 8000, app)
|
||||
httpd.serve_forever()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue