Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions events/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@

from root.base_admin import SummernoteModelAdmin # type: ignore

from .enums import EventStatus
from .models import Banner, Event, EventSignup, Location, Schedule


class LocationInline(admin.StackedInline):
model = Location
can_delete = False
extra = 1
extra = 0


class BannerInline(admin.StackedInline):
model = Banner
can_delete = False
extra = 1
extra = 0


class ScheduleInline(admin.StackedInline):
model = Schedule
can_delete = False
extra = 1
extra = 0


@admin.register(Event)
Expand All @@ -30,10 +31,10 @@ class EventAdmin(SummernoteModelAdmin, admin.ModelAdmin):
"title",
"total_participants",
"formatted_created_at",
"is_verified",
"status",
)
search_fields = ("title", "description", "location")
list_filter = ("created_at", "updated_at", "is_verified")
list_filter = ("created_at", "updated_at", "status")
readonly_fields = ["created_by"]
inlines = [LocationInline, ScheduleInline, BannerInline]

Expand All @@ -54,7 +55,7 @@ def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(Q(created_by=request.user) | Q(is_verified=True))
return qs.filter(Q(created_by=request.user) | Q(status=EventStatus.ACTIVE))


@admin.register(EventSignup)
Expand Down
8 changes: 8 additions & 0 deletions events/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.db import models


class EventStatus(models.TextChoices):
DRAFT = "DRAFT", "Draft"
ACTIVE = "ACTIVE", "Active"
COMPLETED = "COMPLETED", "Completed"
CANCELLED = "CANCELLED", "Cancelled"
12 changes: 8 additions & 4 deletions events/managers/banners.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from django.db import models
from django.db.models import QuerySet

from events.enums import EventStatus


class BannerQuerySet(QuerySet):
def delete(self, *args, **kwargs):
Expand All @@ -14,13 +16,15 @@ def delete(self, *args, **kwargs):
banners_to_delete = self.filter(event_id=event_id).count()

if banners_to_delete >= total_banners:
verified_event = self.model.objects.filter(
event_id=event_id, event__is_verified=True
).exists()
verified_event = (
self.model.objects.filter(event_id=event_id)
.exclude(event__status=EventStatus.DRAFT)
.exists()
)
if verified_event:
raise PermissionDenied(
"Cannot delete banners as it would leave "
"a verified event without any banners."
"a active event without any banners."
)
super().delete(*args, **kwargs)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 5.1.2 on 2024-12-06 08:10

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('events', '0018_alter_eventsignup_event'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.RemoveField(
model_name='event',
name='is_verified',
),
migrations.AddField(
model_name='event',
name='status',
field=models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('COMPLETED', 'Completed'), ('CANCELLED', 'Cancelled')], default='DRAFT', max_length=10),
),
migrations.AlterField(
model_name='eventsignup',
name='event',
field=models.ForeignKey(limit_choices_to={'status': 'ACTIVE'}, on_delete=django.db.models.deletion.PROTECT, to='events.event'),
),
migrations.AlterField(
model_name='eventsignup',
name='user',
field=models.ForeignKey(limit_choices_to={'status': 'ACTIVE'}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]
32 changes: 26 additions & 6 deletions events/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.auth.models import User
from django.db import models

from events.enums import EventStatus
from events.managers.banners import BannerManager
from events.validators import (
validate_event_attributes,
Expand All @@ -17,9 +18,13 @@ class Event(models.Model):
description = models.TextField()
total_participants = models.PositiveIntegerField()
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
is_verified = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True)
status = models.CharField(
max_length=10,
choices=EventStatus.choices,
default=EventStatus.DRAFT,
)

class Meta:
ordering = ["created_at"]
Expand All @@ -28,7 +33,7 @@ class Meta:

def clean(self):
validate_total_participants(self)
validate_event_attributes(self, "is_verified")
validate_event_attributes(self, "status")

def __str__(self):
return f"Event: {self.title}"
Expand All @@ -39,7 +44,10 @@ class EventSignup(models.Model):
User, on_delete=models.CASCADE, limit_choices_to={"is_active": True}, db_index=True
)
event = models.ForeignKey(
Event, on_delete=models.PROTECT, limit_choices_to={"is_verified": True}, db_index=True
Event,
on_delete=models.PROTECT,
limit_choices_to={"status": EventStatus.ACTIVE},
db_index=True,
)
signup_date = models.DateTimeField(auto_now_add=True, editable=False)

Expand All @@ -59,7 +67,11 @@ def __str__(self):


class Location(models.Model):
event = models.OneToOneField(Event, on_delete=models.CASCADE, related_name="location")
event = models.OneToOneField(
Event,
on_delete=models.CASCADE,
related_name="location",
)
address = models.CharField(max_length=255)
google_map_link = models.URLField(max_length=500)

Expand All @@ -71,7 +83,11 @@ def __str__(self):


class Banner(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="banner")
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="banner",
)
image = models.ImageField(upload_to="event_banners/")
uploaded_at = models.DateTimeField(auto_now_add=True)

Expand All @@ -82,7 +98,11 @@ def __str__(self):


