schon/engine/vibes_auth/emailing/models.py
2026-01-26 03:23:41 +03:00

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}"