schon/engine/vibes_auth/emailing/models.py
Egor fureunoir Gorbunov eef774c3a3 feat(markdown): integrate markdown rendering and editor support
Replace WYSIWYG editor with Markdown editor across all relevant models and admin fields. Add utilities for rendering and stripping markdown. Adjust serializers, views, and templates to support markdown content. Introduce `PastedImage` model and upload endpoint for handling inline image uploads in markdown.

This change simplifies content formatting while enhancing flexibility with markdown support.
2026-02-27 23:36:51 +03:00

215 lines
5.9 KiB
Python

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