mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
The placeholder chooser showed repeat blocks as "{{#claims}} … {{/claims}}" and
inserted exactly that on double-click -- the ellipsis is a label, so the saved
template rendered a literal "…" per claim instead of the claim.
Chooser rows are data now: each carries the text it reads as and, separately,
the snippet it inserts. A block contributes a complete, ready-to-edit block with
a sample line built from its own placeholders, and lands on a line of its own
when the cursor sits behind existing text.
Covered from both sides: the snippets are checked against the template validator
and renderer, and a UI test drives the real dialog -- insert, save, send -- and
asserts the mail carries actual claim lines. The Tk tests share one root (the
icon library binds its images to the first one) and skip without a display.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
from email.parser import BytesParser
|
|
from email.policy import default
|
|
|
|
import pytest
|
|
|
|
tk = pytest.importorskip("tkinter")
|
|
|
|
from ccma.config import AppConfig # noqa: E402
|
|
from ccma.domain.mail_templates import placeholder_entries # noqa: E402
|
|
from ccma.services.welcome_mail import generate_and_send_welcome_mail # noqa: E402
|
|
from ccma.storage.repository import MemberRepository # noqa: E402
|
|
|
|
|
|
# One root for the whole session: the icon library binds its images to the first
|
|
# Tk instance, so tearing a root down between tests would invalidate them.
|
|
@pytest.fixture(scope="session")
|
|
def tk_root():
|
|
from ccma.ui.theme import load_theme
|
|
|
|
try:
|
|
root = tk.Tk()
|
|
except tk.TclError as exc: # headless CI has no display to build widgets on
|
|
pytest.skip(f"kein Display verfügbar: {exc}")
|
|
root.withdraw()
|
|
load_theme(root, "dark")
|
|
yield root
|
|
root.destroy()
|
|
|
|
|
|
@pytest.fixture
|
|
def repository(tmp_path):
|
|
repository = MemberRepository(tmp_path / "store")
|
|
repository.initialize()
|
|
organization = repository.get_configuration()["organization"]
|
|
organization.update(
|
|
{
|
|
"name": "Chaos Computer Club Mannheim e.V.",
|
|
"email": "verwaltung@example.org",
|
|
"iban": "DE98670505050038907751",
|
|
"bic": "MANSDE66XXX",
|
|
}
|
|
)
|
|
repository.save_organization(organization)
|
|
return repository
|
|
|
|
|
|
def _open_options(tk_root, repository):
|
|
from ccma.ui.options_dialog import OptionsDialog
|
|
|
|
dialog = OptionsDialog(tk_root, AppConfig(store_path=str(repository.root)), repository)
|
|
tk_root.update()
|
|
return dialog
|
|
|
|
|
|
def _block_row(dialog):
|
|
for row in dialog.mail_template_placeholders.get_children():
|
|
label = str(dialog.mail_template_placeholders.item(row, "values")[0])
|
|
if label.startswith("{{#claims}}"):
|
|
return row
|
|
raise AssertionError("Der Wiederholungsblock fehlt in der Platzhalterliste.")
|
|
|
|
|
|
def test_double_click_on_a_block_is_wired_up(tk_root, repository):
|
|
dialog = _open_options(tk_root, repository)
|
|
try:
|
|
assert dialog.mail_template_placeholders.bind("<Double-1>")
|
|
assert dialog.mail_template_snippets[_block_row(dialog)] == next(
|
|
entry.snippet
|
|
for entry in placeholder_entries("welcome")
|
|
if entry.label.startswith("{{#claims}}")
|
|
)
|
|
finally:
|
|
dialog.grab_release()
|
|
dialog.destroy()
|
|
|
|
|
|
def test_inserting_a_block_puts_editable_template_code_into_the_body(tk_root, repository):
|
|
dialog = _open_options(tk_root, repository)
|
|
try:
|
|
dialog.mail_template_body.delete("1.0", "end")
|
|
dialog.mail_template_body.insert("1.0", "Offen sind aktuell:")
|
|
dialog.mail_template_placeholders.selection_set(_block_row(dialog))
|
|
dialog._insert_mail_placeholder()
|
|
tk_root.update()
|
|
body = dialog.mail_template_body.get("1.0", "end-1c")
|
|
finally:
|
|
dialog.grab_release()
|
|
dialog.destroy()
|
|
|
|
assert "…" not in body
|
|
assert "{{#claims}}" in body and "{{/claims}}" in body
|
|
assert "{{claim.description}}" in body
|
|
# The block was appended behind existing text and still starts on its own line.
|
|
assert body.splitlines()[0] == "Offen sind aktuell:"
|
|
assert body.splitlines()[1] == "{{#claims}}"
|
|
|
|
|
|
def test_edited_template_survives_saving_and_produces_real_lines_in_the_mail(
|
|
tk_root, repository, tmp_path
|
|
):
|
|
member = repository.create_member(
|
|
first_name="Ada", last_name="Lovelace", birth_date="1990-01-01"
|
|
)
|
|
member.email = "ada@example.org"
|
|
member.accepted_at = "2026-08-20"
|
|
repository.save_member(member)
|
|
repository.create_manual_claim(
|
|
member.member_id, title="Aufnahmegebühr", amount="15.00", due_date="2026-09-17"
|
|
)
|
|
|
|
dialog = _open_options(tk_root, repository)
|
|
try:
|
|
dialog.mail_template_subject_var.set("Willkommen, {{member.first_name}}")
|
|
dialog.mail_template_body.delete("1.0", "end")
|
|
dialog.mail_template_body.insert("1.0", "Offen sind aktuell:\n")
|
|
dialog.mail_template_placeholders.selection_set(_block_row(dialog))
|
|
dialog._insert_mail_placeholder()
|
|
tk_root.update()
|
|
dialog._save_mail_templates()
|
|
finally:
|
|
dialog.grab_release()
|
|
dialog.destroy()
|
|
|
|
generated = generate_and_send_welcome_mail(
|
|
repository,
|
|
member.member_id,
|
|
delivery_mode="local",
|
|
output_path=tmp_path / "Willkommen.eml",
|
|
sender_name="Verwaltung C3MA",
|
|
sender_email="verwaltung@example.org",
|
|
signature="Der Vorstand",
|
|
)
|
|
|
|
content = BytesParser(policy=default).parsebytes(
|
|
generated.export_path.read_bytes()
|
|
).get_content()
|
|
assert "Aufnahmegebühr (fällig 17.09.2026): 15.00 Euro" in content
|
|
assert "…" not in content
|