diff --git a/backend/djangoindia/api/serializers/event.py b/backend/djangoindia/api/serializers/event.py index 2f3de88e..280b7ca2 100644 --- a/backend/djangoindia/api/serializers/event.py +++ b/backend/djangoindia/api/serializers/event.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from djangoindia.db.models.event import Event, EventRegistration,Sponsor,Sponsorship +from djangoindia.db.models.event import Event, EventRegistration @@ -28,6 +28,8 @@ class EventSerializer(serializers.Serializer): end_date= serializers.DateTimeField() registration_end_date= serializers.DateTimeField() event_mode = serializers.CharField() + max_seats = serializers.IntegerField() + seats_left = serializers.IntegerField() sponsors = SponsorSerializer(many=True, read_only=True, source='event_sponsors') @@ -36,8 +38,8 @@ class EventRegistrationSerializer(serializers.Serializer): email = serializers.EmailField() first_name = serializers.CharField(max_length=255) last_name = serializers.CharField(max_length=255) - professional_status = serializers.ChoiceField(choices=EventRegistration.PROFESSIONAL_STATUS_CHOICES) - gender = serializers.ChoiceField(choices=EventRegistration.GENDER_CHOICES) + professional_status = serializers.ChoiceField(choices=EventRegistration.ProfessionalStatus) + gender = serializers.ChoiceField(choices=EventRegistration.Gender) organization = serializers.CharField(max_length=100,required=False, allow_blank=True) description = serializers.CharField(required=False, allow_blank=True) linkedin = serializers.URLField() diff --git a/backend/djangoindia/db/admin.py b/backend/djangoindia/db/admin.py index 499538dd..d8283d29 100644 --- a/backend/djangoindia/db/admin.py +++ b/backend/djangoindia/db/admin.py @@ -12,6 +12,9 @@ from django.urls import path from django.template.response import TemplateResponse from django.contrib import messages +from django.core.exceptions import ValidationError +from django.db import transaction +from django.db.models import F, Count # Register your models here. @@ -43,6 +46,7 @@ class EventAdmin(admin.ModelAdmin): class EventRegistrationAdmin(admin.ModelAdmin): list_display = ('event', 'first_name', 'email', 'created_at') readonly_fields = ('created_at', 'updated_at') + list_filter = ('event',) search_fields=['email','event__name','first_name','last_name',] actions = [send_email_to_selected_users] @@ -53,6 +57,38 @@ def get_urls(self): ] return custom_urls + urls + @transaction.atomic + def save_model(self, request, obj, form, change): + # This is a new registration + if not change: + if obj.event.seats_left > 0: + obj.event.seats_left -= 1 + obj.event.save() + else: + raise ValidationError("No seats left for this event.") + super().save_model(request, obj, form, change) + + @transaction.atomic + def delete_model(self, request, obj): + if obj.event.seats_left < obj.event.max_seats: + obj.event.seats_left += 1 + obj.event.save() + super().delete_model(request, obj) + + @transaction.atomic + def delete_queryset(self, request, queryset): + # Group registrations by event and count them + event_counts = queryset.values('event').annotate(count=Count('id')) + + # Update seats_left for each affected event + for event_count in event_counts: + Event.objects.filter(id=event_count['event']).update( + seats_left=F('seats_left') + event_count['count'] + ) + + # Perform the actual deletion + super().delete_queryset(request, queryset) + def send_email_view(self, request): if request.method == 'POST': form = EmailForm(request.POST) diff --git a/backend/djangoindia/db/migrations/0008_event_max_seats_event_seats_left_and_more.py b/backend/djangoindia/db/migrations/0008_event_max_seats_event_seats_left_and_more.py new file mode 100644 index 00000000..2cf983e7 --- /dev/null +++ b/backend/djangoindia/db/migrations/0008_event_max_seats_event_seats_left_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.5 on 2024-09-25 16:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0007_sponsor_remove_event_event_end_date_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='max_seats', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='event', + name='seats_left', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='sponsorship', + name='amount_inr', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='event', + name='event_mode', + field=models.CharField(choices=[('in_person', 'In Person'), ('online', 'Online')], default='in_person', max_length=20), + ), + migrations.AlterField( + model_name='eventregistration', + name='gender', + field=models.CharField(choices=[('male', 'Male'), ('female', 'Female'), ('other', 'Other')], max_length=15), + ), + migrations.AlterField( + model_name='eventregistration', + name='professional_status', + field=models.CharField(choices=[('working_professional', 'Working Professional'), ('student', 'Student'), ('freelancer', 'Freelancer'), ('other', 'Other')], default='other', max_length=100), + ), + migrations.AlterField( + model_name='sponsor', + name='type', + field=models.CharField(choices=[('individual', 'Individual'), ('organization', 'Organization')], max_length=20), + ), + migrations.AlterField( + model_name='sponsorship', + name='tier', + field=models.CharField(choices=[('platinum', 'Platinum'), ('gold', 'Gold'), ('silver', 'Silver'), ('venue_sponsors', 'Venue Sponsors'), ('food_sponsors', 'Food Sponsors'), ('schwag_sponsors', 'Schwag Sponsors'), ('grant', 'Grant'), ('individual', 'Individual'), ('organization', 'Organization')], max_length=20), + ), + migrations.AlterField( + model_name='sponsorship', + name='type', + field=models.CharField(choices=[('community_sponsorship', 'Community Sponsorship'), ('event_sponsorship', 'Event Sponsorship')], max_length=30), + ), + ] diff --git a/backend/djangoindia/db/models/event.py b/backend/djangoindia/db/models/event.py index d543bab4..d13c6c0d 100644 --- a/backend/djangoindia/db/models/event.py +++ b/backend/djangoindia/db/models/event.py @@ -3,7 +3,6 @@ from .base import BaseModel from django.utils import timezone from django.core.exceptions import ValidationError -from datetime import timedelta def validate_future_date(value): if value <= timezone.now(): @@ -11,13 +10,10 @@ def validate_future_date(value): class Event(BaseModel): - IN_PERSON = "In-person" - ONLINE = "Online" - - EVENT_MODE_CHOICES = [ - (IN_PERSON, IN_PERSON), - (ONLINE, ONLINE) - ] + + class EventModes(models.TextChoices): + IN_PERSON = "in_person" + ONLINE = "online" name = models.CharField(max_length=255, unique=True) cover_image = models.ImageField(upload_to="event_images/", blank=True) @@ -28,7 +24,9 @@ class Event(BaseModel): start_date = models.DateTimeField(null=True, blank=True, validators=[validate_future_date]) end_date = models.DateTimeField(null=True, blank=True, validators=[validate_future_date]) registration_end_date = models.DateTimeField(null=True, blank=True, validators=[validate_future_date]) - event_mode = models.CharField(max_length=20,choices=EVENT_MODE_CHOICES,default=IN_PERSON) + event_mode = models.CharField(max_length=20,choices=EventModes.choices,default=EventModes.IN_PERSON) + max_seats = models.IntegerField(null=True, blank=True) + seats_left = models.IntegerField(null=True, blank=True) def clean(self): super().clean() @@ -41,26 +39,16 @@ def __str__(self) -> str: return f"{self.name} @ {self.city} ({self.start_date.date()})" class EventRegistration(BaseModel): - WORKING_PROFESSIONAL = "working_professional" - STUDENT = "student" - FREELANCER = "freelancer" - OTHER = "other" - - MALE = "male" - FEMALE = "female" - - PROFESSIONAL_STATUS_CHOICES = [ - (WORKING_PROFESSIONAL, WORKING_PROFESSIONAL), - (STUDENT, STUDENT), - (FREELANCER, FREELANCER), - (OTHER, OTHER) - ] - - GENDER_CHOICES = [ - (MALE, MALE), - (FEMALE, FEMALE), - (OTHER, OTHER) - ] + class ProfessionalStatus(models.TextChoices): + WORKING_PROFESSIONAL = "working_professional" + STUDENT = "student" + FREELANCER = "freelancer" + OTHER = "other" + + class Gender(models.TextChoices): + MALE = "male" + FEMALE = "female" + OTHER = "other" event = models.ForeignKey( "db.Event", @@ -71,13 +59,13 @@ class EventRegistration(BaseModel): first_name = models.CharField(max_length=255) last_name = models.CharField(max_length=255) professional_status = models.CharField( - max_length=100, choices=PROFESSIONAL_STATUS_CHOICES, default=OTHER + max_length=100, choices=ProfessionalStatus.choices, default=ProfessionalStatus.OTHER ) organization = models.CharField(max_length=100, null=True, blank=True) description = models.TextField(null=True, blank=True) gender = models.CharField( max_length=15, - choices=GENDER_CHOICES + choices=Gender.choices ) linkedin = models.URLField() github = models.URLField(null=True, blank=True) @@ -93,26 +81,30 @@ class Meta: ) ] + def save(self, *args, **kwargs): + # This is a new registration + if self._state.adding: + if self.event.seats_left > 0: + self.event.seats_left -= 1 + self.event.save() + else: + raise ValueError("No seats left for this event.") + super().save(*args, **kwargs) + def __str__(self) -> str: return ( f"{self.first_name} {self.last_name} ({self.email}) --- {self.event.name}" ) - - class Sponsor(BaseModel): - - INDIVIDUAL = "individual" - ORGANIZATION = "organization" - - SPONSOR_TYPE_CHOICES = [ - (INDIVIDUAL, INDIVIDUAL), - (ORGANIZATION, ORGANIZATION), - ] + + class SponsorType(models.TextChoices): + INDIVIDUAL = "individual" + ORGANIZATION = "organization" name = models.CharField(max_length=255) email = models.CharField(max_length=100) - type = models.CharField(max_length=20, choices=SPONSOR_TYPE_CHOICES) + type = models.CharField(max_length=20, choices=SponsorType.choices) logo = models.ImageField(upload_to='sponsors/logos/') url = models.URLField(max_length=500, blank=True, null=True) @@ -121,40 +113,33 @@ def __str__(self): class Sponsorship(BaseModel): + class SponsorshipTier(models.TextChoices): + PLATINUM = "platinum" + GOLD = "gold" + SILVER = "silver" + VENUE_SPONSORS = "venue_sponsors" + FOOD_SPONSORS = "food_sponsors" + SCHWAG_SPONSORS = "schwag_sponsors" + GRANT = "grant" + INDIVIDUAL = "individual" + ORGANIZATION = "organization" - PLATINUM = "platinum" - GOLD = "gold" - SILVER = "silver" - INDIVIDUAL = "individual" - ORGANIZATION = "organization" - - COMMUNITY_SPONSORSHIP = "community_sponsorship" - EVENT_SPONSORSHIP = "event_sponsorship" + class SponsorshipType(models.TextChoices): + COMMUNITY_SPONSORSHIP = "community_sponsorship" + EVENT_SPONSORSHIP = "event_sponsorship" - SPONSORSHIP_TIER_CHOICES = [ - (PLATINUM, PLATINUM), - (GOLD, GOLD), - (SILVER, SILVER), - (INDIVIDUAL, INDIVIDUAL), - (ORGANIZATION, ORGANIZATION), - ] - - SPONSORSHIP_TYPE_CHOICES = [ - (COMMUNITY_SPONSORSHIP, COMMUNITY_SPONSORSHIP), - (EVENT_SPONSORSHIP, EVENT_SPONSORSHIP), - ] - sponsor_details = models.ForeignKey(Sponsor, on_delete=models.CASCADE, related_name='sponsorships') event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='sponsors', null=True, blank=True) - tier = models.CharField(max_length=20, choices=SPONSORSHIP_TIER_CHOICES) - type = models.CharField(max_length=30, choices=SPONSORSHIP_TYPE_CHOICES) + tier = models.CharField(max_length=20, choices=SponsorshipTier.choices) + type = models.CharField(max_length=30, choices=SponsorshipType.choices) + amount_inr = models.IntegerField(null=True, blank=True) def clean(self): super().clean() - if self.type == self.COMMUNITY_SPONSORSHIP and self.tier not in [self.INDIVIDUAL, self.ORGANIZATION]: - raise ValidationError("For community sponsorship, tier must be either 'individual' or 'organization'.") - elif self.type == self.EVENT_SPONSORSHIP and self.tier not in [self.PLATINUM, self.GOLD, self.SILVER]: - raise ValidationError("For event sponsorship, tier must be either 'platinum', 'gold', or 'silver'.") + if self.type == self.SponsorshipType.COMMUNITY_SPONSORSHIP and self.tier not in [self.SponsorshipTier.INDIVIDUAL, self.SponsorshipTier.ORGANIZATION, self.SponsorshipTier.GRANT]: + raise ValidationError("For community sponsorship, tier must be 'individual', 'organization' or 'grant.") + elif self.type == self.SponsorshipType.EVENT_SPONSORSHIP and self.tier not in [self.SponsorshipTier.PLATINUM, self.SponsorshipTier.GOLD, self.SponsorshipTier.SILVER, self.SponsorshipTier.VENUE_SPONSORS, self.SponsorshipTier.FOOD_SPONSORS, self.SponsorshipTier.GRANT, self.SponsorshipTier.SCHWAG_SPONSORS]: + raise ValidationError("For event sponsorship, tier must be either 'platinum', 'gold', 'silver', 'venue_sponsors', 'food_sponsors', 'grant_sponsors' or 'schwag_sponsors'.") def __str__(self): diff --git a/backend/requirements/base.txt b/backend/requirements/base.txt index b6812d4a..c950bf3e 100644 --- a/backend/requirements/base.txt +++ b/backend/requirements/base.txt @@ -1,7 +1,7 @@ -psycopg2-binary==2.9.6 +psycopg2-binary==2.9.9 celery==5.3.4 sqlalchemy==2.0.27 -Pillow==9.5.0 +Pillow==10.4.0 Django==4.2.5 djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework