added backends, improved templating, rbac

This commit is contained in:
2026-06-10 14:40:22 +02:00
parent d9ca48addc
commit ce43f2658f
28 changed files with 1183 additions and 78 deletions

View File

@@ -17,6 +17,7 @@ from app.mailer.attachments.resolver import (
resolve_entry_attachments,
)
from app.mailer.campaign.entries import load_campaign_entries
from app.mailer.campaign.field_values import effective_entry_field_values, ignored_entry_field_overrides
from app.mailer.campaign.models import (
Behavior,
BuildStatus,
@@ -38,7 +39,26 @@ from .models import (
MessageValidationStatus,
)
_FIELD_PATTERN = re.compile(r"(?<!\\)\$\{(.*?)(?<!\\)\}")
_DOLLAR_FIELD_PATTERN = re.compile(r"(?<!\\)\$\{(.*?)(?<!\\)\}")
_BRACE_FIELD_PATTERN = re.compile(r"(?<!\\)\{\{\s*(.*?)\s*\}\}")
def _normalize_template_key(raw: str) -> str:
key = raw.strip()
if key.startswith("fields."):
key = key.removeprefix("fields.")
elif key.startswith("local."):
key = "local::" + key.removeprefix("local.")
elif key.startswith("global."):
key = "global::" + key.removeprefix("global.")
if key.startswith("local::") or key.startswith("global::"):
return key
if key.startswith("local:"):
return "local::" + key.removeprefix("local:")
if key.startswith("global:"):
return "global::" + key.removeprefix("global:")
return key
@dataclass(slots=True)
@@ -70,20 +90,25 @@ def _read_text(campaign_file: str | Path, raw_path: str | None, encoding: str =
def _render_template(template: str, values: dict[str, Any], *, keep_missing: bool = True) -> str:
def replace(match: re.Match[str]) -> str:
key = match.group(1)
key = _normalize_template_key(match.group(1))
if key in values:
value = values[key]
return "" if value is None else str(value)
return match.group(0) if keep_missing else ""
rendered = _FIELD_PATTERN.sub(replace, template)
rendered = _DOLLAR_FIELD_PATTERN.sub(replace, template)
rendered = _BRACE_FIELD_PATTERN.sub(replace, rendered)
return rendered.replace(r"\${", "${").replace(r"\}", "}")
def _find_unresolved_placeholders(text: str | None) -> set[str]:
if not text:
return set()
return set(_FIELD_PATTERN.findall(text))
return {
_normalize_template_key(match.group(1))
for pattern in (_DOLLAR_FIELD_PATTERN, _BRACE_FIELD_PATTERN)
for match in pattern.finditer(text)
}
def _recipient_values(entry: EntryConfig) -> dict[str, str]:
@@ -106,7 +131,7 @@ def _template_values(config: CampaignConfig, entry: EntryConfig) -> dict[str, An
values: dict[str, Any] = {}
for key, value in config.global_values.items():
values[f"global::{key}"] = value
for key, value in entry.fields.items():
for key, value in effective_entry_field_values(config, entry).items():
values[f"local::{key}"] = value
if entry.id:
values["local::id"] = entry.id
@@ -390,6 +415,20 @@ def build_entry_message(
issues = _message_issues_from_attachment_resolution(resolution)
validation_status = _validation_status_from_attachment_status(resolution.status)
ignored_field_overrides = ignored_entry_field_overrides(config, entry)
if ignored_field_overrides:
issues.append(
MessageIssue(
severity="warning",
code="field_override_not_allowed",
message="Recipient field value(s) ignored because the campaign field does not allow overrides: " + ", ".join(ignored_field_overrides),
behavior="warn",
source="fields",
)
)
if validation_status == MessageValidationStatus.READY:
validation_status = MessageValidationStatus.WARNING
if not entry.active:
draft = MessageDraft(
entry_index=entry_index,