Improve campaign personalization and contact duplicate handling.
Deploy Beta / unit-tests (push) Successful in 13s
Deploy Beta / docker (push) Successful in 17s
Deploy Beta / deploy-beta (push) Successful in 1m44s

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:
2026-08-10 09:47:04 -05:00
parent 58258f2875
commit 8680c082fe
13 changed files with 747 additions and 101 deletions
+141
View File
@@ -140,6 +140,10 @@ class PortalConsentToggleTests(TestCase):
response = self.client.post(
url,
{
"first_name": "Sam",
"last_name": "",
"email": "sam@example.com",
"phone": "",
"notes": "updated",
"consent_sms": "1",
"consent_postcard": "1",
@@ -155,6 +159,32 @@ class PortalConsentToggleTests(TestCase):
)
self.assertEqual(email_consent.reason, "portal_manual")
def test_portal_can_update_profile(self):
url = reverse("contacts:detail", kwargs={"pk": self.contact.pk})
response = self.client.post(
url,
{
"first_name": "Samantha",
"last_name": "Lee",
"email": "sam.lee@example.com",
"phone": "6305550100",
"address_line1": "10 Main St",
"address_city": "Naperville",
"address_state": "IL",
"address_zip": "60540",
"notes": "VIP",
"consent_email": "1",
},
)
self.assertEqual(response.status_code, 302)
self.contact.refresh_from_db()
self.assertEqual(self.contact.first_name, "Samantha")
self.assertEqual(self.contact.last_name, "Lee")
self.assertEqual(self.contact.email, "sam.lee@example.com")
self.assertEqual(self.contact.phone, "6305550100")
self.assertEqual(self.contact.postal_address.get("line1"), "10 Main St")
self.assertEqual(self.contact.notes, "VIP")
class CampaignDraftSaveTests(TestCase):
def setUp(self):
@@ -899,3 +929,114 @@ class StoredFileUploadTests(TestCase):
self.assertEqual(fetch.status_code, 200)
self.assertEqual(fetch["Content-Type"], "image/png")
self.assertEqual(b"".join(fetch.streaming_content), png)
class MergeTagTests(TestCase):
def test_render_merge_tags_double_and_single_braces(self):
from messaging.services import render_merge_tags
contact = Contact.objects.create(
email="pat@example.com",
first_name="Pat",
last_name="Lee",
phone="6305551212",
)
text = "Hi {{first_name}} {{last_name}} / {first_name} {email}"
out = render_merge_tags(text, contact)
self.assertEqual(out, "Hi Pat Lee / Pat pat@example.com")
def test_email_send_substitutes_merge_tags(self):
from django.core import mail
from messaging.providers.email.smtp2go import send_email
from messaging.services import create_campaign_draft, set_channel_consent
User = get_user_model()
user = User.objects.create_user(username="merge", password="x")
contact = Contact.objects.create(
email="pat@example.com", first_name="Pat", last_name="Lee"
)
set_channel_consent(contact, Channel.EMAIL, opted_in=True, reason="test")
campaign = create_campaign_draft(
name="Merge",
audience=Campaign.Audience.EMAIL_OPT_IN,
subject="Hello {{first_name}}",
body="<p>Dear {{first_name}} {{last_name}}</p>",
created_by=user,
)
send_email(campaign.messages.get())
self.assertEqual(mail.outbox[0].subject, "Hello Pat")
html = mail.outbox[0].alternatives[0][0]
self.assertIn("Dear Pat Lee", html)
self.assertNotIn("{{first_name}}", html)
class CampaignRecipientTableTests(TestCase):
def setUp(self):
from messaging.services import create_campaign_draft, set_channel_consent
User = get_user_model()
self.user = User.objects.create_user(
username="recip", password="test-pass-123"
)
self.client = Client()
self.client.login(username="recip", password="test-pass-123")
self.contacts = []
for i in range(3):
c = Contact.objects.create(
email=f"c{i}@example.com", first_name=f"Name{i}"
)
set_channel_consent(c, Channel.EMAIL, opted_in=True, reason="test")
self.contacts.append(c)
self.campaign = create_campaign_draft(
name="Recipients",
audience=Campaign.Audience.EMAIL_OPT_IN,
subject="Hi",
body="Body",
created_by=self.user,
)
def test_detail_does_not_flash_message_objects(self):
url = reverse(
"messaging:campaign_detail", kwargs={"pk": self.campaign.pk}
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "email →")
self.assertContains(response, "Recipients")
self.assertContains(response, "Remove")
def test_remove_recipient(self):
msg = self.campaign.messages.first()
url = reverse(
"messaging:campaign_message_remove",
kwargs={"pk": self.campaign.pk, "message_id": msg.pk},
)
response = self.client.post(url)
self.assertEqual(response.status_code, 302)
self.assertEqual(self.campaign.messages.count(), 2)
def test_postcard_shows_pcm_events_title(self):
from messaging.services import create_campaign_draft
Contact.objects.create(
email="pc@example.com",
postal_address=Contact.make_postal_address(line1="1 Main"),
)
campaign = create_campaign_draft(
name="Cards",
audience=Campaign.Audience.POSTCARD_OPT_IN,
body="Postcard mailing",
created_by=self.user,
template=MessageTemplate.objects.create(
name="Design",
channel=Channel.POSTCARD,
body="",
postcard_front={"design_id": "99"},
),
)
url = reverse("messaging:campaign_detail", kwargs={"pk": campaign.pk})
response = self.client.get(url)
self.assertContains(response, "Recent PCM Integrations events")
self.assertNotContains(response, "Recent SMTP2GO events")