Bildeditor, klickbare Felder, Einstellungen; Kopfzeile und Namensfeld raus
All checks were successful
Build & Deploy / deploy (push) Successful in 1m29s

Bildbearbeitung im Nachgang: Drehen in 90-Grad-Schritten, Ausrichten per
Schieberegler, Zuschneiden per Rahmen mit anfassbaren Ecken. Der Server
rechnet jede Korrektur vom unbearbeiteten Original, das beim ersten
Eingriff daneben abgelegt wird - sonst summieren sich die Verluste über
mehrere Korrekturen, und Zurücksetzen wäre nicht möglich. Zwei
Zuschnitte hintereinander werden ineinander verrechnet.

Der Ort ließ sich nicht übernehmen, weil beide Quellen versagen können:
iOS entfernt beim Weitergeben an eine Webseite oft die GPS-Daten aus dem
Foto, und der Browserstandort scheiterte bisher stumm. Jetzt meldet er
seinen Grund ("Standortfreigabe fehlt"), die Detailansicht zeigt die
Herkunft des Ortes, und der Scan-Ort ist von Hand nachtragbar.

E-Mail, Web, Telefon und Mobil sind in der Tabelle jetzt Links; Straße,
PLZ, Ort und Land stehen als eine Adresszeile, die die eingestellte
Navigations-App öffnet.

Einstellungen als vierter Reiter: Navigations-App (Apple Karten, Google
Maps, OpenStreetMap), Statusübersicht und Abmelden - der Knopf saß
vorher in der Kopfzeile.

Die Kopfzeile ist weg. Sie kostete auf dem Telefon eine Bildschirmzeile
und wiederholte nur, was die Tableiste schon sagt.

Kein full_name mehr, nur Vor- und Nachname; akademische Titel gehören in
den Vornamen. Bestehende Datensätze werden bei der Migration am letzten
Leerzeichen aufgeteilt. Die Spalte bleibt ungenutzt stehen: SQLite baut
zum Löschen die ganze Tabelle neu, der Gewinn wären ein paar Byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas Orth
2026-09-06 15:19:10 +02:00
parent bf0a397597
commit 1e8918e0cd
14 changed files with 592 additions and 110 deletions

View File

@@ -75,10 +75,10 @@ def test_bearbeiten_und_suchen(client, scanned):
patched = client.patch(
f"/api/cards/{card_id}",
json={"full_name": "Anna Berg", "company": "Berg & Partner", "notes": "Messe"},
json={"first_name": "Anna", "last_name": "Berg", "company": "Berg & Partner", "notes": "Messe"},
)
assert patched.status_code == 200
assert patched.json()["full_name"] == "Anna Berg"
assert patched.json()["last_name"] == "Berg"
hits = client.get("/api/cards", params={"q": "berg"}).json()["cards"]
assert [c["id"] for c in hits] == [card_id]
@@ -97,7 +97,7 @@ def test_unbekannte_felder_werden_abgewiesen(client, scanned):
def test_vcard_download_markiert_die_karte_als_exportiert(client, scanned):
card_id = scanned["cards"][1]["id"]
client.patch(f"/api/cards/{card_id}", json={"full_name": "Bea Kurz"})
client.patch(f"/api/cards/{card_id}", json={"first_name": "Bea", "last_name": "Kurz"})
response = client.get(f"/vcf/{card_id}")
@@ -143,7 +143,7 @@ def test_erinnerung_ohne_datum_wird_abgewiesen(client, scanned):
def test_faellige_erinnerung_wird_verschickt(client, scanned, monkeypatch):
card_id = scanned["cards"][3]["id"]
client.patch(f"/api/cards/{card_id}", json={"full_name": "Carl Spät"})
client.patch(f"/api/cards/{card_id}", json={"first_name": "Carl", "last_name": "Spät"})
past = datetime.now(timezone.utc) - timedelta(minutes=5)
client.post(
f"/api/cards/{card_id}/reminders",
@@ -254,13 +254,14 @@ def test_handeintrag_ueberlebt_den_hintergrundlauf(client, monkeypatch):
"/api/scan", files={"file": ("einzel.jpg", photo_bytes(1), "image/jpeg")}
).json()["cards"][0]["id"]
client.patch(f"/api/cards/{card_id}", json={"full_name": "Von Hand", "notes": "Messe"})
client.patch(f"/api/cards/{card_id}", json={"first_name": "Von", "last_name": "Hand", "notes": "Messe"})
monkeypatch.undo()
warte_bis_gelesen(client, [card_id])
card = client.get(f"/api/cards/{card_id}").json()
assert card["full_name"] == "Von Hand"
assert card["first_name"] == "Von"
assert card["last_name"] == "Hand"
assert card["notes"] == "Messe"