class Schedule(models.Model):
event = models.OneToOneField(Event, on_delete=models.CASCADE, related_name="schedule")
event = models.OneToOneField(
Event,
on_delete=models.CASCADE,
related_name="schedule",
)
start_date = models.DateField()
end_date = models.DateField()
start_time = models.TimeField(default="00:00:00")
Expand Down
5 changes: 3 additions & 2 deletions events/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework import serializers

from .models import Event, EventSignup
from events.enums import EventStatus
from events.models import Event, EventSignup


class EventSerializer(serializers.ModelSerializer):
Expand All @@ -13,7 +14,7 @@ class Meta:
def save(self, **kwargs):
user = self.context["request"].user
if not user.is_superuser:
self.validated_data["is_verified"] = False
self.validated_data["status"] = EventStatus.DRAFT
try:
return super().save(**kwargs)
except DjangoValidationError as e:
Expand Down
27 changes: 20 additions & 7 deletions events/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db.models.signals import post_delete, post_save, pre_delete, pre_save
from django.dispatch import receiver

from events.enums import EventStatus
from events.models import Banner, Event, EventSignup, Location, Schedule
from preferences.enums import EmailTemplateType
from preferences.models import EmailTemplate
Expand Down Expand Up @@ -80,34 +81,46 @@ def validate_schedule(instance, **kwargs):
@receiver(post_delete, sender=Schedule)
def check_verified_event_requirements(instance, **kwargs):
event = instance.event
if event:
if event and event.status in [EventStatus.ACTIVE]:
has_banner = Banner.objects.filter(event=event).exists()
has_location = Location.objects.filter(event=event).exists()
has_schedule = Schedule.objects.filter(event=event).exists()

if not all([has_banner, has_location, has_schedule]):
event.is_verified = False
event.status = EventStatus.DRAFT
event.save()


@receiver(pre_delete, sender=Location)
@receiver(pre_delete, sender=Schedule)
def restrict_deletion(instance, **kwargs):
"""Restrict the deletion of a related model if the event is verified."""
if instance.event.is_verified:
if instance.event.status in [EventStatus.ACTIVE.name, EventStatus.COMPLETED.name]:
raise PermissionDenied(
"You cannot delete this object because its associated with verified event."
"You cannot delete this object because its associated with active event."
)


@receiver(pre_delete, sender=Banner)
def restrict_banner_deletion(instance, **kwargs):
"""Restrict the deletion of a banner if it is the last one for a verified event."""
"""Restrict the deletion of a banner if it is the last one for a active/completed event."""
event = instance.event
if event.is_verified:
if event.status in [EventStatus.ACTIVE.name, EventStatus.COMPLETED.name]:
remaining_banners = Banner.objects.filter(event=event).exclude(id=instance.id).count()
if remaining_banners == 0:
raise PermissionDenied(
"You cannot delete this banner because it is the last one "
"associated with a verified event."
"associated with a active event."
)


@receiver(pre_save, sender=Banner)
def validate_banner(instance, **kwargs):
"""
Validate the banner instance before saving it to the database.
Restrict the addition of a banner for a completed/cancelled event.
"""
event = instance.event
if event.status in [EventStatus.COMPLETED, EventStatus.CANCELLED]:
raise PermissionDenied("You cannot add a banner for a cancelled/completed event.")
instance.full_clean()
12 changes: 8 additions & 4 deletions events/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from django.utils import timezone
from django.utils.timezone import make_aware

from events.enums import EventStatus


def validate_event_dates_and_time(instance):
now = timezone.now()
Expand Down Expand Up @@ -46,8 +48,8 @@ def validate_event_exists(instance):
if not instance.event_id:
raise ValidationError({"event": "Event does not exist."})

if not instance.event.is_verified:
raise ValidationError({"event": "Event is not verified."})
if not instance.event.status == EventStatus.ACTIVE:
raise ValidationError({"event": "Event is not active."})


def validate_event_capacity(instance):
Expand All @@ -71,14 +73,16 @@ def validate_event_attributes(instance, context):
"schedule",
"banner",
]
if context == "is_verified":
if context == "status":
event_instance = instance
elif context == "event":
event_instance = instance.event
else:
return

if (context == "is_verified" and instance.is_verified) or context == "event":
if (
context == "status" and instance.status in [EventStatus.ACTIVE, EventStatus.COMPLETED]
) or context == "event":
missing_attributes = []
for attr in required_attributes:
if attr == "banner":
Expand Down
5 changes: 3 additions & 2 deletions events/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response

from .enums import EventStatus
from .models import Event, EventSignup
from .serializers import EventSerializer, EventSignupSerializer

Expand All @@ -15,10 +16,10 @@ class EventViewSet(viewsets.ModelViewSet):
def get_queryset(self):
user = self.request.user
if user.is_anonymous:
return Event.objects.filter(is_verified=True)
return Event.objects.filter(status=EventStatus.ACTIVE)
if user.is_superuser:
return Event.objects.all()
return Event.objects.filter(Q(created_by=user) | Q(is_verified=True))
return Event.objects.filter(Q(created_by=user) | Q(status=EventStatus.ACTIVE))

def get_permissions(self):
if self.action in ["list", "retrieve"]:
Expand Down
Loading