feat: replace email flows with in-app notifications
Add recipient notification detail and filtering, a queued admin composer, and contextual automatic titles. Retire email delivery, obsolete auth routes, newsletter data, and related templates and settings.
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
from django.db import models
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
import hashlib
|
||||
import uuid
|
||||
|
||||
import markdown
|
||||
@@ -135,8 +133,8 @@ class Registration(BaseModel):
|
||||
default=StatusChoices.PENDING)
|
||||
ticket_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||||
|
||||
confirmation_email_sent_at = models.DateTimeField(null=True, blank=True)
|
||||
cancellation_email_sent_at = models.DateTimeField(null=True, blank=True)
|
||||
confirmation_notification_sent_at = models.DateTimeField(null=True, blank=True)
|
||||
cancellation_notification_sent_at = models.DateTimeField(null=True, blank=True)
|
||||
discount_code = models.ForeignKey(
|
||||
"payments.DiscountCode",
|
||||
null=True,
|
||||
@@ -176,113 +174,22 @@ class Registration(BaseModel):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# 1) on create -> send confirmation if pending/confirmed (and not sent before)
|
||||
if is_create and self.status == self.StatusChoices.CONFIRMED and not self.confirmation_email_sent_at:
|
||||
if is_create and self.status == self.StatusChoices.CONFIRMED and not self.confirmation_notification_sent_at:
|
||||
# lazy import to avoid circular import
|
||||
from apps.events.tasks import send_registration_confirmation_email
|
||||
send_registration_confirmation_email.delay(str(self.pk))
|
||||
self.confirmation_email_sent_at = timezone.now()
|
||||
super().save(update_fields=["confirmation_email_sent_at"])
|
||||
|
||||
from apps.events.tasks import send_registration_confirmation_notification
|
||||
send_registration_confirmation_notification.delay(str(self.pk))
|
||||
self.confirmation_notification_sent_at = timezone.now()
|
||||
super().save(update_fields=["confirmation_notification_sent_at"])
|
||||
# 2) status changed -> cancelled
|
||||
if (not is_create) and (old_status != self.StatusChoices.CANCELLED) and (self.status == self.StatusChoices.CANCELLED) and (not self.cancellation_email_sent_at):
|
||||
from apps.events.tasks import send_registration_cancellation_email
|
||||
send_registration_cancellation_email.delay(str(self.pk))
|
||||
self.cancellation_email_sent_at = timezone.now()
|
||||
super().save(update_fields=["cancellation_email_sent_at"])
|
||||
if (not is_create) and (old_status != self.StatusChoices.CANCELLED) and (self.status == self.StatusChoices.CANCELLED) and (not self.cancellation_notification_sent_at):
|
||||
from apps.events.tasks import send_registration_cancellation_notification
|
||||
send_registration_cancellation_notification.delay(str(self.pk))
|
||||
self.cancellation_notification_sent_at = timezone.now()
|
||||
super().save(update_fields=["cancellation_notification_sent_at"])
|
||||
|
||||
# 3) status changed -> confirmed (if not sent before)
|
||||
if (not is_create) and (old_status != self.StatusChoices.CONFIRMED) and (self.status == self.StatusChoices.CONFIRMED) and (not self.confirmation_email_sent_at):
|
||||
from apps.events.tasks import send_registration_confirmation_email
|
||||
send_registration_confirmation_email.delay(str(self.pk))
|
||||
self.confirmation_email_sent_at = timezone.now()
|
||||
super().save(update_fields=["confirmation_email_sent_at"])
|
||||
|
||||
|
||||
class EventEmailLog(BaseModel):
|
||||
class KindChoices(models.TextChoices):
|
||||
INVITE_NON_REGISTERED = "invite_non_registered", "Invite non-registered users"
|
||||
SKYROOM_CREDENTIALS = "send_skyroom_credentials", "Skyroom credentials"
|
||||
EVENT_ANNOUNCEMENT = "send_event_announcement", "Event announcement"
|
||||
EVENT_ANNOUNCEMENT2 = "send_event_announcement2", "Event announcement 2"
|
||||
EVENT_ANNOUNCEMENT3 = "send_event_announcement3", "Event announcement 3"
|
||||
EVENT_REMINDER = "send_event_reminder", "Event reminder"
|
||||
|
||||
class StatusChoices(models.TextChoices):
|
||||
PENDING = "pending", "Pending"
|
||||
SENT = "sent", "Sent"
|
||||
FAILED = "failed", "Failed"
|
||||
|
||||
KIND_INVITE_NON_REGISTERED = KindChoices.INVITE_NON_REGISTERED
|
||||
KIND_SKYROOM_CREDENTIALS = KindChoices.SKYROOM_CREDENTIALS
|
||||
KIND_EVENT_ANNOUNCEMENT = KindChoices.EVENT_ANNOUNCEMENT
|
||||
KIND_EVENT_ANNOUNCEMENT2 = KindChoices.EVENT_ANNOUNCEMENT2
|
||||
KIND_EVENT_ANNOUNCEMENT3 = KindChoices.EVENT_ANNOUNCEMENT3
|
||||
KIND_EVENT_REMINDER = KindChoices.EVENT_REMINDER
|
||||
KIND_CHOICES = KindChoices.choices
|
||||
|
||||
STATUS_PENDING = StatusChoices.PENDING
|
||||
STATUS_SENT = StatusChoices.SENT
|
||||
STATUS_FAILED = StatusChoices.FAILED
|
||||
STATUS_CHOICES = StatusChoices.choices
|
||||
|
||||
event = models.ForeignKey('events.Event', on_delete=models.CASCADE, related_name='email_logs')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='email_logs')
|
||||
kind = models.CharField(max_length=64, choices=KIND_CHOICES)
|
||||
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_PENDING)
|
||||
error = models.TextField(blank=True, null=True)
|
||||
sent_at = models.DateTimeField(blank=True, null=True)
|
||||
context_hash = models.CharField(max_length=64, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("event", "user", "kind", "context_hash")
|
||||
indexes = [
|
||||
models.Index(fields=["event", "kind", "status"]),
|
||||
models.Index(fields=["user", "kind", "status"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.event.id} - {self.user.id} - {self.kind} - {self.status}"
|
||||
|
||||
@staticmethod
|
||||
def _hash_context(context):
|
||||
if context is None:
|
||||
return None
|
||||
if not isinstance(context, str):
|
||||
context = str(context)
|
||||
return hashlib.sha256(context.encode("utf-8")).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def claim(cls, *, event_id, user_id, kind, context=None):
|
||||
context_hash = cls._hash_context(context)
|
||||
log, created = cls.objects.get_or_create(
|
||||
event_id=event_id,
|
||||
user_id=user_id,
|
||||
kind=kind,
|
||||
context_hash=context_hash,
|
||||
defaults={"status": cls.STATUS_PENDING},
|
||||
)
|
||||
if not created and log.status in (cls.STATUS_PENDING, cls.STATUS_SENT):
|
||||
return log, True
|
||||
if not created:
|
||||
log._commit_status(cls.STATUS_PENDING, error="")
|
||||
return log, False
|
||||
|
||||
def _commit_status(self, status, *, error="", sent_at=None):
|
||||
self.status = status
|
||||
self.error = error
|
||||
update_fields = ["status", "error"]
|
||||
if status == self.STATUS_SENT:
|
||||
self.sent_at = sent_at or timezone.now()
|
||||
update_fields.append("sent_at")
|
||||
elif self.sent_at is not None:
|
||||
self.sent_at = None
|
||||
update_fields.append("sent_at")
|
||||
if hasattr(self, "updated_at"):
|
||||
update_fields.append("updated_at")
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
def mark_sent(self):
|
||||
self._commit_status(self.STATUS_SENT)
|
||||
|
||||
def mark_failed(self, error):
|
||||
self._commit_status(self.STATUS_FAILED, error=error)
|
||||
if (not is_create) and (old_status != self.StatusChoices.CONFIRMED) and (self.status == self.StatusChoices.CONFIRMED) and (not self.confirmation_notification_sent_at):
|
||||
from apps.events.tasks import send_registration_confirmation_notification
|
||||
send_registration_confirmation_notification.delay(str(self.pk))
|
||||
self.confirmation_notification_sent_at = timezone.now()
|
||||
super().save(update_fields=["confirmation_notification_sent_at"])
|
||||
|
||||
Reference in New Issue
Block a user