diff --git a/events/admin.py b/events/admin.py index f849013..943714e 100644 --- a/events/admin.py +++ b/events/admin.py @@ -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) @@ -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] @@ -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) diff --git a/events/enums.py b/events/enums.py new file mode 100644 index 0000000..4936c34 --- /dev/null +++ b/events/enums.py @@ -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" diff --git a/events/managers/banners.py b/events/managers/banners.py index 41e6a0e..13ff71a 100644 --- a/events/managers/banners.py +++ b/events/managers/banners.py @@ -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): @@ -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) diff --git a/events/migrations/0019_remove_event_is_verified_event_status_and_more.py b/events/migrations/0019_remove_event_is_verified_event_status_and_more.py new file mode 100644 index 0000000..7281c7a --- /dev/null +++ b/events/migrations/0019_remove_event_is_verified_event_status_and_more.py @@ -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), + ), + ] diff --git a/events/models.py b/events/models.py index fc43f4b..5a72cc0 100644 --- a/events/models.py +++ b/events/models.py @@ -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, @@ -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"] @@ -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}" @@ -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) @@ -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) @@ -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) @@ -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") diff --git a/events/serializers.py b/events/serializers.py index be16b30..8845233 100644 --- a/events/serializers.py +++ b/events/serializers.py @@ -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): @@ -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: diff --git a/events/signals.py b/events/signals.py index 76c897a..b5e7e3f 100644 --- a/events/signals.py +++ b/events/signals.py @@ -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 @@ -80,13 +81,13 @@ 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() @@ -94,20 +95,32 @@ def check_verified_event_requirements(instance, **kwargs): @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() diff --git a/events/validators.py b/events/validators.py index 19e1131..978d94d 100644 --- a/events/validators.py +++ b/events/validators.py @@ -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() @@ -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): @@ -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": diff --git a/events/views.py b/events/views.py index df9cb52..4e3c7a5 100644 --- a/events/views.py +++ b/events/views.py @@ -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 @@ -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"]: