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

@@ -27,7 +27,7 @@ def test_status_reads_and_expiry_worker_progress_around_a_locked_checkout():
start_time=now + timedelta(days=2), end_time=now + timedelta(days=2, hours=1),
registration_start_date=now - timedelta(days=1),
registration_end_date=now + timedelta(days=1),
status=Event.StatusChoices.PUBLISHED, price=1000, capacity=2,
is_published=True, price=1000, capacity=2,
)
user = User.objects.create_user(username="checkout-lock-owner", mobile="09191112233", password="TestPass123!")
other = User.objects.create_user(username="checkout-lock-other", mobile="09191112234", password="TestPass123!")

View File

@@ -30,10 +30,10 @@ def test_event_reschedule_and_cancellation_notify_registered_users(client, user_
assert event.location == "New hall"
assert notify.call_args.args[1]["type"] == "event_rescheduled"
sms.assert_called_with(attendee.mobile, "event_reschedule", event.title)
cancelled = _put(client, path, {"status": Event.StatusChoices.CANCELLED}, auth_headers(staff))
cancelled = _put(client, path, {"is_cancelled": True}, auth_headers(staff))
assert cancelled.status_code == 200, cancelled.content
event.refresh_from_db()
assert event.status == Event.StatusChoices.CANCELLED
assert event.is_cancelled
assert notify.call_args.args[1]["type"] == "event_cancelled"
assert sms.call_count == 2

View File

@@ -15,7 +15,7 @@ class TestEventModelTests:
def setup_method(self):
now = timezone.now()
self.event = Event.objects.create(title='Workshop', description='**Important** details', start_time=now + timedelta(days=1), end_time=now + timedelta(days=1, hours=2), capacity=1, registration_start_date=now - timedelta(days=1), registration_end_date=now + timedelta(hours=1))
self.event = Event.objects.create(title='Workshop', description='**Important** details', start_time=now + timedelta(days=1), end_time=now + timedelta(days=1, hours=2), capacity=1, registration_start_date=now - timedelta(days=1), registration_end_date=now + timedelta(hours=1), is_published=True)
self.user = User.objects.create_user(username='attendee', mobile='09191110003', password='password')
def test_markdown_description_and_registration_window_remain_available(self):

View File

@@ -43,16 +43,18 @@ class TestEventsAPIIntegrationTests:
type(self).setUpTestData()
self.token = create_jwt_token(self.user)
self.staff_token = create_jwt_token(self.staff)
self.event = self._create_event(title='Integration Event', description='Integration description.', status=Event.StatusChoices.PUBLISHED, price=0)
self.other_event = self._create_event(title='Other Published', description='Searchable', status=Event.StatusChoices.PUBLISHED, price=0)
self.event = self._create_event(title='Integration Event', description='Integration description.', price=0)
self.other_event = self._create_event(title='Other Published', description='Searchable', price=0)
def _auth_headers(self, token):
return {'HTTP_AUTHORIZATION': f'Bearer {token}'}
def _create_event(self, **overrides):
now = timezone.now()
defaults = {'title': 'Event Title', 'description': 'Description', 'start_time': now, 'end_time': now + timedelta(hours=2), 'registration_start_date': now - timedelta(days=1), 'registration_end_date': now + timedelta(days=5), 'slug': f'event-{uuid.uuid4().hex[:6]}', 'location': 'Campus', 'online_link': 'https://meet.example.com', 'price': 0, 'capacity': 10, 'status': Event.StatusChoices.PUBLISHED}
defaults = {'title': 'Event Title', 'description': 'Description', 'start_time': now + timedelta(days=2), 'end_time': now + timedelta(days=2, hours=2), 'registration_start_date': now - timedelta(days=1), 'registration_end_date': now + timedelta(days=1), 'slug': f'event-{uuid.uuid4().hex[:6]}', 'location': 'Campus', 'online_link': 'https://meet.example.com', 'price': 0, 'capacity': 10, 'is_published': True}
defaults.update(overrides)
if 'registration_end_date' not in overrides and defaults['registration_end_date'] > defaults['start_time']:
defaults['registration_end_date'] = defaults['start_time']
return Event.objects.create(**defaults)
def _create_gallery_image(self):
@@ -69,7 +71,7 @@ class TestEventsAPIIntegrationTests:
return Registration.objects.create(event=event, user=user, status=status, final_price=event.price)
def test_list_events_filters_and_search(self):
response = self.client.get('/api/events/', {'status': 'published', 'search': 'Searchable'})
response = self.client.get('/api/events/', {'search': 'Searchable'})
data = response.json()
assert response.status_code == 200
assert any((item['id'] == self.other_event.id for item in data))
@@ -83,7 +85,7 @@ class TestEventsAPIIntegrationTests:
assert response_slug.json()['slug'] == self.event.slug
def test_create_update_and_delete_event(self):
payload = {'title': 'New Event', 'description': 'Desc', 'start_time': (timezone.now() + timedelta(days=1)).isoformat(), 'end_time': (timezone.now() + timedelta(days=1, hours=1)).isoformat(), 'event_type': Event.TypeChoices.ON_SITE, 'status': Event.StatusChoices.DRAFT, 'price': 5000}
payload = {'title': 'New Event', 'description': 'Desc', 'start_time': (timezone.now() + timedelta(days=1)).isoformat(), 'end_time': (timezone.now() + timedelta(days=1, hours=1)).isoformat(), 'event_type': Event.TypeChoices.ON_SITE, 'is_published': False, 'price': 5000}
created = self.client.post('/api/events/', data=json.dumps(payload), content_type='application/json', **self._auth_headers(self.staff_token))
assert created.status_code == 201
event_id = created.json()['id']
@@ -105,7 +107,7 @@ class TestEventsAPIIntegrationTests:
assert detail_resp.status_code == 200
def test_list_events_filters_by_event_type_and_search(self):
event = self._create_event(title='Special Search', description='Unique discovery', event_type=Event.TypeChoices.ONLINE, status=Event.StatusChoices.PUBLISHED)
event = self._create_event(title='Special Search', description='Unique discovery', event_type=Event.TypeChoices.ONLINE)
response = self.client.get('/api/events/', {'event_type': Event.TypeChoices.ONLINE, 'search': 'Unique discovery'})
assert response.status_code == 200
assert any((item['id'] == event.id for item in response.json()))
@@ -160,11 +162,17 @@ class TestEventsAPIIntegrationTests:
self._create_registration(full, self.user, status=Registration.StatusChoices.CONFIRMED)
upcoming = self.client.get('/api/events/browse/', {'state': 'upcoming', 'search': 'Browse'}).json()
open_results = self.client.get('/api/events/browse/', {'state': 'open', 'search': 'Browse'}).json()
closed_results = self.client.get('/api/events/browse/', {'state': 'registration_closed', 'search': 'Browse'}).json()
full_results = self.client.get('/api/events/browse/', {'state': 'full', 'search': 'Browse'}).json()
open_results = self.client.get('/api/events/browse/', {'state': 'registration_open', 'search': 'Browse'}).json()
upcoming_ids = {item['id'] for item in upcoming['results']}
closed_ids = {item['id'] for item in closed_results['results']}
full_ids = {item['id'] for item in full_results['results']}
open_ids = {item['id'] for item in open_results['results']}
assert {not_started.id, closed.id, full.id}.issubset(upcoming_ids)
assert upcoming_ids == {not_started.id}
assert closed_ids == {closed.id}
assert full_ids == {full.id}
assert open_event.id not in upcoming_ids
assert open_ids == {open_event.id}
@@ -220,13 +228,13 @@ class TestEventsAPIIntegrationTests:
assert outside_day.id not in {item['id'] for item in paid_results['results']}
def test_list_events_handles_comma_status_parameter(self):
event = self._create_event(title='Comma Event', status=Event.StatusChoices.PUBLISHED)
results = list_events(None, status=f'{Event.StatusChoices.PUBLISHED},{Event.StatusChoices.DRAFT}', event_type=None, search=None, limit=10, offset=0)
event = self._create_event(title='Comma Event')
results = list_events(None, status='registration_open,in_progress', event_type=None, search=None, limit=10, offset=0)
assert event in list(results)
def test_create_event_attaches_gallery_images(self):
gallery = self._create_gallery_image()
payload = {'title': 'Gallery Event', 'description': 'Gallery desc', 'start_time': (timezone.now() + timedelta(days=1)).isoformat(), 'end_time': (timezone.now() + timedelta(days=1, hours=1)).isoformat(), 'event_type': Event.TypeChoices.ON_SITE, 'status': Event.StatusChoices.DRAFT, 'price': 5000, 'gallery_image_ids': [gallery.id]}
payload = {'title': 'Gallery Event', 'description': 'Gallery desc', 'start_time': (timezone.now() + timedelta(days=1)).isoformat(), 'end_time': (timezone.now() + timedelta(days=1, hours=1)).isoformat(), 'event_type': Event.TypeChoices.ON_SITE, 'is_published': False, 'price': 5000, 'gallery_image_ids': [gallery.id]}
response = self.client.post('/api/events/', data=json.dumps(payload), content_type='application/json', **self._auth_headers(self.staff_token))
body = response.json()
assert response.status_code == 201
@@ -461,7 +469,8 @@ class TestEventSchemasIntegrationTests:
self.user = User.objects.create_user(username='schema_user', email='schema.user@example.com', mobile='09198000003', password=self.password)
self.user.is_email_verified = True
self.user.save(update_fields=['is_email_verified'])
self.event = Event.objects.create(title='Schema Event', description='**bold**', start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=1), registration_start_date=timezone.now() - timedelta(days=1), registration_end_date=timezone.now() + timedelta(days=1), price=1000, slug='schema-event')
now = timezone.now()
self.event = Event.objects.create(title='Schema Event', description='**bold**', start_time=now + timedelta(days=2), end_time=now + timedelta(days=2, hours=1), registration_start_date=now - timedelta(days=1), registration_end_date=now + timedelta(days=1), price=1000, slug='schema-event', is_published=True)
Registration.objects.create(event=self.event, user=self.user, status=Registration.StatusChoices.CONFIRMED, final_price=0)
Registration.objects.create(event=self.event, user=self.user, status=Registration.StatusChoices.ATTENDED, final_price=0)

View File

@@ -37,7 +37,7 @@ class TestRegistrationCheckout:
slug=f"checkout-{suffix}",
price=1250000,
capacity=1,
status=Event.StatusChoices.PUBLISHED,
is_published=True,
)
def auth_headers(self):
@@ -509,8 +509,8 @@ class TestRegistrationCheckout:
from apps.payments.tasks import sweep_stale_payment_verifications
enqueue = mocker.patch("apps.payments.tasks.reconcile_payment.apply_async")
self.event.status = Event.StatusChoices.CANCELLED
self.event.save(update_fields=["status"])
self.event.is_cancelled = True
self.event.save(update_fields=["is_cancelled"])
cancelled = cancel_event_checkouts(self.event.id)