feat: derive event lifecycle status from dates and flags

Replace persisted event statuses with publication and cancellation flags, and compute public lifecycle labels from event timing and registration capacity. Keep filters, registration checks, and profile responses aligned with the derived status.
This commit is contained in:
2026-10-03 12:46:16 +03:30
parent f268c9b88a
commit 6c5fadef2f
26 changed files with 525 additions and 178 deletions

View File

@@ -1,5 +1,6 @@
from django.db import models, transaction
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.text import slugify
@@ -14,6 +15,7 @@ from core.media import (
safe_process_public_image,
)
from core.models import BaseModel
from apps.events.event_status import EVENT_STATUS_LABELS
class Event(BaseModel):
@@ -22,12 +24,6 @@ class Event(BaseModel):
ON_SITE = 'on_site', 'حضوری'
HYBRID = 'hybrid', 'آنلاین/حضوری'
class StatusChoices(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
CANCELLED = 'cancelled', 'Cancelled'
COMPLETED = 'completed', 'Completed'
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True)
description = models.TextField(help_text="Event description in Markdown format")
@@ -43,7 +39,8 @@ class Event(BaseModel):
online_link = models.URLField(max_length=500, blank=True, null=True,
help_text="Link for online events (e.g., Zoom, Google Meet)")
status = models.CharField(max_length=10, choices=StatusChoices.choices, default=StatusChoices.DRAFT)
is_published = models.BooleanField(default=False)
is_cancelled = models.BooleanField(default=False)
capacity = models.PositiveIntegerField(null=True, blank=True,
help_text="Maximum number of attendees (leave blank for unlimited)")
@@ -63,14 +60,75 @@ class Event(BaseModel):
class Meta:
ordering = ['-start_time']
indexes = [
models.Index(fields=['status', 'start_time']),
models.Index(fields=['is_published', 'start_time'], name='event_pub_start_idx'),
models.Index(fields=['event_type']),
]
def __str__(self):
return self.title
def clean(self):
super().clean()
should_validate_deadline = self.pk is None
if self.pk is not None:
previous_schedule = type(self).all_objects.filter(pk=self.pk).values(
"registration_end_date", "start_time"
).first()
should_validate_deadline = previous_schedule is None or (
previous_schedule["registration_end_date"] != self.registration_end_date
or previous_schedule["start_time"] != self.start_time
)
if (
should_validate_deadline
and self.registration_end_date
and self.start_time
and self.registration_end_date > self.start_time
):
raise ValidationError({
"registration_end_date": "مهلت ثبت‌نام باید تا زمان شروع رویداد پایان یابد."
})
@property
def effective_registration_end_date(self):
"""Cap registration at event start, including records with legacy dates."""
if self.registration_end_date is None:
return self.start_time
return min(self.registration_end_date, self.start_time)
def get_display_status(self, *, now=None, registration_count=None):
now = now or timezone.now()
if not self.is_published:
return "draft"
if self.is_cancelled:
return "cancelled"
if self.end_time <= now:
return "completed"
if self.start_time <= now:
return "in_progress"
if now >= self.effective_registration_end_date:
return "registration_closed"
if registration_count is None:
registration_count = getattr(self, "browse_registration_count", None)
if registration_count is None:
registration_count = self.current_attendees_count
if self.capacity is not None and registration_count >= self.capacity:
return "full"
if self.registration_start_date is not None and now < self.registration_start_date:
return "upcoming"
return "registration_open"
@property
def status(self):
"""Computed lifecycle state; no status value is persisted on Event."""
return self.get_display_status()
@property
def status_label(self):
return EVENT_STATUS_LABELS[self.status]
def save(self, *args, **kwargs):
self.clean()
previous_image_name = get_image_previous_name(self, "featured_image")
current_image_name = self.featured_image.name if self.featured_image else None
@@ -103,8 +161,13 @@ class Event(BaseModel):
@property
def is_registration_open(self):
now = timezone.now()
return (self.registration_start_date is None or now >= self.registration_start_date) and \
(self.registration_end_date is None or now <= self.registration_end_date)
return (
self.is_published
and not self.is_cancelled
and now < self.start_time
and (self.registration_start_date is None or now >= self.registration_start_date)
and now < self.effective_registration_end_date
)
@property
def current_attendees_count(self):