Improve outreach compose, contact merge, and email assets.
Add a Quill email editor with DB-backed image storage, selectable PCM designs, postcard defaults for addressed contacts, and merge-by-phone/address on the contact form.
This commit is contained in:
@@ -32,14 +32,29 @@ def contact_may_receive(contact: Contact, channel: str) -> bool:
|
||||
).exists():
|
||||
return False
|
||||
consent = ConsentRecord.objects.filter(contact=contact, channel=channel).first()
|
||||
if channel == Channel.POSTCARD:
|
||||
# Address on file defaults to postcard-eligible until explicit opt-out.
|
||||
if consent is None:
|
||||
return Contact.postal_address_has_content(contact.postal_address)
|
||||
return bool(consent.opted_in)
|
||||
return bool(consent and consent.opted_in)
|
||||
|
||||
|
||||
def channel_preferences(contact: Contact) -> dict[str, bool]:
|
||||
"""Current opt-in flags for every channel (missing record = False)."""
|
||||
"""Current opt-in flags for every channel (missing record = False).
|
||||
|
||||
Postcard: missing consent + postal address → shown as opted in (default).
|
||||
"""
|
||||
flags = {c.value: False for c in Channel}
|
||||
seen: set[str] = set()
|
||||
for record in contact.consents.all():
|
||||
flags[record.channel] = record.opted_in
|
||||
seen.add(record.channel)
|
||||
if (
|
||||
Channel.POSTCARD not in seen
|
||||
and Contact.postal_address_has_content(contact.postal_address)
|
||||
):
|
||||
flags[Channel.POSTCARD] = True
|
||||
return flags
|
||||
|
||||
|
||||
@@ -165,6 +180,20 @@ def opted_in_contacts(channel: str) -> QuerySet[Contact]:
|
||||
suppressed = Suppression.objects.filter(
|
||||
channel=channel, active=True
|
||||
).values_list("contact_id", flat=True)
|
||||
|
||||
if channel == Channel.POSTCARD:
|
||||
# Explicit opt-in, or address on file with no postcard consent row yet.
|
||||
with_address = Contact.objects.filter(
|
||||
postal_address__has_key="line1",
|
||||
).exclude(postal_address__line1="")
|
||||
explicit = with_address.filter(
|
||||
consents__channel=Channel.POSTCARD,
|
||||
consents__opted_in=True,
|
||||
)
|
||||
implicit = with_address.exclude(consents__channel=Channel.POSTCARD)
|
||||
qs = (explicit | implicit).exclude(pk__in=suppressed).distinct()
|
||||
return qs.order_by("first_name", "last_name", "email")
|
||||
|
||||
qs = (
|
||||
Contact.objects.filter(
|
||||
consents__channel=channel,
|
||||
@@ -174,10 +203,6 @@ def opted_in_contacts(channel: str) -> QuerySet[Contact]:
|
||||
.distinct()
|
||||
.order_by("first_name", "last_name", "email")
|
||||
)
|
||||
if channel == Channel.POSTCARD:
|
||||
qs = qs.filter(postal_address__has_key="line1").exclude(
|
||||
postal_address__line1=""
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
@@ -399,7 +424,11 @@ def send_campaign_test_email(campaign: Campaign, to_email: str) -> None:
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import get_template
|
||||
|
||||
from public.email_branding import email_brand_context, plain_text_to_email_html
|
||||
from public.email_branding import (
|
||||
campaign_body_to_email_html,
|
||||
campaign_body_to_plain_text,
|
||||
email_brand_context,
|
||||
)
|
||||
|
||||
if campaign.channel != Channel.EMAIL:
|
||||
raise ValueError("Test send is only available for email campaigns.")
|
||||
@@ -420,9 +449,9 @@ def send_campaign_test_email(campaign: Campaign, to_email: str) -> None:
|
||||
)
|
||||
ctx = email_brand_context(
|
||||
title=f"[TEST] {subject}",
|
||||
content=f"{body}\n\n{notice}",
|
||||
content=f"{campaign_body_to_plain_text(body)}\n\n{notice}",
|
||||
content_html=(
|
||||
f"{plain_text_to_email_html(body)}"
|
||||
f"{campaign_body_to_email_html(body)}"
|
||||
f'<p style="margin:24px 0 0;color:#6b7280;font-size:13px;">{notice}</p>'
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user