Template
Populate the client website template with catalog feature flags.
Extract always-on public/portal/UTM plus optional email_sms, directmail, blog, payments, social, and social_ai so new client sites can be bootstrapped from this seed. Refs #1 Refs #2 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Shared context for branded HTML/text emails (public site palette)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_URL_RE = re.compile(r"(https?://[^\s<]+)")
|
||||
_LOGO_STATIC_PATH = "brand/logo.png"
|
||||
|
||||
|
||||
def _absolute_static_url(site_url: str, relative: str) -> str:
|
||||
"""
|
||||
Build an absolute URL for a static asset.
|
||||
|
||||
Prefer the hashed Manifest URL when available. Fall back to the stable
|
||||
path when the manifest is missing (dj-queue worker does not run
|
||||
collectstatic) so sending mail never crashes.
|
||||
"""
|
||||
try:
|
||||
path = staticfiles_storage.url(relative)
|
||||
except ValueError:
|
||||
logger.debug(
|
||||
"staticfiles manifest miss for %s; using unhashed URL", relative
|
||||
)
|
||||
static_prefix = settings.STATIC_URL or "/static/"
|
||||
path = f"{static_prefix}{relative.lstrip('/')}"
|
||||
|
||||
if path.startswith("http://") or path.startswith("https://"):
|
||||
return path
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
return f"{site_url}{path}"
|
||||
|
||||
|
||||
def _tagline_with_site_link(tagline: str, site_url: str) -> str:
|
||||
"""Escape the tagline; keep a hook for per-client domain linking."""
|
||||
raw = (tagline or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
return html.escape(raw)
|
||||
|
||||
|
||||
def email_brand_context(**extra):
|
||||
site_url = (getattr(settings, "PUBLIC_SITE_URL", None) or "").rstrip("/")
|
||||
if not site_url:
|
||||
site_url = "https://example.com"
|
||||
|
||||
logo_url = _absolute_static_url(site_url, _LOGO_STATIC_PATH)
|
||||
|
||||
brand_name = getattr(settings, "SITE_NAME", None) or "Your Company"
|
||||
brand_legal = getattr(settings, "CREDIT_NAME", None) or brand_name
|
||||
tagline = getattr(settings, "SITE_TAGLINE", None) or ""
|
||||
host_label = site_url.replace("https://", "").replace("http://", "")
|
||||
|
||||
return {
|
||||
"site_url": site_url,
|
||||
"logo_url": logo_url,
|
||||
"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,
|
||||
}
|
||||
|
||||
|
||||
def plain_text_to_email_html(text: str) -> str:
|
||||
"""Escape plain text and turn paragraphs / URLs into simple HTML."""
|
||||
raw = (text or "").replace("\r\n", "\n").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
|
||||
blocks: list[str] = []
|
||||
for para in re.split(r"\n\s*\n", raw):
|
||||
lines = [html.escape(line) for line in para.split("\n")]
|
||||
joined = "<br>\n".join(lines)
|
||||
joined = _URL_RE.sub(
|
||||
r'<a href="\1" style="color:#00626c;text-decoration:underline;">\1</a>',
|
||||
joined,
|
||||
)
|
||||
blocks.append(
|
||||
f'<p style="margin:0 0 16px;color:#212121;font-size:15px;'
|
||||
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
|
||||
Reference in New Issue
Block a user