Ask every member to check the data the club stores about them

The club has to keep its member data current, and until now that meant writing
to each member by hand. "Datenüberprüfung anfragen" in the members tab sends one
mail per member, each listing that member's own record: number, name, nickname,
birth date, contact data, address, status, member since, payment frequency, and
the bank details only for members who have any.

Two decisions the record itself forced:

A field with no value is printed as "(nicht hinterlegt)" rather than left out.
The point of the mail is to have gaps filled in, and a missing line is a gap
nobody sees.

The IBAN is masked down to its country code and last four digits. That is enough
to recognise the account, and it keeps a full account number out of a mail the
club sends to dozens of people at once.

Every member is listed as a recipient, with the live memberships that have an
address preselected -- a member who resigned at year's end may still need to
confirm their address, so the board can add them by hand. Members without an
address are skipped and reported instead of failing the run.

Delivery reuses the existing mail machinery: the configured delivery mode, one
SMTP/IMAP connection for the whole run, an archive copy in the member file, a
"data_review_email_sent" event, and the read-only guard before anything is
rendered. A member whose mail fails is reported as a warning and the run
continues -- one bad address must not stop a mailing to the whole club halfway
through. Because the run cannot be taken back, the recipient count is confirmed
once more before it starts.

Subject and text come from a new "Datenüberprüfung" template, editable like the
others, with {{data.sheet}} for the whole record and a {{#data}} block for a
layout of the board's own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-09-04 22:05:38 +02:00
co-authored by Claude Opus 5
parent 0252a0c0e3
commit 9e9bb7d668
13 changed files with 1070 additions and 4 deletions
+72
View File
@@ -0,0 +1,72 @@
import pytest
pytest.importorskip("tkinter")
from ccma.storage.repository import MemberRepository # noqa: E402
@pytest.fixture
def repository(tmp_path):
repository = MemberRepository(tmp_path / "store")
repository.initialize()
organization = repository.get_configuration()["organization"]
organization.update({"name": "CCC Mannheim e.V.", "email": "verwaltung@example.org"})
repository.save_organization(organization)
return repository
def _member(repository, first_name, last_name, *, email="", status="active"):
member = repository.create_member(
first_name=first_name, last_name=last_name, birth_date="1990-01-01"
)
member.email = email
member.status = status
repository.save_member(member)
return member
def test_the_dialog_preselects_reachable_live_memberships(tk_root, repository):
from ccma.ui.data_review_mail_dialog import DataReviewMailDialog
_member(repository, "Ada", "Lovelace", email="ada@example.org")
_member(repository, "Grace", "Hopper", email="grace@example.org", status="ended")
_member(repository, "Anon", "Ymous")
dialog = DataReviewMailDialog(tk_root, repository)
try:
tk_root.update()
selected = [member.display_name for member in dialog._selected_members()]
assert selected == ["Ada Lovelace"]
# Every member stays listed so a resigned one can still be picked by hand.
assert len(dialog.table.get_children()) == 3
assert "1 von 3 Mitgliedern ausgewählt" in dialog.summary_var.get()
assert "1 Mail(s) werden erzeugt" in dialog.summary_var.get()
assert "1 Mitglied(er) ohne E-Mail-Adresse" in dialog.warning_var.get()
finally:
dialog.destroy()
tk_root.update()
def test_a_selection_without_an_address_generates_nothing(tk_root, repository, monkeypatch):
from tkinter import messagebox
import ccma.ui.data_review_mail_dialog as module
_member(repository, "Anon", "Ymous")
dialog = module.DataReviewMailDialog(tk_root, repository)
try:
tk_root.update()
dialog.table.selection_set(dialog.table.get_children())
informed = []
monkeypatch.setattr(messagebox, "showinfo", lambda *args, **kwargs: informed.append(args))
monkeypatch.setattr(
module, "resolve_delivery_mode", lambda *args: pytest.fail("kein Versand erwartet")
)
dialog._generate()
assert informed and "Keine Empfänger" in informed[0][0]
finally:
dialog.destroy()
tk_root.update()