Improve campaign personalization and contact duplicate handling.
Add merge tags for email/SMS, paginated removable recipients, PCM event panels for postcard campaigns, and a portal modal when phone/address matches an existing contact.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -25,6 +26,49 @@ AUDIENCE_CHANNEL = {
|
||||
UNSUB_SALT = "monica-site-unsubscribe"
|
||||
UNSUB_MAX_AGE = 60 * 60 * 24 * 365 # 1 year
|
||||
|
||||
# {{first_name}} preferred; {first_name} also accepted (composer hint legacy).
|
||||
_MERGE_TAG_RE = re.compile(
|
||||
r"\{\{\s*(first_name|last_name|email|phone|full_name)\s*\}\}"
|
||||
r"|\{\s*(first_name|last_name|email|phone|full_name)\s*\}",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
REMOVABLE_MESSAGE_STATUSES = frozenset(
|
||||
{
|
||||
Message.Status.DRAFT,
|
||||
Message.Status.SCHEDULED,
|
||||
Message.Status.FAILED,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def render_merge_tags(text: str, contact: Contact | None) -> str:
|
||||
"""Replace personalization tags with contact field values."""
|
||||
if not text:
|
||||
return text or ""
|
||||
first = (getattr(contact, "first_name", None) or "").strip() if contact else ""
|
||||
last = (getattr(contact, "last_name", None) or "").strip() if contact else ""
|
||||
email = (getattr(contact, "email", None) or "").strip() if contact else ""
|
||||
phone = (getattr(contact, "phone", None) or "").strip() if contact else ""
|
||||
full = f"{first} {last}".strip()
|
||||
values = {
|
||||
"first_name": first,
|
||||
"last_name": last,
|
||||
"email": email,
|
||||
"phone": phone,
|
||||
"full_name": full,
|
||||
}
|
||||
|
||||
def _replace(match: re.Match[str]) -> str:
|
||||
key = (match.group(1) or match.group(2) or "").lower()
|
||||
return values.get(key, "")
|
||||
|
||||
return _MERGE_TAG_RE.sub(_replace, text)
|
||||
|
||||
|
||||
def message_is_removable(message: Message) -> bool:
|
||||
return message.status in REMOVABLE_MESSAGE_STATUSES
|
||||
|
||||
|
||||
def contact_may_receive(contact: Contact, channel: str) -> bool:
|
||||
if Suppression.objects.filter(
|
||||
@@ -443,6 +487,16 @@ def send_campaign_test_email(campaign: Campaign, to_email: str) -> None:
|
||||
if not body.strip():
|
||||
raise ValueError("Campaign has no body.")
|
||||
|
||||
# Preview merge tags using first recipient when available.
|
||||
sample = (
|
||||
campaign.messages.select_related("contact")
|
||||
.order_by("created_at")
|
||||
.first()
|
||||
)
|
||||
sample_contact = sample.contact if sample else None
|
||||
subject = render_merge_tags(subject, sample_contact)
|
||||
body = render_merge_tags(body, sample_contact)
|
||||
|
||||
notice = (
|
||||
"This is a test send from the Monica portal. "
|
||||
"Recipient list was not notified."
|
||||
|
||||
Reference in New Issue
Block a user