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,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}"
|
||||
Reference in New Issue
Block a user