From 821149ae542bb1fb6dc9f1158558efd4c7602554 Mon Sep 17 00:00:00 2001 From: Adibov Date: Fri, 24 Nov 2023 14:44:36 +0330 Subject: [PATCH 1/2] feat: add capacity field to presentation --- .../migrations/0048_presentation_capacity.py | 18 +++++++ backend/backend_api/models.py | 51 ++++++++++++------- backend/backend_api/serializers.py | 18 ++++++- backend/backend_api/views.py | 37 +++++--------- 4 files changed, 81 insertions(+), 43 deletions(-) create mode 100644 backend/backend_api/migrations/0048_presentation_capacity.py 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/models.py b/backend/backend_api/models.py index 8519973..26c1f11 100644 --- a/backend/backend_api/models.py +++ b/backend/backend_api/models.py @@ -9,15 +9,12 @@ from django.template.loader import render_to_string from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from rest_framework import status -from rest_framework.response import Response from aaiss_backend import settings from aaiss_backend.settings import BASE_URL from backend_api import validators from backend_api.email import MailerThread from utils.random import create_random_string -from utils.renderers import new_detailed_response SMALL_MAX_LENGTH = 255 BIG_MAX_LENGTH = 65535 @@ -70,7 +67,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 +119,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(presentation=self, + status= + WorkshopRegistration.StatusChoices.PURCHASED): + participants += participant.user + return participants def __str__(self): name = "" @@ -145,6 +151,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 +173,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 = "" @@ -344,7 +360,8 @@ def create_payment_for_user(user: User): 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 total_cost += presentation.cost 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..06554f0 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): From ed6fa30b326d16f0b48b031572b8e37f8ec184fa Mon Sep 17 00:00:00 2001 From: Adibov Date: Fri, 24 Nov 2023 23:02:48 +0330 Subject: [PATCH 2/2] fix: check workshop/presentation capacity on payment --- .../migrations/0049_alter_payment_id.py | 19 +++++++++++++++++++ backend/backend_api/models.py | 19 ++++++++++++++++--- backend/backend_api/views.py | 6 +----- 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 backend/backend_api/migrations/0049_alter_payment_id.py 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 26c1f11..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,12 +10,15 @@ 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 aaiss_backend import settings from aaiss_backend.settings import BASE_URL from backend_api import validators from backend_api.email import MailerThread from utils.random import create_random_string +from utils.renderers import new_detailed_response SMALL_MAX_LENGTH = 255 BIG_MAX_LENGTH = 65535 @@ -132,7 +136,7 @@ def remaining_capacity(self) -> int: @property def participants(self): participants = [] - for participant in WorkshopRegistration.objects.filter(presentation=self, + for participant in WorkshopRegistration.objects.filter(workshop=self, status= WorkshopRegistration.StatusChoices.PURCHASED): participants += participant.user @@ -310,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) @@ -354,6 +358,10 @@ 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: @@ -364,12 +372,17 @@ def create_payment_for_user(user: User): 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/views.py b/backend/backend_api/views.py index 06554f0..fc2385f 100644 --- a/backend/backend_api/views.py +++ b/backend/backend_api/views.py @@ -219,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']