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:
@@ -39,6 +39,27 @@ def _absolute_static_url(site_url: str, relative: str) -> str:
|
||||
return f"{site_url}{path}"
|
||||
|
||||
|
||||
def _tagline_with_site_link(tagline: str, site_url: str) -> str:
|
||||
"""Turn leading MKDRealtor.com (or similar) into a link to the public site."""
|
||||
raw = (tagline or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
# Match "MKDRealtor.com" (any case) at the start, optional trailing " · rest"
|
||||
match = re.match(
|
||||
r"(?i)^(MKDRealtor\.com)(\s*[·•\-–—]\s*.*)?$",
|
||||
raw,
|
||||
)
|
||||
if not match:
|
||||
return html.escape(raw)
|
||||
label = html.escape(match.group(1))
|
||||
rest = html.escape(match.group(2) or "")
|
||||
href = html.escape(site_url, quote=True)
|
||||
return (
|
||||
f'<a href="{href}" style="color:#00626c;text-decoration:none;">{label}</a>'
|
||||
f"{rest}"
|
||||
)
|
||||
|
||||
|
||||
def email_brand_context(**extra):
|
||||
site_url = (getattr(settings, "PUBLIC_SITE_URL", None) or "").rstrip("/")
|
||||
if not site_url:
|
||||
@@ -57,6 +78,7 @@ def email_brand_context(**extra):
|
||||
"brand_name": brand_name,
|
||||
"brand_legal": brand_legal,
|
||||
"brand_tagline": tagline,
|
||||
"brand_tagline_html": _tagline_with_site_link(tagline, site_url),
|
||||
"host_label": host_label,
|
||||
**extra,
|
||||
}
|
||||
@@ -81,3 +103,44 @@ def plain_text_to_email_html(text: str) -> str:
|
||||
f'line-height:1.6;">{joined}</p>'
|
||||
)
|
||||
return "\n".join(blocks)
|
||||
|
||||
|
||||
_HTML_TAG_RE = re.compile(
|
||||
r"<\s*(p|div|br|span|strong|em|b|i|u|a|img|h[1-6]|ul|ol|li|font|table)\b",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def sanitize_email_html(raw: str) -> str:
|
||||
"""Light cleanup for staff-authored HTML (Quill) before sending."""
|
||||
text = raw or ""
|
||||
text = re.sub(r"(?is)<script[^>]*>.*?</script>", "", text)
|
||||
text = re.sub(r"(?is)<iframe[^>]*>.*?</iframe>", "", text)
|
||||
text = re.sub(r"(?is)<object[^>]*>.*?</object>", "", text)
|
||||
text = re.sub(r"(?i)\son\w+\s*=\s*([\"']).*?\1", "", text)
|
||||
text = re.sub(r"(?i)\son\w+\s*=\s*[^\s>]+", "", text)
|
||||
text = re.sub(r"(?i)javascript:", "", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def campaign_body_to_email_html(body: str) -> str:
|
||||
"""Render campaign body for email — HTML as-is when Quill markup, else plain."""
|
||||
raw = (body or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if _HTML_TAG_RE.search(raw):
|
||||
return sanitize_email_html(raw)
|
||||
return plain_text_to_email_html(raw)
|
||||
|
||||
|
||||
def campaign_body_to_plain_text(body: str) -> str:
|
||||
"""Plain-text alternative for multipart emails."""
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
raw = (body or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if _HTML_TAG_RE.search(raw):
|
||||
text = strip_tags(sanitize_email_html(raw))
|
||||
return html.unescape(re.sub(r"[ \t]+\n", "\n", text)).strip()
|
||||
return raw
|
||||
|
||||
@@ -73,7 +73,9 @@
|
||||
{% if brand_name %}
|
||||
<p style="margin:12px 0 0;font-size:15px;font-weight:600;color:#212121;">{{ brand_name }}</p>
|
||||
{% endif %}
|
||||
{% if brand_tagline %}
|
||||
{% if brand_tagline_html %}
|
||||
<p style="margin:4px 0 0;font-size:12px;color:#6b7280;">{{ brand_tagline_html|safe }}</p>
|
||||
{% elif brand_tagline %}
|
||||
<p style="margin:4px 0 0;font-size:12px;color:#6b7280;">{{ brand_tagline }}</p>
|
||||
{% endif %}
|
||||
{% block header_extra %}{% endblock %}
|
||||
@@ -97,10 +99,14 @@
|
||||
© {% now "Y" %} {{ brand_name|default:"Monica Dhillon" }}. All rights reserved.
|
||||
</p>
|
||||
<p style="margin:0;color:#6b7280;">
|
||||
{% if brand_tagline_html %}
|
||||
{{ brand_tagline_html|safe }}
|
||||
{% else %}
|
||||
<a href="{{ site_url|default:'https://mkdrealtor.com' }}" style="color:#00626c;text-decoration:none;">{{ host_label|default:"mkdrealtor.com" }}</a>
|
||||
{% if brand_tagline %}
|
||||
· {{ brand_tagline }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
</td>
|
||||
|
||||
+26
-11
@@ -8,6 +8,7 @@ from django.views.decorators.http import require_GET, require_http_methods
|
||||
|
||||
from analytics.services import attribute_lead_from_request
|
||||
from contacts.models import Channel, ConsentRecord, Contact
|
||||
from contacts.services import upsert_contact
|
||||
from leads.models import Lead
|
||||
from messaging.services import (
|
||||
channel_preferences,
|
||||
@@ -91,12 +92,6 @@ def contact(request):
|
||||
form = ContactForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
defaults = {
|
||||
"first_name": data["first_name"],
|
||||
"last_name": data.get("last_name") or "",
|
||||
"phone": data.get("phone") or "",
|
||||
"source": Contact.Source.CONTACT_FORM,
|
||||
}
|
||||
postal = Contact.make_postal_address(
|
||||
line1=data.get("address_line1") or "",
|
||||
line2=data.get("address_line2") or "",
|
||||
@@ -104,11 +99,16 @@ def contact(request):
|
||||
state=data.get("address_state") or "",
|
||||
zip_code=data.get("address_zip") or "",
|
||||
)
|
||||
if Contact.postal_address_has_content(postal):
|
||||
defaults["postal_address"] = postal
|
||||
contact_obj, _ = Contact.objects.update_or_create(
|
||||
email=data["email"].lower(),
|
||||
defaults=defaults,
|
||||
submitted_email = data["email"].lower()
|
||||
contact_obj, _created, match_reason = upsert_contact(
|
||||
email=submitted_email,
|
||||
first_name=data["first_name"],
|
||||
last_name=data.get("last_name") or "",
|
||||
phone=data.get("phone") or "",
|
||||
postal_address=postal
|
||||
if Contact.postal_address_has_content(postal)
|
||||
else None,
|
||||
source=Contact.Source.CONTACT_FORM,
|
||||
)
|
||||
ConsentRecord.objects.update_or_create(
|
||||
contact=contact_obj,
|
||||
@@ -121,11 +121,26 @@ def contact(request):
|
||||
channel=Channel.SMS,
|
||||
defaults={"opted_in": True, "reason": "contact_form"},
|
||||
)
|
||||
if Contact.postal_address_has_content(postal):
|
||||
ConsentRecord.objects.get_or_create(
|
||||
contact=contact_obj,
|
||||
channel=Channel.POSTCARD,
|
||||
defaults={"opted_in": True, "reason": "contact_form"},
|
||||
)
|
||||
interest = data.get("interest") or ""
|
||||
interest_label = dict(ContactForm.INTEREST_CHOICES).get(interest, interest)
|
||||
body = data.get("message") or ""
|
||||
if interest_label:
|
||||
body = f"Interest: {interest_label}\n\n{body}".strip()
|
||||
if (
|
||||
match_reason in {"phone", "address"}
|
||||
and (contact_obj.email or "").lower() != submitted_email
|
||||
):
|
||||
body = (
|
||||
f"Submitted email: {submitted_email} "
|
||||
f"(merged by {match_reason} with "
|
||||
f"{contact_obj.email or 'existing contact'})\n\n{body}"
|
||||
).strip()
|
||||
lead = Lead.objects.create(
|
||||
contact=contact_obj,
|
||||
message=body,
|
||||
|
||||
Reference in New Issue
Block a user