mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
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:
co-authored by
Claude Opus 5
parent
592c5482d6
commit
d3dbb5e96d
@@ -26,6 +26,33 @@ class MailTemplate:
|
||||
body: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MailTemplateBlock:
|
||||
name: str
|
||||
description: str
|
||||
# Placeholder name -> short explanation, valid inside this block only.
|
||||
placeholders: tuple[tuple[str, str], ...]
|
||||
# The line the inserted block starts out with. It makes the inserted snippet a
|
||||
# working block that renders real content -- the board edits this line rather
|
||||
# than having to write the whole construct by hand.
|
||||
sample: str
|
||||
|
||||
@property
|
||||
def snippet(self) -> str:
|
||||
return f"{{{{#{self.name}}}}}\n{self.sample}\n{{{{/{self.name}}}}}\n"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PlaceholderEntry:
|
||||
"""One row of the options dialog's placeholder chooser: what it reads as, and
|
||||
what double-clicking it puts into the template."""
|
||||
|
||||
label: str
|
||||
snippet: str
|
||||
description: str
|
||||
indented: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MailTemplateSpec:
|
||||
key: str
|
||||
@@ -36,8 +63,7 @@ class MailTemplateSpec:
|
||||
# dialog. The mail services must supply exactly these keys, so this doubles as
|
||||
# the contract a stored template is validated against.
|
||||
placeholders: tuple[tuple[str, str], ...]
|
||||
# Repeat block name -> (explanation, item placeholders).
|
||||
blocks: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...] = ()
|
||||
blocks: tuple[MailTemplateBlock, ...] = ()
|
||||
|
||||
|
||||
BASE_PLACEHOLDERS: tuple[tuple[str, str], ...] = (
|
||||
@@ -99,10 +125,11 @@ MAIL_TEMPLATES: tuple[MailTemplateSpec, ...] = (
|
||||
("payment.reference", "Vorgeschlagener Verwendungszweck"),
|
||||
),
|
||||
blocks=(
|
||||
(
|
||||
"claims",
|
||||
"Wiederholt sich je ausgewählter Forderung",
|
||||
CLAIM_ITEM_PLACEHOLDERS,
|
||||
MailTemplateBlock(
|
||||
name="claims",
|
||||
description="Wiederholt sich je ausgewählter Forderung",
|
||||
placeholders=CLAIM_ITEM_PLACEHOLDERS,
|
||||
sample="{{claim.description}} (fällig {{claim.due_date}}): {{claim.balance}} Euro",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -128,13 +155,14 @@ MAIL_TEMPLATES: tuple[MailTemplateSpec, ...] = (
|
||||
("payment.reference", "Vorgeschlagener Verwendungszweck"),
|
||||
),
|
||||
blocks=(
|
||||
(
|
||||
"reminder.items",
|
||||
"Wiederholt sich je Mahnposition",
|
||||
(
|
||||
MailTemplateBlock(
|
||||
name="reminder.items",
|
||||
description="Wiederholt sich je Mahnposition",
|
||||
placeholders=(
|
||||
("item.description", "Bezeichnung der Position"),
|
||||
("item.amount", "Betrag der Position"),
|
||||
),
|
||||
sample="{{item.description}}: {{item.amount}} Euro",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -157,6 +185,28 @@ MAIL_TEMPLATES: tuple[MailTemplateSpec, ...] = (
|
||||
)
|
||||
|
||||
|
||||
def placeholder_entries(key: str) -> list[PlaceholderEntry]:
|
||||
"""Rows for the placeholder chooser. Repeat blocks contribute a complete,
|
||||
ready-to-edit block -- inserting one has to leave a working template behind,
|
||||
not a description of one."""
|
||||
spec = template_spec(key)
|
||||
entries = [
|
||||
PlaceholderEntry(f"{{{{{name}}}}}", f"{{{{{name}}}}}", description)
|
||||
for name, description in spec.placeholders
|
||||
]
|
||||
for block in spec.blocks:
|
||||
entries.append(
|
||||
PlaceholderEntry(
|
||||
f"{{{{#{block.name}}}}} … {{{{/{block.name}}}}}", block.snippet, block.description
|
||||
)
|
||||
)
|
||||
entries.extend(
|
||||
PlaceholderEntry(f"{{{{{name}}}}}", f"{{{{{name}}}}}", description, indented=True)
|
||||
for name, description in block.placeholders
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
def template_spec(key: str) -> MailTemplateSpec:
|
||||
for spec in MAIL_TEMPLATES:
|
||||
if spec.key == key:
|
||||
@@ -209,15 +259,12 @@ def validate_mail_template(key: str, subject: str, body: str) -> None:
|
||||
while editing the template instead of during a send run."""
|
||||
spec = template_spec(key)
|
||||
known = {name for name, _help in spec.placeholders}
|
||||
block_names = {name for name, _help, _items in spec.blocks}
|
||||
block_names = {block.name for block in spec.blocks}
|
||||
unknown: set[str] = set()
|
||||
for block_name, _help, item_placeholders in spec.blocks:
|
||||
for match in _blocks_of(body, block_name):
|
||||
unknown.update(
|
||||
name
|
||||
for name in _placeholder_names(match)
|
||||
if name not in known and name not in {item for item, _text in item_placeholders}
|
||||
)
|
||||
for block in spec.blocks:
|
||||
block_known = known | {name for name, _help in block.placeholders}
|
||||
for match in _blocks_of(body, block.name):
|
||||
unknown.update(name for name in _placeholder_names(match) if name not in block_known)
|
||||
remaining = body
|
||||
for block_name in block_names:
|
||||
remaining = _strip_blocks(remaining, block_name)
|
||||
|
||||
Reference in New Issue
Block a user