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:
2026-08-26 07:55:26 -05:00
co-authored by Cursor
parent 45d0888d33
commit 787f0e48fb
297 changed files with 32534 additions and 3 deletions
+101
View File
@@ -0,0 +1,101 @@
from django.db import models
from core.models import TimeStampedModel, UUIDPrimaryKeyModel
class Channel(models.TextChoices):
EMAIL = "email", "Email"
SMS = "sms", "SMS"
POSTCARD = "postcard", "Postcard"
class Contact(UUIDPrimaryKeyModel, TimeStampedModel):
class Source(models.TextChoices):
CONTACT_FORM = "contact_form", "Contact form"
IMPORT = "import", "Import"
MANUAL = "manual", "Manual"
NOTIFY_ME = "notify_me", "Notify me"
OTHER = "other", "Other"
email = models.EmailField(unique=True, blank=True, null=True)
phone = models.CharField(max_length=32, blank=True)
first_name = models.CharField(max_length=100, blank=True)
last_name = models.CharField(max_length=100, blank=True)
postal_address = models.JSONField(default=dict, blank=True)
source = models.CharField(
max_length=32, choices=Source.choices, default=Source.OTHER
)
notes = models.TextField(blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
name = f"{self.first_name} {self.last_name}".strip()
return name or self.email or self.phone or str(self.pk)
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}".strip()
@staticmethod
def make_postal_address(
*,
line1: str = "",
line2: str = "",
city: str = "",
state: str = "",
zip_code: str = "",
country: str = "US",
) -> dict:
"""Normalize Lob-shaped postal address dict."""
return {
"line1": (line1 or "").strip(),
"line2": (line2 or "").strip(),
"city": (city or "").strip(),
"state": (state or "").strip(),
"zip": (zip_code or "").strip(),
"country": ((country or "").strip() or "US"),
}
@staticmethod
def postal_address_has_content(addr: dict | None) -> bool:
if not addr:
return False
return any(
(addr.get(key) or "").strip()
for key in ("line1", "line2", "city", "state", "zip")
)
class ConsentRecord(TimeStampedModel):
contact = models.ForeignKey(
Contact, on_delete=models.CASCADE, related_name="consents"
)
channel = models.CharField(max_length=16, choices=Channel.choices)
opted_in = models.BooleanField(default=False)
changed_at = models.DateTimeField(auto_now=True)
reason = models.CharField(max_length=255, blank=True)
class Meta:
unique_together = ("contact", "channel")
ordering = ["-changed_at"]
def __str__(self) -> str:
state = "in" if self.opted_in else "out"
return f"{self.contact} {self.channel} opt-{state}"
class Suppression(TimeStampedModel):
contact = models.ForeignKey(
Contact, on_delete=models.CASCADE, related_name="suppressions"
)
channel = models.CharField(max_length=16, choices=Channel.choices)
reason = models.CharField(max_length=255, blank=True)
active = models.BooleanField(default=True)
class Meta:
unique_together = ("contact", "channel")
def __str__(self) -> str:
return f"suppress {self.contact} {self.channel}"