from uuid import uuid4 from django.conf import settings from django.db.models import ( CASCADE, SET_NULL, CharField, DateTimeField, ForeignKey, 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: """Kept for historic migrations that reference this callable.""" return f"email_images/{instance.uuid}/{filename}" 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 }}"), ) content = TextField( verbose_name=_("content"), help_text=_( "email body in markdown - 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}"