Insert a working repeat block instead of its description

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>
This commit is contained in:
Marcel Peterkau
2026-08-28 21:08:23 +02:00
co-authored by Claude Opus 5
parent 592c5482d6
commit d3dbb5e96d
4 changed files with 275 additions and 31 deletions
+51
View File
@@ -6,6 +6,7 @@ from ccma.domain.mail_templates import (
MailTemplateError,
default_mail_template,
parse_mail_template,
placeholder_entries,
render_mail_template,
serialize_mail_template,
validate_mail_template,
@@ -103,3 +104,53 @@ def test_saving_a_broken_template_is_refused(tmp_path) -> None:
repository.save_mail_template("reminder", subject="Mahnung", body="Hallo {{member.nonsense}}")
with pytest.raises(RepositoryError, match="Betreff"):
repository.save_mail_template("reminder", subject=" ", body="Hallo")
def test_block_rows_insert_a_working_block_not_its_description() -> None:
entries = placeholder_entries("welcome")
block = next(entry for entry in entries if entry.label.startswith("{{#claims}}"))
# The label may abbreviate the block, the inserted snippet may not.
assert block.label == "{{#claims}} … {{/claims}}"
assert block.snippet.startswith("{{#claims}}\n")
assert block.snippet.rstrip().endswith("{{/claims}}")
assert "{{claim.description}}" in block.snippet
assert "" not in block.snippet
assert all("" not in entry.snippet for entry in entries)
def test_inserted_block_validates_and_renders_one_line_per_claim() -> None:
block = next(
entry for entry in placeholder_entries("welcome") if entry.label.startswith("{{#claims}}")
)
body = f"Hallo {{{{member.first_name}}}},\n\n{block.snippet}\nSumme: {{{{claims.total}}}}"
validate_mail_template("welcome", "Betreff", body)
rendered = render_mail_template(
MailTemplate("Betreff", body),
{"member.first_name": "Ada", "claims.total": "75.00"},
{
"claims": [
{
"claim.description": "Aufnahmegebühr",
"claim.due_date": "17.09.2026",
"claim.balance": "15.00",
},
{
"claim.description": "Mitgliedsbeitrag 2. Halbjahr 2026",
"claim.due_date": "17.09.2026",
"claim.balance": "60.00",
},
]
},
)
assert "Aufnahmegebühr (fällig 17.09.2026): 15.00 Euro" in rendered.body
assert "Mitgliedsbeitrag 2. Halbjahr 2026 (fällig 17.09.2026): 60.00 Euro" in rendered.body
assert "" not in rendered.body
def test_every_block_snippet_is_valid_in_its_own_template() -> None:
for spec in MAIL_TEMPLATES:
for block in spec.blocks:
validate_mail_template(spec.key, "Betreff", block.snippet)