Compare commits

...

4 Commits

6 changed files with 181 additions and 42 deletions

9
.gitignore vendored
View File

@@ -16,4 +16,11 @@ __pycache__/
.DS_Store .DS_Store
# vCards # vCards
vcards vcards/
# Logs
logs/
#
anmeldung.sqbpro
docs/CHAT_SUMMARY.md

62
app.py
View File

@@ -2,8 +2,9 @@ from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
import os import os
import re import re
import pathlib from utils import generate_vcard
import unicodedata import logging
from logging.handlers import RotatingFileHandler
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, 'anmeldung.db') DB_PATH = os.path.join(BASE_DIR, 'anmeldung.db')
@@ -14,6 +15,18 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app) db = SQLAlchemy(app)
# --- logging setup -------------------------------------------------
LOG_DIR = os.path.join(BASE_DIR, 'logs')
os.makedirs(LOG_DIR, exist_ok=True)
log_file = os.path.join(LOG_DIR, 'app.log')
handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=3)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s')
handler.setFormatter(formatter)
handler.setLevel(logging.INFO)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
# --------------------------------------------------------------------
class Adresse(db.Model): class Adresse(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -98,45 +111,12 @@ def index():
# vCard 4.0 erzeugen und speichern # vCard 4.0 erzeugen und speichern
try: try:
vcards_dir = os.path.join(BASE_DIR, 'vcards') # determine base dir: prefer app.config, then app attribute, then module BASE_DIR
os.makedirs(vcards_dir, exist_ok=True) base_dir = app.config.get('BASE_DIR') if app.config.get('BASE_DIR') else getattr(app, 'BASE_DIR', BASE_DIR)
generate_vcard(adresse, base_dir)
# sanitize filename: remove diacritics and unsafe chars except Exception as e:
def slug(s): # Log the exception with stack trace but don't abort the request
s = unicodedata.normalize('NFKD', s) app.logger.exception('Fehler beim Erzeugen der vCard for adresse id=%s: %s', adresse.id if hasattr(adresse, 'id') else 'unknown', e)
s = ''.join(c for c in s if not unicodedata.combining(c))
s = ''.join(c for c in s if c.isalnum() or c in (' ', '_', '-'))
return s.replace(' ', '_')
filename = f"{slug(adresse.nachname)}_{slug(adresse.vorname)}_{adresse.id}.vcf"
filepath = os.path.join(vcards_dir, filename)
# build vCard 4.0 content
lines = [
'BEGIN:VCARD',
'VERSION:4.0',
f'N:{adresse.nachname};{adresse.vorname};;;',
f'FN:{adresse.vorname} {adresse.nachname}',
]
# ADR: PO Box;Extended;Street;Locality;Region;PostalCode;Country
street = adresse.strasse or ''
if adresse.hausnummer:
street = f"{street} {adresse.hausnummer}".strip()
adr = f'ADR:;;{street};{adresse.ort};;{adresse.plz};{adresse.land}'
lines.append(adr)
if adresse.email:
lines.append(f'EMAIL;TYPE=internet:{adresse.email}')
phone = ''
if adresse.telefon_vorwahl or adresse.telefon_nummer:
phone = f"+{adresse.telefon_vorwahl}{adresse.telefon_nummer}".replace('++', '+')
lines.append(f'TEL;TYPE=voice:{phone}')
lines.append('END:VCARD')
with open(filepath, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
except Exception:
# nicht kritisch: bei Fehlern nicht die ganze Anfrage abbrechen
pass
# Nach erfolgreichem Speichern weiterleiten # Nach erfolgreichem Speichern weiterleiten
return redirect(url_for('danke', id=adresse.id)) return redirect(url_for('danke', id=adresse.id))

1
requirements-dev.txt Normal file
View File

@@ -0,0 +1 @@
pytest

29
tests/test_utils_vcard.py Normal file
View File

@@ -0,0 +1,29 @@
from types import SimpleNamespace
from pathlib import Path
from utils import generate_vcard
def test_generate_vcard_writes_file(tmp_path):
addr = SimpleNamespace(
vorname='Anna',
nachname='Muster',
strasse='Beispielweg',
hausnummer='5a',
plz='54321',
ort='Beispielstadt',
land='Deutschland',
email='anna@example.com',
telefon_vorwahl='49',
telefon_nummer='7654321',
id=42,
)
path = generate_vcard(addr, str(tmp_path))
assert Path(path).exists()
content = Path(path).read_text(encoding='utf-8')
assert 'BEGIN:VCARD' in content
assert 'FN:Anna Muster' in content
assert 'EMAIL;TYPE=internet:anna@example.com' in content
assert 'TEL;TYPE=voice:+497654321' in content

View File

@@ -0,0 +1,65 @@
import pytest
from pathlib import Path
from app import app, db, Frage
@pytest.fixture
def client(tmp_path):
# Use a temporary directory for vcards and a temporary sqlite db
# temp DB file
db_file = tmp_path / "test.db"
# Configure app for testing
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_file}'
# Use temp vcards dir by setting app.BASE_DIR
app.BASE_DIR = str(tmp_path)
# ensure clean DB and create tables
with app.app_context():
db.drop_all()
db.create_all()
# create a sample question so POST processing loops over it
q = Frage(text='Testfrage?')
db.session.add(q)
db.session.commit()
# record question id for use in test POST data
app.config['TEST_QUESTION_ID'] = q.id
with app.test_client() as test_client:
yield test_client
def test_vcard_created_after_submit(client):
data = {
'vorname': 'Max',
'nachname': 'Mustermann',
'strasse': 'Musterstraße',
'hausnummer': '1',
'plz': '12345',
'ort': 'Musterstadt',
'land': 'Deutschland',
'telefon_vorwahl': '49',
'telefon_nummer': '1234567',
'email': 'max@example.com',
# question id set in fixture
f'frage_{app.config.get("TEST_QUESTION_ID")}': 'Antwort'
}
# Submit the form
res = client.post('/', data=data, follow_redirects=True)
assert res.status_code == 200
# find vcards dir under tmp_path (app.BASE_DIR)
vcards_dir = Path(app.BASE_DIR) / 'vcards'
files = list(vcards_dir.glob('*.vcf'))
assert len(files) == 1
content = files[0].read_text(encoding='utf-8')
assert 'BEGIN:VCARD' in content
assert 'VERSION:4.0' in content
assert 'FN:Max Mustermann' in content
assert 'EMAIL;TYPE=internet:max@example.com' in content
assert 'ADR:' in content

57
utils.py Normal file
View File

@@ -0,0 +1,57 @@
import os
import unicodedata
def _slug(s: str) -> str:
if not s:
return ''
s = unicodedata.normalize('NFKD', s)
s = ''.join(c for c in s if not unicodedata.combining(c))
s = ''.join(c for c in s if c.isalnum() or c in (' ', '_', '-'))
return s.replace(' ', '_')
def generate_vcard(adresse, base_dir: str):
"""Generate a vCard 4.0 file for the given adresse object.
adresse: object with attributes vorname, nachname, strasse, hausnummer,
plz, ort, land, email, telefon_vorwahl, telefon_nummer, id
base_dir: directory where 'vcards/' will be created
Returns the path to the written vcard file.
"""
vcards_dir = os.path.join(base_dir, 'vcards')
os.makedirs(vcards_dir, exist_ok=True)
filename = f"{_slug(adresse.nachname)}_{_slug(adresse.vorname)}_{adresse.id}.vcf"
filepath = os.path.join(vcards_dir, filename)
# build vCard 4.0 content
lines = [
'BEGIN:VCARD',
'VERSION:4.0',
f'N:{adresse.nachname};{adresse.vorname};;;',
f'FN:{adresse.vorname} {adresse.nachname}',
]
street = getattr(adresse, 'strasse', '') or ''
hausnummer = getattr(adresse, 'hausnummer', '') or ''
if hausnummer:
street = f"{street} {hausnummer}".strip()
adr = f'ADR:;;{street};{getattr(adresse, "ort", "")};;{getattr(adresse, "plz", "")};{getattr(adresse, "land", "")}'
lines.append(adr)
if getattr(adresse, 'email', None):
lines.append(f'EMAIL;TYPE=internet:{adresse.email}')
if getattr(adresse, 'telefon_vorwahl', None) or getattr(adresse, 'telefon_nummer', None):
tel = f"+{getattr(adresse, 'telefon_vorwahl', '')}{getattr(adresse, 'telefon_nummer', '')}".replace('++', '+')
lines.append(f'TEL;TYPE=voice:{tel}')
lines.append('END:VCARD')
with open(filepath, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
return filepath