View File

@@ -29,8 +29,8 @@ def leere_tabelle():
def test_gleiche_email_wird_gefunden():
_card(full_name="Anna Berg", email="A.Berg@Firma.de")
neu = _card(full_name="A. Berg", email="a.berg@firma.de")
_card(first_name="Anna", last_name="Berg", email="A.Berg@Firma.de")
neu = _card(first_name="A.", last_name="Berg", email="a.berg@firma.de")
treffer = duplicates.find(neu)
@@ -38,8 +38,8 @@ def test_gleiche_email_wird_gefunden():
def test_telefonnummer_in_anderer_schreibweise():
_card(full_name="Anna Berg", phone="+49 511 1234560")
neu = _card(full_name="Anna B.", mobile="0511/1234560")
_card(first_name="Anna", last_name="Berg", phone="+49 511 1234560")
neu = _card(first_name="Anna", last_name="B.", mobile="0511/1234560")
treffer = duplicates.find(neu)
@@ -62,27 +62,27 @@ def test_gleicher_nachname_allein_reicht_nicht():
def test_kurze_nummern_werden_nicht_verglichen():
# Eine dreistellige Durchwahl darf keine Dublette erzeugen.
_card(full_name="Anna Berg", phone="123")
neu = _card(full_name="Bea Kurz", phone="123")
_card(first_name="Anna", last_name="Berg", phone="123")
neu = _card(first_name="Bea", last_name="Kurz", phone="123")
assert duplicates.find(neu) == []
def test_leere_felder_erzeugen_keine_treffer():
_card(full_name="Anna Berg")
neu = _card(full_name="Bea Kurz")
_card(first_name="Anna", last_name="Berg")
neu = _card(first_name="Bea", last_name="Kurz")
assert duplicates.find(neu) == []
def test_die_karte_selbst_ist_keine_dublette():
karte = _card(full_name="Anna Berg", email="a@b.de")
karte = _card(first_name="Anna", last_name="Berg", email="a@b.de")
assert duplicates.find(karte) == []
def test_noch_nicht_gelesene_karten_bleiben_aussen_vor():
_card(full_name="Anna Berg", email="a@b.de", extract_status="offen")
neu = _card(full_name="Anna Berg", email="a@b.de")
_card(first_name="Anna", last_name="Berg", email="a@b.de", extract_status="offen")
neu = _card(first_name="Anna", last_name="Berg", email="a@b.de")
assert duplicates.find(neu) == []

View File

@@ -1,8 +1,7 @@
from app import vcard
BASE = {
"full_name": "Dr. Anna Berg",
"first_name": "Anna",
"first_name": "Dr. Anna",
"last_name": "Berg",
"job_title": "Geschäftsführerin",
"company": "Berg & Partner GmbH",
@@ -28,7 +27,7 @@ def test_enthaelt_die_pflichtzeilen():
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 "N:Berg;Dr. Anna;;;" in output
assert "TEL;TYPE=CELL:+49 170 1234567" in output
@@ -52,6 +51,13 @@ def test_leere_karte_bekommt_trotzdem_einen_namen():
assert "FN:Unbenannt" in output
def test_name_setzt_sich_aus_vor_und_nachname_zusammen():
output = vcard.build({"first_name": "Dr. Anna", "last_name": "Berg"})
assert "FN:Dr. Anna Berg" in output
assert "N:Berg;Dr. Anna;;;" in output
def test_firma_ersetzt_den_namen_wenn_keiner_erkannt_wurde():
output = vcard.build({"company": "Berg & Partner GmbH"})
@@ -61,4 +67,4 @@ def test_firma_ersetzt_den_namen_wenn_keiner_erkannt_wurde():
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"
assert vcard.filename({"first_name": "?!"}) == "kontakt.vcf"