diff --git a/backend/backend_api/migrations/0048_presentation_capacity.py b/backend/backend_api/migrations/0048_presentation_capacity.py new file mode 100644 index 0000000..9b2e9f8 --- /dev/null +++ b/backend/backend_api/migrations/0048_presentation_capacity.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.4 on 2023-11-24 11:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('backend_api', '0047_remove_payment_is_done_payment_status'), + ] + + operations = [ + migrations.AddField( + model_name='presentation', + name='capacity', + field=models.PositiveIntegerField(default=50), + ), + ] diff --git a/backend/backend_api/migrations/0049_alter_payment_id.py b/backend/backend_api/migrations/0049_alter_payment_id.py new file mode 100644 index 0000000..4f58abc --- /dev/null +++ b/backend/backend_api/migrations/0049_alter_payment_id.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.4 on 2023-11-24 19:06 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('backend_api', '0048_presentation_capacity'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='id', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + ] diff --git a/backend/backend_api/models.py b/backend/backend_api/models.py index 8519973..36c5eff 100644 --- a/backend/backend_api/models.py +++ b/backend/backend_api/models.py @@ -1,4 +1,5 @@ import datetime +import uuid from urllib.parse import urljoin from django.contrib.auth.models import AbstractBaseUser @@ -9,8 +10,8 @@ from django.template.loader import render_to_string from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from rest_framework.exceptions import ValidationError from rest_framework import status -from rest_framework.response import Response from aaiss_backend import settings from aaiss_backend.settings import BASE_URL @@ -70,7 +71,7 @@ class Teacher(models.Model): bio = models.CharField(max_length=BIG_MAX_LENGTH) order = models.SmallIntegerField(default=0) year = models.IntegerField(blank=False, default=2020) - + def __str__(self): return f"Teacher with id {self.id}: {self.name}" @@ -122,15 +123,24 @@ class Workshop(models.Model): start_date = models.DateTimeField() end_date = models.DateTimeField() - def no_of_participants(self): - return len(User.objects.filter(registered_workshops=self).all()) + @property + def no_of_participants(self) -> int: + return len( + WorkshopRegistration.objects.filter(workshop=self, + status=WorkshopRegistration.StatusChoices.PURCHASED)) + + @property + def remaining_capacity(self) -> int: + return max(self.capacity - self.no_of_participants, 0) @property def participants(self): - users = [] - for user in User.objects.filter(registered_workshops=self).all(): - users.append(user) - return users + participants = [] + for participant in WorkshopRegistration.objects.filter(workshop=self, + status= + WorkshopRegistration.StatusChoices.PURCHASED): + participants += participant.user + return participants def __str__(self): name = "" @@ -145,6 +155,7 @@ class Presentation(models.Model): desc = models.CharField(max_length=BIG_MAX_LENGTH) year = models.IntegerField(blank=False, default=2020) cost = models.PositiveIntegerField(default=0) + capacity = models.PositiveIntegerField(default=50) NOT_ASSIGNED = 'NOT_ASSIGNED' ELEMENTARY = 'Elementary' @@ -166,15 +177,24 @@ class Presentation(models.Model): start_date = models.DateTimeField() end_date = models.DateTimeField() - def no_of_participants(self): - return len(User.objects.filter(registered_for_presentations=True).all()) + @property + def no_of_participants(self) -> int: + return len( + PresentationParticipation.objects.filter(presentation=self, + status=PresentationParticipation.StatusChoices.PURCHASED)) + + @property + def remaining_capacity(self) -> int: + return max(self.capacity - self.no_of_participants, 0) @property def participants(self): - users = [] - for user in User.objects.filter(registered_for_presentations=True).all(): - users.append(user) - return users + participants = [] + for participant in PresentationParticipation.objects.filter(presentation=self, + status= + PresentationParticipation.StatusChoices.PURCHASED): + participants += participant.user + return participants def __str__(self): name = "" @@ -294,7 +314,7 @@ class PaymentStatus(models.IntegerChoices): PAYMENT_CONFIRMED = 1, _('Payment confirmed') PAYMENT_REJECTED = 2, _('Payment rejected') - id = models.UUIDField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) amount = models.PositiveIntegerField() user = models.ForeignKey(User, on_delete=models.CASCADE) workshops = models.ManyToManyField(Workshop, blank=True) @@ -338,21 +358,31 @@ def create_payment_for_user(user: User): workshop_registration = workshop.workshopregistration_set.get(workshop_id=workshop.id) if workshop_registration.status != WorkshopRegistration.StatusChoices.AWAITING_PAYMENT: continue + if workshop.remaining_capacity <= 0: + raise ValidationError( + new_detailed_response(status.HTTP_400_BAD_REQUEST, + f"Workshop {workshop.id} is full")) total_cost += workshop.cost workshops.append(workshop) except ObjectDoesNotExist: raise ValueError(f"User {user} is registered for workshop {workshop} but has no registration") for presentation in user.participated_presentations.all(): try: - presentation_participation = presentation.presentationparticipation_set.get(presentation_id=presentation.id) + presentation_participation = presentation.presentationparticipation_set.get( + presentation_id=presentation.id) if presentation_participation.status != PresentationParticipation.StatusChoices.AWAITING_PAYMENT: continue + if presentation.remaining_capacity <= 0: + raise ValidationError( + new_detailed_response(status.HTTP_400_BAD_REQUEST, + f"Presentation {presentation.id} is full")) total_cost += presentation.cost presentations.append(presentation) except ObjectDoesNotExist: raise ValueError(f"User {user} is registered for presentation {presentation} but has no registration") if len(workshops) == 0 and len(presentations) == 0: - return None + raise ValidationError( + new_detailed_response(status.HTTP_400_BAD_REQUEST, f"User {user} has no unpaid registrations")) payment = Payment.objects.create(user=user, amount=total_cost, year=datetime.date.today().year, date=datetime.datetime.now()) payment.workshops.set(workshops) diff --git a/backend/backend_api/serializers.py b/backend/backend_api/serializers.py index 3f41622..83732ff 100644 --- a/backend/backend_api/serializers.py +++ b/backend/backend_api/serializers.py @@ -18,8 +18,6 @@ class Meta: FieldOfInterestSerializer = all_serializer_creator(models.FieldOfInterest) TeacherSerializer = all_serializer_creator(models.Teacher) PresenterSerializer = all_serializer_creator(models.Presenter) -WorkshopSerializer = all_serializer_creator(models.Workshop) -PresentationSerializer = all_serializer_creator(models.Presentation) MiscSerializer = all_serializer_creator(models.Misc) CommitteeSerializer = all_serializer_creator(models.Committee) StaffSerializer = all_serializer_creator(models.Staff) @@ -57,6 +55,22 @@ class AllStaffSectionSerializer(serializers.Serializer): people = serializers.ListField(child=serializers.DictField()) +class WorkshopSerializer(serializers.ModelSerializer): + remaining_capacity = serializers.IntegerField() + + class Meta: + model = models.Workshop + fields = '__all__' + + +class PresentationSerializer(serializers.ModelSerializer): + remaining_capacity = serializers.IntegerField() + + class Meta: + model = models.Presentation + fields = '__all__' + + class WorkshopRegistrationSerializer(serializers.ModelSerializer): workshop = serializers.PrimaryKeyRelatedField(queryset=WorkshopSerializer.Meta.model.objects.all()) diff --git a/backend/backend_api/views.py b/backend/backend_api/views.py index b4ef891..fc2385f 100644 --- a/backend/backend_api/views.py +++ b/backend/backend_api/views.py @@ -93,53 +93,42 @@ def retrieve(self, request, year=None, pk=None): return Response(response) -class WorkshopViewSet(viewsets.ViewSet): +class WorkshopViewSet(viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin): serializer_class = serializers.WorkshopSerializer + queryset = models.Workshop.objects.all() def list(self, request, year=None, **kwargs): if year is None: year = datetime.datetime.now().year queryset = models.Workshop.objects.filter(year=year) - serializer = self.serializer_class(queryset, many=True) - for workshop_data in serializer.data: - workshop = get_object_or_404(queryset, pk=workshop_data['id']) - workshop_data['is_full'] = ( - len(models.User.objects.filter(registered_workshops=workshop).all()) >= workshop.capacity) - return Response(serializer.data) + return super().list(request, queryset=queryset, **kwargs) def retrieve(self, request, year=None, pk=None): if year is None: year = datetime.datetime.now().year queryset = models.Workshop.objects.filter(year=year) - workshop = get_object_or_404(queryset, pk=pk) - serializer = self.serializer_class(workshop) - response = dict(serializer.data) - response['is_full'] = ( - len(models.User.objects.filter(registered_workshops=workshop).all()) >= workshop.capacity) - return Response(response) + return super().retrieve(request, pk=pk, queryset=queryset) -class PresentationViewSet(viewsets.ViewSet): +class PresentationViewSet(viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin): serializer_class = serializers.PresentationSerializer + queryset = models.Presentation.objects.all() def list(self, request, year=None, **kwargs): if year is None: year = datetime.datetime.now().year - queryset = models.Presentation.objects.filter(year=year) - serializer = self.serializer_class(queryset, many=True) - total_registered_for_presentation = len(models.User.objects.filter(registered_for_presentations=True).all()) - response = list(serializer.data) - response.append( - {'is_full': total_registered_for_presentation >= int(models.Misc.objects.get(pk='presentation_cap').desc)}) - return Response(response) + queryset = self.queryset.filter(year=year) + return super().list(request, queryset=queryset, **kwargs) def retrieve(self, request, year=None, pk=None): if year is None: year = datetime.datetime.now().year queryset = models.Presentation.objects.filter(year=year) - presentation = get_object_or_404(queryset, pk=pk) - serializer = self.serializer_class(presentation) - return Response(serializer.data) + return super().retrieve(request, pk=pk, queryset=queryset) class MiscViewSet(viewsets.ViewSet): @@ -230,11 +219,7 @@ def payment(self, request): status.HTTP_400_BAD_REQUEST, "User not found")) payment = Payment.create_payment_for_user(user) - if payment is None: - return Response(new_detailed_response( - status.HTTP_400_BAD_REQUEST, "User has no unpaid item")) - - response = ZIFYRequest().create_payment(payment.pk, payment.amount, user.name, user.phone_number, + response = ZIFYRequest().create_payment(str(payment.pk), payment.amount, user.name, user.phone_number, user.account.email) if response['status'] == ZIFY_STATUS_OK: payment.track_id = response['data']['order']