diff --git a/.gitignore b/.gitignore index 9b976cc..c246afc 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ __pycache__/ # vCards vcards + +# +anmeldung.sqbpro +CHAT_SUMMARY.md diff --git a/app.py b/app.py index e3d8527..5bd526c 100644 --- a/app.py +++ b/app.py @@ -2,8 +2,7 @@ from flask import Flask, render_template, request, redirect, url_for from flask_sqlalchemy import SQLAlchemy import os import re -import pathlib -import unicodedata +from utils import generate_vcard BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, 'anmeldung.db') @@ -98,42 +97,8 @@ def index(): # vCard 4.0 erzeugen und speichern try: - vcards_dir = os.path.join(BASE_DIR, 'vcards') - os.makedirs(vcards_dir, exist_ok=True) - - # sanitize filename: remove diacritics and unsafe chars - def slug(s): - 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(' ', '_') - - 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)) + # generate vcard using helper + generate_vcard(adresse, BASE_DIR) except Exception: # nicht kritisch: bei Fehlern nicht die ganze Anfrage abbrechen pass diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pytest diff --git a/tests/test_utils_vcard.py b/tests/test_utils_vcard.py new file mode 100644 index 0000000..4bbdf3b --- /dev/null +++ b/tests/test_utils_vcard.py @@ -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 diff --git a/tests/test_vcard_export.py b/tests/test_vcard_export.py new file mode 100644 index 0000000..94fdc1b --- /dev/null +++ b/tests/test_vcard_export.py @@ -0,0 +1,62 @@ +import pytest +from pathlib import Path + +from app import app, db, Frage + + +@pytest.fixture +def client(tmp_path, monkeypatch): + # 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() + + with app.test_client() as test_client: + yield test_client + + +def test_vcard_created_after_submit(client, tmp_path): + 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', + 'frage_1': '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 diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..ba9a740 --- /dev/null +++ b/utils.py @@ -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