248 lines
6.7 KiB
Python
248 lines
6.7 KiB
Python
from uuid import uuid4
|
|
|
|
from django.conf import settings
|
|
from django.db.models import (
|
|
CASCADE,
|
|
SET_NULL,
|
|
CharField,
|
|
DateTimeField,
|
|
ForeignKey,
|
|
ImageField,
|
|
Index,
|
|
PositiveIntegerField,
|
|
SlugField,
|
|
TextField,
|
|
UUIDField,
|
|
)
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from engine.core.abstract import NiceModel
|
|
from engine.vibes_auth.emailing.choices import CampaignStatus, RecipientStatus
|
|
|
|
|
|
def get_email_image_path(instance, filename: str) -> str:
|
|
return f"email_images/{instance.uuid}/{filename}"
|
|
|
|
|
|
class EmailImage(NiceModel):
|
|
"""
|
|
Stores images that can be used in email templates.
|
|
Images are uploaded to filesystem and referenced by URL in templates.
|
|
"""
|
|
|
|
name = CharField(
|
|
max_length=100,
|
|
verbose_name=_("name"),
|
|
help_text=_("descriptive name for the image"),
|
|
)
|
|
image = ImageField(
|
|
upload_to=get_email_image_path,
|
|
verbose_name=_("image"),
|
|
help_text=_("image file to use in email templates"),
|
|
)
|
|
alt_text = CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default="",
|
|
verbose_name=_("alt text"),
|
|
help_text=_("alternative text for accessibility"),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("email image")
|
|
verbose_name_plural = _("email images")
|
|
ordering = ("-created",)
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class EmailTemplate(NiceModel):
|
|
"""
|
|
Customizable email template stored in the database.
|
|
Uses WYSIWYG editor for rich HTML content editing.
|
|
|
|
Available context variables should be documented in the template
|
|
to help administrators understand what placeholders they can use.
|
|
"""
|
|
|
|
name = CharField(
|
|
max_length=100,
|
|
verbose_name=_("name"),
|
|
help_text=_("internal name for the template"),
|
|
)
|
|
slug = SlugField(
|
|
unique=True,
|
|
verbose_name=_("slug"),
|
|
help_text=_("unique identifier for the template"),
|
|
)
|
|
subject = CharField(
|
|
max_length=255,
|
|
verbose_name=_("subject"),
|
|
help_text=_("email subject line - supports {{ variables }}"),
|
|
)
|
|
html_content = TextField(
|
|
verbose_name=_("HTML content"),
|
|
help_text=_(
|
|
"email body content - supports {{ user.first_name }}, "
|
|
"{{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}"
|
|
),
|
|
)
|
|
plain_content = TextField(
|
|
blank=True,
|
|
default="",
|
|
verbose_name=_("plain text content"),
|
|
help_text=_("plain text fallback (auto-generated if empty)"),
|
|
)
|
|
available_variables = TextField(
|
|
blank=True,
|
|
default="user.first_name, user.last_name, user.email, project_name, unsubscribe_url",
|
|
verbose_name=_("available variables"),
|
|
help_text=_("documentation of available template variables"),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("email template")
|
|
verbose_name_plural = _("email templates")
|
|
ordering = ("name",)
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class EmailCampaign(NiceModel):
|
|
"""
|
|
Represents an email campaign that can be sent to subscribed users.
|
|
Only campaigns send emails with unsubscribe links.
|
|
"""
|
|
|
|
name = CharField(
|
|
max_length=200,
|
|
verbose_name=_("name"),
|
|
help_text=_("internal name for the campaign"),
|
|
)
|
|
template = ForeignKey(
|
|
EmailTemplate,
|
|
on_delete=SET_NULL,
|
|
null=True,
|
|
related_name="campaigns",
|
|
verbose_name=_("template"),
|
|
help_text=_("email template to use for this campaign"),
|
|
)
|
|
status = CharField(
|
|
max_length=16,
|
|
choices=CampaignStatus.choices,
|
|
default=CampaignStatus.DRAFT,
|
|
verbose_name=_("status"),
|
|
)
|
|
scheduled_at = DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("scheduled at"),
|
|
help_text=_("when to send the campaign (leave empty for manual send)"),
|
|
)
|
|
sent_at = DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("sent at"),
|
|
help_text=_("when the campaign was actually sent"),
|
|
)
|
|
# Statistics
|
|
total_recipients = PositiveIntegerField(
|
|
default=0,
|
|
verbose_name=_("total recipients"),
|
|
)
|
|
sent_count = PositiveIntegerField(
|
|
default=0,
|
|
verbose_name=_("sent count"),
|
|
)
|
|
failed_count = PositiveIntegerField(
|
|
default=0,
|
|
verbose_name=_("failed count"),
|
|
)
|
|
opened_count = PositiveIntegerField(
|
|
default=0,
|
|
verbose_name=_("opened count"),
|
|
)
|
|
clicked_count = PositiveIntegerField(
|
|
default=0,
|
|
verbose_name=_("clicked count"),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("email campaign")
|
|
verbose_name_plural = _("email campaigns")
|
|
ordering = ("-created",)
|
|
indexes = [
|
|
Index(fields=["status", "scheduled_at"], name="campaign_status_sched_idx"),
|
|
]
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class CampaignRecipient(NiceModel):
|
|
"""
|
|
Tracks individual email sends for a campaign.
|
|
Used for delivery tracking and preventing duplicate sends.
|
|
"""
|
|
|
|
campaign = ForeignKey(
|
|
EmailCampaign,
|
|
on_delete=CASCADE,
|
|
related_name="recipients",
|
|
verbose_name=_("campaign"),
|
|
)
|
|
user = ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=CASCADE,
|
|
related_name="campaign_emails",
|
|
verbose_name=_("user"),
|
|
)
|
|
status = CharField(
|
|
max_length=16,
|
|
choices=RecipientStatus.choices,
|
|
default=RecipientStatus.PENDING,
|
|
verbose_name=_("status"),
|
|
)
|
|
sent_at = DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("sent at"),
|
|
)
|
|
opened_at = DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("opened at"),
|
|
)
|
|
clicked_at = DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("clicked at"),
|
|
)
|
|
# For tracking pixel and link tracking
|
|
tracking_id = UUIDField(
|
|
default=uuid4,
|
|
unique=True,
|
|
verbose_name=_("tracking ID"),
|
|
help_text=_("unique ID for tracking opens and clicks"),
|
|
)
|
|
error_message = TextField(
|
|
blank=True,
|
|
default="",
|
|
verbose_name=_("error message"),
|
|
help_text=_("error details if sending failed"),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("campaign recipient")
|
|
verbose_name_plural = _("campaign recipients")
|
|
ordering = ("-created",)
|
|
indexes = [
|
|
Index(fields=["campaign", "status"], name="recipient_camp_status_idx"),
|
|
Index(fields=["tracking_id"], name="recipient_tracking_idx"),
|
|
]
|
|
constraints = []
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.campaign.name} -> {self.user.email}"
|