Selbst gehostete PWA, die Visitenkarten von einem Foto freistellt, ausliest und als Kontakt bereitstellt. - Stapelscan: OpenCV findet die Kartenrechtecke über mehrere Binärmasken, entzerrt sie perspektivisch und schneidet sie einzeln aus. Ohne Fund gilt das ganze Foto als eine Karte. - Extraktion: ein Aufruf je Zuschnitt an das Vision-Modell mit JSON-Schema. Kein vorgeschaltetes OCR - das würde Layout und Schriftgrößen wegwerfen, aus denen die Feldzuordnung entsteht. - Metadaten: Aufnahmezeit und GPS aus den EXIF-Daten des Fotos, Ortsname über Nominatim, Browserstandort nur als Rückfallebene. - Übersicht mit Volltextsuche und Filtern, Detailansicht mit Korrekturmaske. - Notizfeld je Karte, Erinnerungen per Mail inklusive Nachholen verpasster Termine nach einem Neustart. - vCard 3.0 einzeln und als Sammeldatei, Karte gilt danach als exportiert. - Anmeldung über ein Passwort, Sitzung als signiertes Cookie. - Deployment per Tag-Push nach den Konventionen in DEPLOY.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.0 KiB
Python
72 lines
2.0 KiB
Python
from app import vcard
|
|
|
|
BASE = {
|
|
"full_name": "Dr. Anna Berg",
|
|
"first_name": "Anna",
|
|
"last_name": "Berg",
|
|
"job_title": "Geschäftsführerin",
|
|
"company": "Berg & Partner GmbH",
|
|
"email": "a.berg@berg-partner.de",
|
|
"phone": "+49 511 1234560",
|
|
"mobile": "+49 170 1234567",
|
|
"website": "berg-partner.de",
|
|
"street": "Musterweg 3",
|
|
"postal_code": "30159",
|
|
"city": "Hannover",
|
|
"country": "Deutschland",
|
|
"notes": "Sucht ein neues CRM.",
|
|
"captured_at": "2026-09-01T10:15:00+00:00",
|
|
"place": "Hannover, Messegelände",
|
|
"lat": 52.3216,
|
|
"lon": 9.8078,
|
|
}
|
|
|
|
|
|
def test_enthaelt_die_pflichtzeilen():
|
|
output = vcard.build(BASE)
|
|
|
|
assert output.startswith("BEGIN:VCARD\r\nVERSION:3.0\r\n")
|
|
assert output.endswith("END:VCARD\r\n")
|
|
assert "FN:Dr. Anna Berg" in output
|
|
assert "N:Berg;Anna;;;" in output
|
|
assert "TEL;TYPE=CELL:+49 170 1234567" in output
|
|
|
|
|
|
def test_kommas_und_semikolons_werden_maskiert():
|
|
output = vcard.build({**BASE, "company": "Berg, Partner; Co"})
|
|
|
|
assert "ORG:Berg\\, Partner\\; Co" in output
|
|
|
|
|
|
def test_notiz_traegt_zeit_und_ort():
|
|
note = [line for line in vcard.build(BASE).splitlines() if line.startswith("NOTE:")][0]
|
|
|
|
assert "Sucht ein neues CRM." in note
|
|
assert "01.09.2026" in note
|
|
assert "Messegelände" in note
|
|
|
|
|
|
def test_leere_karte_bekommt_trotzdem_einen_namen():
|
|
output = vcard.build({"notes": ""})
|
|
|
|
assert "FN:Unbenannt" in output
|
|
|
|
|
|
def test_firma_ersetzt_den_namen_wenn_keiner_erkannt_wurde():
|
|
output = vcard.build({"company": "Berg & Partner GmbH"})
|
|
|
|
assert "FN:Berg & Partner GmbH" in output
|
|
|
|
|
|
def test_mehrere_karten_in_einer_datei():
|
|
output = vcard.build_many([BASE, {"full_name": "Bea Kurz"}])
|
|
|
|
assert output.count("BEGIN:VCARD") == 2
|
|
assert output.count("END:VCARD") == 2
|
|
|
|
|
|
def test_dateiname_ist_dateisystemtauglich():
|
|
assert vcard.filename(BASE) == "Dr-Anna-Berg.vcf"
|
|
assert vcard.filename({}) == "Unbenannt.vcf"
|
|
assert vcard.filename({"full_name": "?!"}) == "kontakt.vcf"
|