From 074fda5c8a82380682f57c726db0c796fda7c663 Mon Sep 17 00:00:00 2001 From: cphalen Date: Mon, 13 Dec 2021 01:53:32 -0500 Subject: [PATCH 01/44] Ticket model --- backend/clubs/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 64877ee8b..ad10f12e0 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1695,6 +1695,17 @@ class QuestionResponse(models.Model): response = models.TextField(blank=True) +class Ticket(models.Model): + """ + Represents a single ticket for a particular event + """ + + event = models.ForeignKey(Event, on_delete=models.DO_NOTHING) + buyer = models.ForeignKey( + get_user_model(), on_delete=models.SET_NULL, null=True, related_name="tickets" + ) + + @receiver(models.signals.pre_delete, sender=Asset) def asset_delete_cleanup(sender, instance, **kwargs): if instance.file: From 4171a610dcb762b8776c0a6a7e6f6aaa549dfb92 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sun, 16 Jan 2022 14:00:16 -0500 Subject: [PATCH 02/44] some basic untested functionality --- backend/clubs/models.py | 16 +++++-- backend/clubs/serializers.py | 18 +++++++ backend/clubs/views.py | 92 ++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/backend/clubs/models.py b/backend/clubs/models.py index ad10f12e0..d16b9655c 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -941,6 +941,10 @@ def create_thumbnail(self, request=None): def __str__(self): return self.name + @property + def tickets_count(self): + return Ticket.objects.count(event=self) + class Favorite(models.Model): """ @@ -1697,12 +1701,16 @@ class QuestionResponse(models.Model): class Ticket(models.Model): """ - Represents a single ticket for a particular event + Represents an instance of a ticket for an event """ - event = models.ForeignKey(Event, on_delete=models.DO_NOTHING) - buyer = models.ForeignKey( - get_user_model(), on_delete=models.SET_NULL, null=True, related_name="tickets" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + event = models.ForeignKey( + Event, related_name="tickets", on_delete=models.DO_NOTHING + ) + type = models.CharField(max_length=100) + owner = models.ForeignKey( + get_user_model(), related_name="tickets", on_delete=models.SET_NULL, blank=True ) diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index 898fa3633..e2f7efafe 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -54,6 +54,7 @@ TargetStudentType, TargetYear, Testimonial, + Ticket, Year, ) from clubs.utils import clean @@ -1721,6 +1722,23 @@ class Meta: fields = ("club", "role", "title", "active", "public") +class TicketSerializer(serializers.ModelSerializer): + + """ + Used to return a ticket object + """ + + owner = serializers.SerializerMethodField("get_owner_name") + event = EventSerializer(source="obj.event") + + def get_owner_name(self, obj): + return obj.owner.get_full_name() + + class Meta: + model = Ticket + fields = ("id", "event", "owner") + + class UserUUIDSerializer(serializers.ModelSerializer): """ Used to get the uuid of a user (for ICS Calendar export) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 2bf170b26..d95939f0f 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -91,6 +91,7 @@ Subscribe, Tag, Testimonial, + Ticket, Year, ZoomMeetingVisit, get_mail_type_annotation, @@ -157,6 +158,7 @@ SubscribeSerializer, TagSerializer, TestimonialSerializer, + TicketSerializer, UserClubVisitSerializer, UserClubVisitWriteSerializer, UserMembershipInviteSerializer, @@ -4206,6 +4208,96 @@ def get_object(self): return user +class TicketViewSet(viewsets.ModelViewSet): + """ + create: + Create tickets for an event + + buy: + Buy one ticket + """ + + permission_classes = [IsAuthenticated] + serializer_class = TicketSerializer + http_method_names = ["get", "post"] + + def create(self, request, *args, **kwargs): + """ + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + event: + type: integer + quantities: + type: array + items: + type: object + properties: + type: + type: string + count: + type: integer + responses: + "200": + content: + application/json: + schema: + type: object + properties: + detail: + type: string + description: A success or error message. + """ + name = request.data.get("name") + event = request.data.get("event") + quantities = request.data.get("quantities") + + for type, count in quantities.items(): + for _ in range(count): + Ticket.objects.create(name=name, event=event, type=type) + + @action(detail=False, methods=["get"]) + def buy(self, request, *args, **kwargs): + """ + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + event: + type: integer + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Ticket" + --- + """ + + # Get first unsold ticket of type + ticket = Ticket.objects.filter( + event=request.data.get("event"), + type=request.data.get("type"), + user__isnull=True, + ).first() + ticket.owner = request.user + ticket.save() + return Response(TicketSerializer(ticket).data) + + def get_queryset(self): + return Ticket.objects.filter(user=self.request.user) + + class MemberInviteViewSet(viewsets.ModelViewSet): """ update: From ef872be68d7924e66d4774faf9db85c2063b8814 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sun, 16 Jan 2022 17:17:26 -0500 Subject: [PATCH 03/44] tested routes, added view ticket count route --- backend/clubs/admin.py | 2 + backend/clubs/migrations/0091_ticket.py | 26 +++++++ backend/clubs/models.py | 6 +- backend/clubs/serializers.py | 4 +- backend/clubs/urls.py | 2 + backend/clubs/views.py | 99 +++++++++++++++++++++---- 6 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 backend/clubs/migrations/0091_ticket.py diff --git a/backend/clubs/admin.py b/backend/clubs/admin.py index f99652bf3..3abbef4a8 100644 --- a/backend/clubs/admin.py +++ b/backend/clubs/admin.py @@ -48,6 +48,7 @@ TargetStudentType, TargetYear, Testimonial, + Ticket, Year, ZoomMeetingVisit, ) @@ -443,3 +444,4 @@ class ZoomMeetingVisitAdmin(admin.ModelAdmin): admin.site.register(Year, YearAdmin) admin.site.register(ZoomMeetingVisit, ZoomMeetingVisitAdmin) admin.site.register(AdminNote) +admin.site.register(Ticket) \ No newline at end of file diff --git a/backend/clubs/migrations/0091_ticket.py b/backend/clubs/migrations/0091_ticket.py new file mode 100644 index 000000000..db569b6fb --- /dev/null +++ b/backend/clubs/migrations/0091_ticket.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.8 on 2022-01-16 20:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('clubs', '0090_adminnote'), + ] + + operations = [ + migrations.CreateModel( + name='Ticket', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('type', models.CharField(max_length=100)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tickets', to='clubs.event')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index d16b9655c..41773763e 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1710,7 +1710,11 @@ class Ticket(models.Model): ) type = models.CharField(max_length=100) owner = models.ForeignKey( - get_user_model(), related_name="tickets", on_delete=models.SET_NULL, blank=True + get_user_model(), + related_name="tickets", + on_delete=models.SET_NULL, + blank=True, + null=True, ) diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index e2f7efafe..3e6ef79a6 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -1729,10 +1729,10 @@ class TicketSerializer(serializers.ModelSerializer): """ owner = serializers.SerializerMethodField("get_owner_name") - event = EventSerializer(source="obj.event") + event = EventSerializer() def get_owner_name(self, obj): - return obj.owner.get_full_name() + return obj.owner.get_full_name() if obj.owner else "None" class Meta: model = Ticket diff --git a/backend/clubs/urls.py b/backend/clubs/urls.py index 6c6fbbc85..1a740ccb2 100644 --- a/backend/clubs/urls.py +++ b/backend/clubs/urls.py @@ -42,6 +42,7 @@ SubscribeViewSet, TagViewSet, TestimonialViewSet, + TicketViewSet, UserGroupAPIView, UserPermissionAPIView, UserUpdateAPIView, @@ -67,6 +68,7 @@ router.register(r"searches", SearchQueryViewSet, basename="searches") router.register(r"memberships", MembershipViewSet, basename="members") router.register(r"requests", MembershipRequestViewSet, basename="requests") +router.register(r"tickets", TicketViewSet, basename="tickets") router.register(r"schools", SchoolViewSet, basename="schools") router.register(r"majors", MajorViewSet, basename="majors") diff --git a/backend/clubs/views.py b/backend/clubs/views.py index d95939f0f..7be2b041e 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2218,6 +2218,54 @@ def get_serializer_class(self): return EventWriteSerializer return EventSerializer + @action(detail=True, methods=["get"]) + def tickets(self, request, *args, **kwargs): + """ + Get information about tickets for particular event + ___ + requestBody: {} + responses: + "200": + content: + application/json + schema: + type: object + properties: + totals: + type: array + items: + type: object + properties: + type: + type: string + count: + type: integer + available: + type: array + items: + type: object + properties: + type: + type: string + count: + type: integer + --- + + """ + + tickets = Ticket.objects.filter(event=kwargs["id"]) + types = tickets.values_list("type", flat=True).distinct() + totals = [] + available = [] + + for type in types: + totals.append({"type": type, "count": tickets.filter(type=type).count()}) + available.append({"type": type, "count": ( + tickets.filter(type=type, owner__isnull=True).count() + )}) + + return Response({"totals": totals, "available": available}) + @action(detail=True, methods=["post"]) def upload(self, request, *args, **kwargs): """ @@ -4215,6 +4263,9 @@ class TicketViewSet(viewsets.ModelViewSet): buy: Buy one ticket + + list: + List all tickets owned by user """ permission_classes = [IsAuthenticated] @@ -4253,15 +4304,21 @@ def create(self, request, *args, **kwargs): type: string description: A success or error message. """ - name = request.data.get("name") - event = request.data.get("event") + event = get_object_or_404(Event, id=request.data.get("event")) quantities = request.data.get("quantities") + membership = find_membership_helper(request.user, event.club) - for type, count in quantities.items(): - for _ in range(count): - Ticket.objects.create(name=name, event=event, type=type) + if membership.role <= 10: # Create tickets allowed if officer+ + for item in quantities: + for _ in range(item["count"]): + Ticket.objects.create(event=event, type=item["type"]) + return Response([]) + else: + return Response( + {"detail": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED + ) - @action(detail=False, methods=["get"]) + @action(detail=False, methods=["post"]) def buy(self, request, *args, **kwargs): """ requestBody: @@ -4283,19 +4340,33 @@ def buy(self, request, *args, **kwargs): - $ref: "#/components/schemas/Ticket" --- """ + type = request.data.get("type") + event = request.data.get("event") + + # If ticket already owned, do nothing + if Ticket.objects.filter(event=event, owner=request.user.id).first(): + return Response( + {"detail": "Ticket to event already owned by user"}, + status=status.HTTP_400_BAD_REQUEST, + ) - # Get first unsold ticket of type + # Otherwise get first unowned ticket of requested type ticket = Ticket.objects.filter( - event=request.data.get("event"), - type=request.data.get("type"), - user__isnull=True, + event=request.data.get("event"), type=type, owner__isnull=True, ).first() - ticket.owner = request.user - ticket.save() - return Response(TicketSerializer(ticket).data) + + if ticket: + ticket.owner = request.user + ticket.save() + return Response(TicketSerializer(ticket).data) + else: + return Response( + {"detail": f"No tickets of type {type} left!"}, + status=status.HTTP_403_FORBIDDEN, + ) def get_queryset(self): - return Ticket.objects.filter(user=self.request.user) + return Ticket.objects.filter(owner=self.request.user.id) class MemberInviteViewSet(viewsets.ModelViewSet): From c55fd75bc869473ee9748d8badfd6378df4e26fb Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sun, 16 Jan 2022 17:29:31 -0500 Subject: [PATCH 04/44] lint --- backend/clubs/admin.py | 2 +- backend/clubs/migrations/0091_ticket.py | 41 ++++++++++++++++----- backend/clubs/views.py | 48 +++++++++++++------------ 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/backend/clubs/admin.py b/backend/clubs/admin.py index 3abbef4a8..5a0f0acb5 100644 --- a/backend/clubs/admin.py +++ b/backend/clubs/admin.py @@ -444,4 +444,4 @@ class ZoomMeetingVisitAdmin(admin.ModelAdmin): admin.site.register(Year, YearAdmin) admin.site.register(ZoomMeetingVisit, ZoomMeetingVisitAdmin) admin.site.register(AdminNote) -admin.site.register(Ticket) \ No newline at end of file +admin.site.register(Ticket) diff --git a/backend/clubs/migrations/0091_ticket.py b/backend/clubs/migrations/0091_ticket.py index db569b6fb..a76becfee 100644 --- a/backend/clubs/migrations/0091_ticket.py +++ b/backend/clubs/migrations/0091_ticket.py @@ -1,26 +1,51 @@ # Generated by Django 3.2.8 on 2022-01-16 20:59 +import uuid + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import uuid class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('clubs', '0090_adminnote'), + ("clubs", "0090_adminnote"), ] operations = [ migrations.CreateModel( - name='Ticket', + name="Ticket", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('type', models.CharField(max_length=100)), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tickets', to='clubs.event')), - ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("type", models.CharField(max_length=100)), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="tickets", + to="clubs.event", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tickets", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 7be2b041e..56a30b9c7 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2222,35 +2222,34 @@ def get_serializer_class(self): def tickets(self, request, *args, **kwargs): """ Get information about tickets for particular event - ___ + --- requestBody: {} responses: "200": content: - application/json + application/json: schema: type: object properties: totals: type: array - items: - type: object - properties: - type: - type: string - count: - type: integer + items: + type: object + properties: + type: + type: string + count: + type: integer available: type: array - items: - type: object - properties: - type: - type: string - count: - type: integer + items: + type: object + properties: + type: + type: string + count: + type: integer --- - """ tickets = Ticket.objects.filter(event=kwargs["id"]) @@ -2260,9 +2259,12 @@ def tickets(self, request, *args, **kwargs): for type in types: totals.append({"type": type, "count": tickets.filter(type=type).count()}) - available.append({"type": type, "count": ( - tickets.filter(type=type, owner__isnull=True).count() - )}) + available.append( + { + "type": type, + "count": (tickets.filter(type=type, owner__isnull=True).count()), + } + ) return Response({"totals": totals, "available": available}) @@ -4321,6 +4323,8 @@ def create(self, request, *args, **kwargs): @action(detail=False, methods=["post"]) def buy(self, request, *args, **kwargs): """ + Buy a ticket + --- requestBody: content: application/json: @@ -4336,8 +4340,8 @@ def buy(self, request, *args, **kwargs): content: application/json: schema: - allOf: - - $ref: "#/components/schemas/Ticket" + allOf: + - $ref: "#/components/schemas/Ticket" --- """ type = request.data.get("type") From 2d0db0d4cab7fcbbc2e88ae4331ce5e4b2793115 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sat, 12 Nov 2022 14:57:40 -0500 Subject: [PATCH 05/44] email confirmation, qrcode, retrieve ticket --- backend/clubs/models.py | 50 +++- backend/clubs/serializers.py | 12 +- backend/clubs/views.py | 268 +++++++++++------- .../templates/emails/ticket_confirmation.html | 49 ++++ 4 files changed, 272 insertions(+), 107 deletions(-) create mode 100644 backend/templates/emails/ticket_confirmation.html diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 41773763e..5a8091dae 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1,11 +1,14 @@ +import base64 import datetime import os import re import uuid import warnings +from io import BytesIO from urllib.parse import urlparse import pytz +import qrcode import requests import yaml from django.conf import settings @@ -1571,7 +1574,9 @@ class ApplicationCommittee(models.Model): name = models.TextField(blank=True) application = models.ForeignKey( - ClubApplication, related_name="committees", on_delete=models.CASCADE, + ClubApplication, + related_name="committees", + on_delete=models.CASCADE, ) def get_word_limit(self): @@ -1623,7 +1628,9 @@ class ApplicationMultipleChoice(models.Model): value = models.TextField(blank=True) question = models.ForeignKey( - ApplicationQuestion, related_name="multiple_choice", on_delete=models.CASCADE, + ApplicationQuestion, + related_name="multiple_choice", + on_delete=models.CASCADE, ) @@ -1717,6 +1724,45 @@ class Ticket(models.Model): null=True, ) + def get_qr(self): + """ + Return a QR code image linking to the ticket page + """ + if not self.owner: + return None + + url = f"https://{settings.DOMAIN}/api/tickets/{self.id}" + qr_image = qrcode.make(url, box_size=20, border=0) + return qr_image + + def send_confirmation_email(self): + """ + Send a confirmation email to the ticket owner after purchase + """ + owner = self.owner + + output = BytesIO() + qr_image = self.get_qr() + qr_image.save(output, format="PNG") + decoded_image = base64.b64encode(output.getvalue()).decode("ascii") + + context = { + "first_name": self.owner.first_name, + "name": self.event.name, + "type": self.type, + "start_time": self.event.start_time, + "end_time": self.event.end_time, + "qr": decoded_image, + } + + if self.owner.email: + send_mail_helper( + name="ticket_confirmation", + subject=f"Ticket confirmation for {owner.get_full_name()}", + emails=[owner.email], + context=context, + ) + @receiver(models.signals.pre_delete, sender=Asset) def asset_delete_cleanup(sender, instance, **kwargs): diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index 3e6ef79a6..69ec913ad 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -1736,7 +1736,7 @@ def get_owner_name(self, obj): class Meta: model = Ticket - fields = ("id", "event", "owner") + fields = ("id", "event", "type", "owner") class UserUUIDSerializer(serializers.ModelSerializer): @@ -2007,7 +2007,9 @@ def get_clubs(self, obj): # hide non public memberships if not superuser if user is None or not user.has_perm("clubs.manage_club"): queryset = queryset.filter( - membership__person=obj, membership__public=True, approved=True, + membership__person=obj, + membership__public=True, + approved=True, ) serializer = MembershipClubListSerializer( @@ -2408,7 +2410,8 @@ def save(self): ApplicationMultipleChoice.objects.filter(question=question_obj).delete() for choice in multiple_choice: ApplicationMultipleChoice.objects.create( - value=choice["value"], question=question_obj, + value=choice["value"], + question=question_obj, ) # manually create committee choices as Django does not @@ -2697,7 +2700,8 @@ def save(self): for name in committees: if name not in prev_committee_names: ApplicationCommittee.objects.create( - name=name, application=application_obj, + name=name, + application=application_obj, ) return application_obj diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 56a30b9c7..9cc11866b 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -1072,7 +1072,8 @@ def get_queryset(self): self.request.user, get_user_model() ): SearchQuery( - person=self.request.user, query=self.request.query_params.get("search"), + person=self.request.user, + query=self.request.query_params.get("search"), ).save() # select subset of clubs if requested @@ -1665,7 +1666,9 @@ def constitutions(self, request, *args, **kwargs): query = ( Club.objects.filter(badges=badge, archived=False) .order_by(Lower("name")) - .prefetch_related(Prefetch("asset_set", to_attr="prefetch_asset_set"),) + .prefetch_related( + Prefetch("asset_set", to_attr="prefetch_asset_set"), + ) ) if request.user.is_authenticated: query = query.prefetch_related( @@ -2198,6 +2201,12 @@ class ClubEventViewSet(viewsets.ModelViewSet): destroy: Delete an event. + + tickets: + Get or create tickets for particular event + + buy: + Buy a ticket for an event """ permission_classes = [EventPermission | IsSuperuser] @@ -2218,6 +2227,56 @@ def get_serializer_class(self): return EventWriteSerializer return EventSerializer + @action(detail=True, methods=["post"]) + def buy(self, request, *args, **kwargs): + """ + Buy a ticket + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Ticket" + --- + """ + type = request.data.get("type") + event = self.get_object() + + # If ticket already owned, do nothing + if Ticket.objects.filter(event=event, owner=request.user.id).first(): + return Response( + {"detail": "Ticket to event already owned by user"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Otherwise get first unowned ticket of requested type + ticket = Ticket.objects.filter( + event=event, + type=type, + owner__isnull=True, + ).first() + + if ticket: + ticket.owner = request.user + ticket.save() + ticket.send_confirmation_email() + return Response(TicketSerializer(ticket).data) + else: + return Response( + {"detail": f"No tickets of type {type} left!"}, + status=status.HTTP_403_FORBIDDEN, + ) + @action(detail=True, methods=["get"]) def tickets(self, request, *args, **kwargs): """ @@ -2231,6 +2290,17 @@ def tickets(self, request, *args, **kwargs): schema: type: object properties: + buyers: + type: array + items: + type: object + properties: + fullname: + type: string + id: + type: string + type: + type: string totals: type: array items: @@ -2251,8 +2321,10 @@ def tickets(self, request, *args, **kwargs): type: integer --- """ - - tickets = Ticket.objects.filter(event=kwargs["id"]) + event = self.get_object() + tickets = Ticket.objects.filter(event=event).annotate( + fullname=Concat("owner__first_name", Value(" "), "owner__last_name") + ) types = tickets.values_list("type", flat=True).distinct() totals = [] available = [] @@ -2266,7 +2338,57 @@ def tickets(self, request, *args, **kwargs): } ) - return Response({"totals": totals, "available": available}) + buyers = tickets.filter(owner__isnull=False).values("id", "fullname", "type") + + return Response({"totals": totals, "available": available, "buyers": buyers}) + + @tickets.mapping.put + def create_tickets(self, request, *args, **kwargs): + """ + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + quantities: + type: array + items: + type: object + properties: + type: + type: string + count: + type: integer + responses: + "200": + content: + application/json: + schema: + type: object + properties: + detail: + type: string + description: A success or error message. + """ + event = self.get_object() + quantities = request.data.get("quantities") + membership = find_membership_helper(request.user, event.club) + + if membership.role <= 10: # Create tickets allowed if officer+ + + Ticket.objects.filter(event=event).delete() # Idempotency + + for item in quantities: + for _ in range(item["count"]): + Ticket.objects.create(event=event, type=item["type"]) + return Response([]) + else: + return Response( + {"detail": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED + ) @action(detail=True, methods=["post"]) def upload(self, request, *args, **kwargs): @@ -3008,7 +3130,9 @@ def destroy(self, request, *args, **kwargs): def get_queryset(self): return MembershipRequest.objects.filter( - person=self.request.user, withdrew=False, club__archived=False, + person=self.request.user, + withdrew=False, + club__archived=False, ) @@ -4132,7 +4256,9 @@ def get(self, request): try: response = zoom_api_call( - request.user, "GET", "https://api.zoom.us/v2/users/{uid}/settings", + request.user, + "GET", + "https://api.zoom.us/v2/users/{uid}/settings", ) except requests.exceptions.HTTPError as e: raise DRFValidationError( @@ -4253,125 +4379,61 @@ def get_operation_id(self, **kwargs): def get_object(self): user = self.request.user prefetch_related_objects( - [user], "profile__school", "profile__major", + [user], + "profile__school", + "profile__major", ) return user class TicketViewSet(viewsets.ModelViewSet): """ - create: - Create tickets for an event - - buy: - Buy one ticket - list: List all tickets owned by user + + retrieve: + Retrieve an individual ticket's data + + qr: + Get a ticket's QR code """ permission_classes = [IsAuthenticated] serializer_class = TicketSerializer http_method_names = ["get", "post"] + lookup_field = "id" - def create(self, request, *args, **kwargs): - """ - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - type: string - event: - type: integer - quantities: - type: array - items: - type: object - properties: - type: - type: string - count: - type: integer - responses: - "200": - content: - application/json: - schema: - type: object - properties: - detail: - type: string - description: A success or error message. - """ - event = get_object_or_404(Event, id=request.data.get("event")) - quantities = request.data.get("quantities") - membership = find_membership_helper(request.user, event.club) - - if membership.role <= 10: # Create tickets allowed if officer+ - for item in quantities: - for _ in range(item["count"]): - Ticket.objects.create(event=event, type=item["type"]) - return Response([]) - else: - return Response( - {"detail": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED - ) + def retrieve(self, request, *args, **kwargs): + return Response(TicketSerializer(Ticket.objects.get(id=kwargs["id"])).data) - @action(detail=False, methods=["post"]) - def buy(self, request, *args, **kwargs): + @action(detail=True, methods=["get"]) + def qr(self, request, *args, **kwargs): """ - Buy a ticket + Return a QR code png image representing a link to the ticket. --- - requestBody: - content: - application/json: - schema: - type: object - properties: - type: - type: string - event: - type: integer + operationId: Generate QR Code for ticket responses: "200": + description: Return a png image representing a QR code to the ticket. content: - application/json: + image/png: schema: - allOf: - - $ref: "#/components/schemas/Ticket" + type: binary --- """ - type = request.data.get("type") - event = request.data.get("event") - - # If ticket already owned, do nothing - if Ticket.objects.filter(event=event, owner=request.user.id).first(): - return Response( - {"detail": "Ticket to event already owned by user"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Otherwise get first unowned ticket of requested type - ticket = Ticket.objects.filter( - event=request.data.get("event"), type=type, owner__isnull=True, - ).first() - - if ticket: - ticket.owner = request.user - ticket.save() - return Response(TicketSerializer(ticket).data) - else: - return Response( - {"detail": f"No tickets of type {type} left!"}, - status=status.HTTP_403_FORBIDDEN, - ) + ticket = self.get_object() + qr_image = ticket.get_qr() + response = HttpResponse(content_type="image/png") + qr_image.save(response, "PNG") + return response def get_queryset(self): return Ticket.objects.filter(owner=self.request.user.id) + # def get_object(self, request, pk=None): + # print(pk) + # return Ticket.objects.get(pk=pk) + class MemberInviteViewSet(viewsets.ModelViewSet): """ @@ -4591,7 +4653,9 @@ def question_response(self, *args, **kwargs): } ) submission = ApplicationSubmission.objects.create( - user=self.request.user, application=application, committee=committee, + user=self.request.user, + application=application, + committee=committee, ) for question_pk in questions: question = ApplicationQuestion.objects.filter(pk=question_pk).first() @@ -4612,7 +4676,9 @@ def question_response(self, *args, **kwargs): text = question_data.get("text", None) if text is not None and text != "": obj = ApplicationQuestionResponse.objects.create( - text=text, question=question, submission=submission, + text=text, + question=question, + submission=submission, ).save() response = Response(ApplicationQuestionResponseSerializer(obj).data) elif question_type == ApplicationQuestion.MULTIPLE_CHOICE: diff --git a/backend/templates/emails/ticket_confirmation.html b/backend/templates/emails/ticket_confirmation.html new file mode 100644 index 000000000..7b2991e64 --- /dev/null +++ b/backend/templates/emails/ticket_confirmation.html @@ -0,0 +1,49 @@ + + +{% extends 'emails/base.html' %} + +{% block content %} +

Thanks for using Penn Clubs!

+ +

+ {{ first_name }}, thank you for your recent purchase of a ticket to {{ name }} with ticket type {{type }}. +

+ +

+ As a reminder, the event starts at {{ start_time }} and ends at {{ end_time }}. + + +

+ +

+ Please be 10 minutes early for a smooth seating experience. +

+ +

Below is a + QR code for + your confirmation.

+ + + + +

Note: all tickets issued by us are non-refundable and non-transferable.

+ + + +

+ If you have any questions, feel free to respond to this email. +

+{% endblock %} \ No newline at end of file From 7240a1126d6b739aea224e19304e6a6d9979fca1 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Mon, 17 Jan 2022 17:14:07 -0500 Subject: [PATCH 06/44] remove commented stuff --- backend/clubs/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 9cc11866b..194e54e48 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -4430,10 +4430,6 @@ def qr(self, request, *args, **kwargs): def get_queryset(self): return Ticket.objects.filter(owner=self.request.user.id) - # def get_object(self, request, pk=None): - # print(pk) - # return Ticket.objects.get(pk=pk) - class MemberInviteViewSet(viewsets.ModelViewSet): """ From ae9944648c269aac0fb75d2423376546b8edc905 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sat, 12 Nov 2022 14:58:58 -0500 Subject: [PATCH 07/44] created cart model and add to cart, validate cart, and checkout from cart views --- backend/clubs/admin.py | 2 + .../migrations/0092_auto_20220211_1732.py | 55 ++++++++ backend/clubs/models.py | 13 ++ backend/clubs/views.py | 119 +++++++++++++++++- 4 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 backend/clubs/migrations/0092_auto_20220211_1732.py diff --git a/backend/clubs/admin.py b/backend/clubs/admin.py index 5a0f0acb5..9dcd1c7c5 100644 --- a/backend/clubs/admin.py +++ b/backend/clubs/admin.py @@ -20,6 +20,7 @@ ApplicationSubmission, Asset, Badge, + Cart, Club, ClubApplication, ClubFair, @@ -445,3 +446,4 @@ class ZoomMeetingVisitAdmin(admin.ModelAdmin): admin.site.register(ZoomMeetingVisit, ZoomMeetingVisitAdmin) admin.site.register(AdminNote) admin.site.register(Ticket) +admin.site.register(Cart) diff --git a/backend/clubs/migrations/0092_auto_20220211_1732.py b/backend/clubs/migrations/0092_auto_20220211_1732.py new file mode 100644 index 000000000..ee9adba59 --- /dev/null +++ b/backend/clubs/migrations/0092_auto_20220211_1732.py @@ -0,0 +1,55 @@ +# Generated by Django 3.2.8 on 2022-02-11 22:32 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("clubs", "0091_ticket"), + ] + + operations = [ + migrations.AddField( + model_name="ticket", + name="held", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="ticket", + name="holding_expiration", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.CreateModel( + name="Cart", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cart", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddField( + model_name="ticket", + name="carts", + field=models.ManyToManyField( + blank=True, related_name="tickets", to="clubs.Cart" + ), + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 5a8091dae..aa6183c12 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1706,6 +1706,16 @@ class QuestionResponse(models.Model): response = models.TextField(blank=True) +class Cart(models.Model): + """ + Represents an instance of a ticket cart for a user + """ + + owner = models.ForeignKey( + get_user_model(), related_name="cart", on_delete=models.CASCADE + ) + + class Ticket(models.Model): """ Represents an instance of a ticket for an event @@ -1723,6 +1733,9 @@ class Ticket(models.Model): blank=True, null=True, ) + held = models.BooleanField(default=False) + holding_expiration = models.DateTimeField(null=True, blank=True) + carts = models.ManyToManyField(Cart, related_name="tickets", blank=True) def get_qr(self): """ diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 194e54e48..e5662dfc4 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -69,6 +69,7 @@ ApplicationSubmission, Asset, Badge, + Cart, Club, ClubApplication, ClubFair, @@ -2227,6 +2228,116 @@ def get_serializer_class(self): return EventWriteSerializer return EventSerializer + @action(detail=True, methods=["post"]) + def cart(self, request, *args, **kwargs): + """ + Add a certain number of tickets to cart + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + count: + type: integer + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Ticket" + --- + """ + type = request.data.get("type") + count = request.data.get("count") + event = Event.objects.get(id=self.get_object().id) + cart = Cart.objects.filter(owner=self.request.user).first() + + # Try to get count unowned ticket of requested type + tickets = Ticket.objects.filter( + event=event, type=type, owner__isnull=True, held=False + ).exclude(carts__owner=self.request.user) + print(tickets.count()) + if tickets.count() < count: + return Response( + {"detail": f"Not enough tickets of type {type} left!"}, + status=status.HTTP_403_FORBIDDEN, + ) + else: + for ticket in tickets[:count]: + ticket.carts.add(cart) + ticket.save() + return Response({"detail": "Successfully added to cart"}) + + @action(detail=True, methods=["post"]) + def validate_cart(self, request, *args, **kwargs): + """ + Validate tickets in a cart + --- + requestBody: + content: + application/json: + schema: + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Ticket" + --- + """ + cart = Cart.objects.filter(owner=self.request.user).first() + + for ticket in cart.tickets.all(): + if ticket.owner or ticket.held: + new_ticket = Ticket.objects.filter( + event=ticket.event, type=ticket.type, owner__isnull=True, held=False + ).first() + cart.tickets.remove(ticket) + if new_ticket: + cart.tickets.add(new_ticket) + return Response({"detail": "Cart validated"}) + + @action(detail=True, methods=["post"]) + def checkout(self, request, *args, **kwargs): + """ + Checkout all tickets in cart, assumes all tickets are unowned and unheld + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Ticket" + --- + """ + cart = self.request.user.cart + + # Before Stripe call, need to implement 10 minute hold here + for ticket in cart.tickets: + ticket.held = True + + # If Stripe call succeeds + for ticket in cart.tickets: + ticket.owner = request.user + ticket.save() + ticket.send_confirmation_email() + return Response(TicketSerializer(ticket).data) + @action(detail=True, methods=["post"]) def buy(self, request, *args, **kwargs): """ @@ -2261,12 +2372,14 @@ def buy(self, request, *args, **kwargs): # Otherwise get first unowned ticket of requested type ticket = Ticket.objects.filter( - event=event, - type=type, - owner__isnull=True, + event=event, type=type, owner__isnull=True, held=False ).first() + # Stripe call here + + # If Stripe call succeeds if ticket: + ticket.held = True ticket.owner = request.user ticket.save() ticket.send_confirmation_email() From 0a975fb49e8053039ebdb23b6cb2d5cea78739e2 Mon Sep 17 00:00:00 2001 From: dfeng678 Date: Sun, 20 Feb 2022 13:28:15 -0500 Subject: [PATCH 08/44] added holding expiration functionality, holding is initiated at checkout and updated when any cart is validated before checkout --- backend/clubs/views.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index e5662dfc4..0a369fe51 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2261,7 +2261,6 @@ def cart(self, request, *args, **kwargs): tickets = Ticket.objects.filter( event=event, type=type, owner__isnull=True, held=False ).exclude(carts__owner=self.request.user) - print(tickets.count()) if tickets.count() < count: return Response( {"detail": f"Not enough tickets of type {type} left!"}, @@ -2291,8 +2290,16 @@ def validate_cart(self, request, *args, **kwargs): - $ref: "#/components/schemas/Ticket" --- """ + # Update holding expirations for all tickets + for ticket in Ticket.objects.all(): + if ticket.holding_expiration: + if ticket.holding_expiration <= timezone.now(): + ticket.held = False + ticket.save() + cart = Cart.objects.filter(owner=self.request.user).first() + # Checks every ticket in cart, if held or owned, tries to replace with another ticket of the same type for ticket in cart.tickets.all(): if ticket.owner or ticket.held: new_ticket = Ticket.objects.filter( @@ -2301,6 +2308,7 @@ def validate_cart(self, request, *args, **kwargs): cart.tickets.remove(ticket) if new_ticket: cart.tickets.add(new_ticket) + cart.save() return Response({"detail": "Cart validated"}) @action(detail=True, methods=["post"]) @@ -2312,10 +2320,6 @@ def checkout(self, request, *args, **kwargs): content: application/json: schema: - type: object - properties: - type: - type: string responses: "200": content: @@ -2325,18 +2329,20 @@ def checkout(self, request, *args, **kwargs): - $ref: "#/components/schemas/Ticket" --- """ - cart = self.request.user.cart + cart = Cart.objects.filter(owner=self.request.user).first() - # Before Stripe call, need to implement 10 minute hold here - for ticket in cart.tickets: + for ticket in cart.tickets.all(): ticket.held = True + ticket.holding_expiration = timezone.now() + datetime.timedelta(minutes = 1) + ticket.save() - # If Stripe call succeeds - for ticket in cart.tickets: + # Should only run if Stripe call succeeds + for ticket in cart.tickets.all(): ticket.owner = request.user + ticket.carts.remove(cart) ticket.save() - ticket.send_confirmation_email() - return Response(TicketSerializer(ticket).data) + # ticket.send_confirmation_email() + return Response({"detail": "Successfully checked out!"}) @action(detail=True, methods=["post"]) def buy(self, request, *args, **kwargs): From 7fe194dfb339823f45fc2f0b0eb99e95a03526dc Mon Sep 17 00:00:00 2001 From: dfeng678 Date: Sun, 20 Feb 2022 13:45:52 -0500 Subject: [PATCH 09/44] lint and tests --- backend/clubs/views.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 0a369fe51..bd98225fa 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2228,7 +2228,7 @@ def get_serializer_class(self): return EventWriteSerializer return EventSerializer - @action(detail=True, methods=["post"]) + @action(detail=False, methods=["post"]) def cart(self, request, *args, **kwargs): """ Add a certain number of tickets to cart @@ -2272,7 +2272,7 @@ def cart(self, request, *args, **kwargs): ticket.save() return Response({"detail": "Successfully added to cart"}) - @action(detail=True, methods=["post"]) + @action(detail=False, methods=["post"]) def validate_cart(self, request, *args, **kwargs): """ Validate tickets in a cart @@ -2296,10 +2296,9 @@ def validate_cart(self, request, *args, **kwargs): if ticket.holding_expiration <= timezone.now(): ticket.held = False ticket.save() - + cart = Cart.objects.filter(owner=self.request.user).first() - # Checks every ticket in cart, if held or owned, tries to replace with another ticket of the same type for ticket in cart.tickets.all(): if ticket.owner or ticket.held: new_ticket = Ticket.objects.filter( @@ -2311,7 +2310,7 @@ def validate_cart(self, request, *args, **kwargs): cart.save() return Response({"detail": "Cart validated"}) - @action(detail=True, methods=["post"]) + @action(detail=False, methods=["post"]) def checkout(self, request, *args, **kwargs): """ Checkout all tickets in cart, assumes all tickets are unowned and unheld @@ -2333,7 +2332,7 @@ def checkout(self, request, *args, **kwargs): for ticket in cart.tickets.all(): ticket.held = True - ticket.holding_expiration = timezone.now() + datetime.timedelta(minutes = 1) + ticket.holding_expiration = timezone.now() + datetime.timedelta(minutes=10) ticket.save() # Should only run if Stripe call succeeds From ac0975a73ff1884393c46b8d13b7e985c1f77502 Mon Sep 17 00:00:00 2001 From: dfeng678 Date: Sun, 20 Feb 2022 13:55:32 -0500 Subject: [PATCH 10/44] added cart creation for a user upon adding to a cart if a cart does not yet exist --- backend/clubs/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index bd98225fa..7dd086c38 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2228,7 +2228,7 @@ def get_serializer_class(self): return EventWriteSerializer return EventSerializer - @action(detail=False, methods=["post"]) + @action(detail=True, methods=["post"]) def cart(self, request, *args, **kwargs): """ Add a certain number of tickets to cart @@ -2256,6 +2256,9 @@ def cart(self, request, *args, **kwargs): count = request.data.get("count") event = Event.objects.get(id=self.get_object().id) cart = Cart.objects.filter(owner=self.request.user).first() + if not cart: + new_cart = Cart(owner=self.request.user) + new_cart.save() # Try to get count unowned ticket of requested type tickets = Ticket.objects.filter( From c69254aa81ea8ffff9ea52ef5b20722a7f59043c Mon Sep 17 00:00:00 2001 From: dfeng678 Date: Sun, 27 Feb 2022 13:09:51 -0500 Subject: [PATCH 11/44] slightly changed holding updates, added update to holding status when adding to cart --- backend/clubs/views.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 7dd086c38..f318a2efa 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2252,6 +2252,12 @@ def cart(self, request, *args, **kwargs): - $ref: "#/components/schemas/Ticket" --- """ + # Update holding status for all held tickets + held_tickets = Ticket.objects.filter(held=True).all() + for ticket in held_tickets: + if ticket.holding_expiration <= timezone.now(): + ticket.held = False + ticket.save() type = request.data.get("type") count = request.data.get("count") event = Event.objects.get(id=self.get_object().id) @@ -2293,12 +2299,12 @@ def validate_cart(self, request, *args, **kwargs): - $ref: "#/components/schemas/Ticket" --- """ - # Update holding expirations for all tickets - for ticket in Ticket.objects.all(): - if ticket.holding_expiration: - if ticket.holding_expiration <= timezone.now(): - ticket.held = False - ticket.save() + # Update holding status for all held tickets + held_tickets = Ticket.objects.filter(held=True).all() + for ticket in held_tickets: + if ticket.holding_expiration <= timezone.now(): + ticket.held = False + ticket.save() cart = Cart.objects.filter(owner=self.request.user).first() From 5382f48cce5cafca7c65dc3f2a953bc10781cf68 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sat, 12 Nov 2022 15:00:56 -0500 Subject: [PATCH 12/44] atomic transactions, cleanup, and some views --- backend/clubs/models.py | 12 +- backend/clubs/serializers.py | 4 + backend/clubs/views.py | 274 +++++++++++++++++++++++------------ 3 files changed, 192 insertions(+), 98 deletions(-) diff --git a/backend/clubs/models.py b/backend/clubs/models.py index aa6183c12..8fd25c9f4 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1711,7 +1711,7 @@ class Cart(models.Model): Represents an instance of a ticket cart for a user """ - owner = models.ForeignKey( + owner = models.OneToOneField( get_user_model(), related_name="cart", on_delete=models.CASCADE ) @@ -1728,12 +1728,18 @@ class Ticket(models.Model): type = models.CharField(max_length=100) owner = models.ForeignKey( get_user_model(), - related_name="tickets", + related_name="owned_tickets", + on_delete=models.SET_NULL, + blank=True, + null=True, + ) + holder = models.ForeignKey( + get_user_model(), + related_name="held_tickets", on_delete=models.SET_NULL, blank=True, null=True, ) - held = models.BooleanField(default=False) holding_expiration = models.DateTimeField(null=True, blank=True) carts = models.ManyToManyField(Cart, related_name="tickets", blank=True) diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index 69ec913ad..fe4ec4452 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -340,8 +340,12 @@ class ClubEventSerializer(serializers.ModelSerializer): image_url = serializers.SerializerMethodField("get_image_url") large_image_url = serializers.SerializerMethodField("get_large_image_url") url = serializers.SerializerMethodField("get_event_url") + ticketed = serializers.SerializerMethodField("get_ticketed") creator = serializers.HiddenField(default=serializers.CurrentUserDefault()) + def get_ticketed(self, obj): + return Event.tickets.exists() + def get_event_url(self, obj): # if no url, return that if not obj.url: diff --git a/backend/clubs/views.py b/backend/clubs/views.py index f318a2efa..85c5e2a6a 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -25,18 +25,11 @@ from django.core.management import call_command, get_commands, load_command_class from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import validate_email -from django.db.models import ( - Count, - DurationField, - ExpressionWrapper, - F, - Prefetch, - Q, - TextField, - Value, -) -from django.db.models.expressions import RawSQL -from django.db.models.functions import SHA1, Concat, Lower, Trunc +from django.db import transaction +from django.db.models import Count, DurationField, ExpressionWrapper, F, Prefetch, Q +from django.db.models.expressions import RawSQL, Value +from django.db.models.functions import Lower, Trunc +from django.db.models.functions.text import Concat from django.db.models.query import prefetch_related_objects from django.http import HttpResponse from django.shortcuts import get_object_or_404, render @@ -2228,10 +2221,39 @@ def get_serializer_class(self): return EventWriteSerializer return EventSerializer + def update_holds(self): + """ + Update ticket holds for *all* tickets + """ + held_tickets = Ticket.objects.filter(holder__isnull=False).all() + for ticket in held_tickets: + if ticket.holding_expiration <= timezone.now(): + ticket.holder = None + ticket.save() + + @action(detail=False, methods=["get"]) + def view_cart(self): + """ + View contents of the cart + --- + requestBody: {} + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Cart" + + """ + cart = Cart.objects.get_or_create(owner=self.request.user) + return None # Response(CartSerializer(cart.data)) + @action(detail=True, methods=["post"]) - def cart(self, request, *args, **kwargs): + @transaction.atomic + def add_to_cart(self, request, *args, **kwargs): """ - Add a certain number of tickets to cart + Add a certain number of tickets to the cart --- requestBody: content: @@ -2248,40 +2270,84 @@ def cart(self, request, *args, **kwargs): content: application/json: schema: - allOf: - - $ref: "#/components/schemas/Ticket" + properties: + detail: + type: string + "403": + content: + application/json: + schema: + properties: + detail: + type: string --- """ - # Update holding status for all held tickets - held_tickets = Ticket.objects.filter(held=True).all() - for ticket in held_tickets: - if ticket.holding_expiration <= timezone.now(): - ticket.held = False - ticket.save() + self.update_holds() type = request.data.get("type") count = request.data.get("count") - event = Event.objects.get(id=self.get_object().id) - cart = Cart.objects.filter(owner=self.request.user).first() - if not cart: - new_cart = Cart(owner=self.request.user) - new_cart.save() - - # Try to get count unowned ticket of requested type - tickets = Ticket.objects.filter( - event=event, type=type, owner__isnull=True, held=False - ).exclude(carts__owner=self.request.user) + event = self.get_object() + cart = Cart.objects.get_or_create(owner=self.request.user) + + # count unowned/unheld tickets of requested type + tickets = ( + Ticket.objects.select_for_update(skip_locked=True) + .filter(event=event, type=type, owner__isnull=True, holder__isnull=True) + .exclude(carts__owner=self.request.user) + ) + if tickets.count() < count: return Response( {"detail": f"Not enough tickets of type {type} left!"}, status=status.HTTP_403_FORBIDDEN, ) else: - for ticket in tickets[:count]: - ticket.carts.add(cart) - ticket.save() + cart.tickets.add(*tickets[:count]) + cart.save() return Response({"detail": "Successfully added to cart"}) + @action(detail=True, methods=["post"]) + @transaction.atomic + def remove_from_cart(self, request, *args, **kwargs): + """ + Remove a certain type/number of tickets from the cart + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + count: + type: integer + responses: + "200": + content: + application/json: + schema: + properties: + detail: + type: string + --- + """ + + self.update_holds() + type = request.data.get("type") + event = self.get_object() + cart = get_object_or_404(owner=self.request.user) + tickets = cart.tickets.filter(type=type, event=event) + + # Ensure we don't try to remove more tickets than we can + count = min(request.data.get("count"), tickets.count()) + + cart.tickets.remove(*tickets[:count]) + cart.save() + + return Response({"detail": "Successfully removed from cart"}) + @action(detail=False, methods=["post"]) + @transaction.atomic def validate_cart(self, request, *args, **kwargs): """ Validate tickets in a cart @@ -2299,72 +2365,112 @@ def validate_cart(self, request, *args, **kwargs): - $ref: "#/components/schemas/Ticket" --- """ - # Update holding status for all held tickets - held_tickets = Ticket.objects.filter(held=True).all() - for ticket in held_tickets: - if ticket.holding_expiration <= timezone.now(): - ticket.held = False - ticket.save() + self.update_holds() - cart = Cart.objects.filter(owner=self.request.user).first() + cart = get_object_or_404(Cart, owner=self.request.user) + sold_out_flag = False for ticket in cart.tickets.all(): - if ticket.owner or ticket.held: - new_ticket = Ticket.objects.filter( - event=ticket.event, type=ticket.type, owner__isnull=True, held=False - ).first() + # if ticket in cart has been bought, try to replace + if ticket.owner or ticket.holder: + # lock new ticket until transaction is completed + new_ticket = ( + Ticket.objects.select_for_update(skip_locked=True) + .filter( + event=ticket.event, + type=ticket.type, + owner__isnull=True, + holder__isnull=True, + ) + .first() + ) cart.tickets.remove(ticket) if new_ticket: cart.tickets.add(new_ticket) - cart.save() - return Response({"detail": "Cart validated"}) + else: + sold_out_flag = True + cart.save() + + return Response( + {"detail": "Validated" + ("" if not sold_out_flag else " with changes")} + ) @action(detail=False, methods=["post"]) + @transaction.atomic def checkout(self, request, *args, **kwargs): """ - Checkout all tickets in cart, assumes all tickets are unowned and unheld + Checkout all tickets in cart, to be called after validate_cart + + NOTE: this does NOT buy tickets, it simply initiates a checkout process + which includes a 10-minute ticket hold --- requestBody: content: application/json: schema: + type: object + responses: "200": content: application/json: schema: - allOf: - - $ref: "#/components/schemas/Ticket" + properties: + detail: + type: string --- """ - cart = Cart.objects.filter(owner=self.request.user).first() + cart = get_object_or_404(Cart, owner=self.request.user) - for ticket in cart.tickets.all(): - ticket.held = True + # The assumption is that this filter query should return all tickets in the cart (if run after validate_cart); + # however we cannot guarantee atomicity between validate_cart and checkout + # + # customers will be prompted to review the cart before payment + + for ticket in cart.tickets.select_for_update().filter( + owner__isnull=True, holder__isnull=True + ): + ticket.holder = self.request.user ticket.holding_expiration = timezone.now() + datetime.timedelta(minutes=10) ticket.save() - # Should only run if Stripe call succeeds - for ticket in cart.tickets.all(): + return Response({"detail": "Successfully initated checkout"}) + + @action(detail=False, methods=["post"]) + @transaction.atomic + def checkout_success_callback(self, request, *args, **kwargs): + """ + Callback after third party payment succeeds + --- + requestBody: {} + responses: + "200": + content: + application/json: + schema: + properties: + detail: + type: string + + """ + cart = get_object_or_404(Cart, owner=self.request.user) + + for ticket in cart.tickets.select_for_update().all(): ticket.owner = request.user ticket.carts.remove(cart) - ticket.save() # ticket.send_confirmation_email() - return Response({"detail": "Successfully checked out!"}) + ticket.save() + + return Response({"detail": "callback successful"}) @action(detail=True, methods=["post"]) + @transaction.atomic def buy(self, request, *args, **kwargs): """ - Buy a ticket + Buy tickets in a cart --- requestBody: - content: - application/json: - schema: - type: object - properties: - type: - type: string + content: {} responses: "200": content: @@ -2374,35 +2480,16 @@ def buy(self, request, *args, **kwargs): - $ref: "#/components/schemas/Ticket" --- """ - type = request.data.get("type") - event = self.get_object() - # If ticket already owned, do nothing - if Ticket.objects.filter(event=event, owner=request.user.id).first(): - return Response( - {"detail": "Ticket to event already owned by user"}, - status=status.HTTP_400_BAD_REQUEST, - ) + # Some logic here to serialize all held tickets down to whatever + # format third party asks for - # Otherwise get first unowned ticket of requested type - ticket = Ticket.objects.filter( - event=event, type=type, owner__isnull=True, held=False - ).first() + cart = get_object_or_404(Cart, owner=self.request.user) - # Stripe call here + for ticket in cart.tickets.filter(holder=self.request.user): + pass - # If Stripe call succeeds - if ticket: - ticket.held = True - ticket.owner = request.user - ticket.save() - ticket.send_confirmation_email() - return Response(TicketSerializer(ticket).data) - else: - return Response( - {"detail": f"No tickets of type {type} left!"}, - status=status.HTTP_403_FORBIDDEN, - ) + pass @action(detail=True, methods=["get"]) def tickets(self, request, *args, **kwargs): @@ -2478,8 +2565,6 @@ def create_tickets(self, request, *args, **kwargs): schema: type: object properties: - name: - type: string quantities: type: array items: @@ -2498,7 +2583,7 @@ def create_tickets(self, request, *args, **kwargs): properties: detail: type: string - description: A success or error message. + description: Empty array for success """ event = self.get_object() quantities = request.data.get("quantities") @@ -2536,7 +2621,6 @@ def upload(self, request, *args, **kwargs): description: Returned if the file was successfully uploaded. content: &upload_resp application/json: - schema: type: object properties: detail: From 6508a979391dd0ae6646b9862c97a8a81797a7cc Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Wed, 6 Jul 2022 04:10:47 -0700 Subject: [PATCH 13/44] holding, validation improvements, perms --- backend/clubs/migrations/0091_cart_ticket.py | 90 ++++ backend/clubs/migrations/0091_ticket.py | 51 --- backend/clubs/models.py | 14 + backend/clubs/permissions.py | 11 +- backend/clubs/serializers.py | 3 +- backend/clubs/views.py | 431 ++++++++++--------- 6 files changed, 346 insertions(+), 254 deletions(-) create mode 100644 backend/clubs/migrations/0091_cart_ticket.py delete mode 100644 backend/clubs/migrations/0091_ticket.py diff --git a/backend/clubs/migrations/0091_cart_ticket.py b/backend/clubs/migrations/0091_cart_ticket.py new file mode 100644 index 000000000..758aa2e30 --- /dev/null +++ b/backend/clubs/migrations/0091_cart_ticket.py @@ -0,0 +1,90 @@ +# Generated by Django 3.2.8 on 2022-10-02 16:50 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("clubs", "0090_adminnote"), + ] + + operations = [ + migrations.CreateModel( + name="Cart", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "owner", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="cart", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Ticket", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("type", models.CharField(max_length=100)), + ("holding_expiration", models.DateTimeField(blank=True, null=True)), + ( + "carts", + models.ManyToManyField( + blank=True, related_name="tickets", to="clubs.Cart" + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="tickets", + to="clubs.event", + ), + ), + ( + "holder", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="held_tickets", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="owned_tickets", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/backend/clubs/migrations/0091_ticket.py b/backend/clubs/migrations/0091_ticket.py deleted file mode 100644 index a76becfee..000000000 --- a/backend/clubs/migrations/0091_ticket.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 3.2.8 on 2022-01-16 20:59 - -import uuid - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("clubs", "0090_adminnote"), - ] - - operations = [ - migrations.CreateModel( - name="Ticket", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ("type", models.CharField(max_length=100)), - ( - "event", - models.ForeignKey( - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="tickets", - to="clubs.event", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="tickets", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 8fd25c9f4..a9f90de55 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1716,6 +1716,19 @@ class Cart(models.Model): ) +class TicketManager(models.Manager): + + # Update holds for all tickets + def update_holds(self): + expired_tickets = self.select_for_update().filter( + holder__isnull=False, holding_expiration__lte=timezone.now() + ) + with transaction.atomic(): + for ticket in expired_tickets: + ticket.holder = None + self.bulk_update(expired_tickets, ["holder"]) + + class Ticket(models.Model): """ Represents an instance of a ticket for an event @@ -1742,6 +1755,7 @@ class Ticket(models.Model): ) holding_expiration = models.DateTimeField(null=True, blank=True) carts = models.ManyToManyField(Cart, related_name="tickets", blank=True) + objects = TicketManager() def get_qr(self): """ diff --git a/backend/clubs/permissions.py b/backend/clubs/permissions.py index 09117f98a..4297d9fcc 100644 --- a/backend/clubs/permissions.py +++ b/backend/clubs/permissions.py @@ -188,7 +188,7 @@ def has_permission(self, request, view): class EventPermission(permissions.BasePermission): """ - Officers and above can create/update/delete events. + Officers and above can create/update/delete events and view ticket buyers. Everyone else can view and list events. """ @@ -224,7 +224,14 @@ def has_object_permission(self, request, view, obj): if not old_type == FAIR_TYPE and new_type == FAIR_TYPE: return False - + elif view.action in ["buyers", "create_tickets"]: + if not request.user.is_authenticated: + return False + membership = find_membership_helper(request.user, obj.club) + return membership is not None and membership.role <= Membership.ROLE_OFFICER + elif view.action in ["add_to_cart", "remove_from_cart"]: + return request.user.is_authenticated + print("action", view.action) return True diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index fe4ec4452..9e3286308 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -343,7 +343,7 @@ class ClubEventSerializer(serializers.ModelSerializer): ticketed = serializers.SerializerMethodField("get_ticketed") creator = serializers.HiddenField(default=serializers.CurrentUserDefault()) - def get_ticketed(self, obj): + def get_ticketed(self, obj) -> bool: return Event.tickets.exists() def get_event_url(self, obj): @@ -483,6 +483,7 @@ class Meta: "location", "name", "start_time", + "ticketed", "type", "url", ] diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 85c5e2a6a..e7b259fc7 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -8,6 +8,7 @@ import re import secrets import string +from functools import wraps from urllib.parse import urlparse import pytz @@ -171,6 +172,19 @@ from clubs.utils import fuzzy_lookup_club, html_to_text +def update_holds(func): + """ + Decorator to update ticket holds + """ + + @wraps(func) + def wrap(self, request, *args, **kwargs): + Ticket.objects.update_holds() + return func(self, request, *args, **kwargs) + + return wrap + + def file_upload_endpoint_helper(request, code): obj = get_object_or_404(Club, code=code) if "file" in request.data and isinstance(request.data["file"], UploadedFile): @@ -1561,7 +1575,6 @@ def booths(self, request, *args, **kwargs): """ club = self.get_object() res = ClubFairBooth.objects.filter(club=club).select_related("club").all() - return Response(ClubBoothSerializer(res, many=True).data) def get_operation_id(self, **kwargs): @@ -2201,6 +2214,16 @@ class ClubEventViewSet(viewsets.ModelViewSet): buy: Buy a ticket for an event + + buyers: + Get information about the buyers of an event's ticket + + remove_from_cart: + Remove a ticket for this event from cart + + add_to_cart: + Add a ticket for this event to cart + """ permission_classes = [EventPermission | IsSuperuser] @@ -2221,36 +2244,9 @@ def get_serializer_class(self): return EventWriteSerializer return EventSerializer - def update_holds(self): - """ - Update ticket holds for *all* tickets - """ - held_tickets = Ticket.objects.filter(holder__isnull=False).all() - for ticket in held_tickets: - if ticket.holding_expiration <= timezone.now(): - ticket.holder = None - ticket.save() - - @action(detail=False, methods=["get"]) - def view_cart(self): - """ - View contents of the cart - --- - requestBody: {} - responses: - "200": - content: - application/json: - schema: - allOf: - - $ref: "#/components/schemas/Cart" - - """ - cart = Cart.objects.get_or_create(owner=self.request.user) - return None # Response(CartSerializer(cart.data)) - @action(detail=True, methods=["post"]) @transaction.atomic + @update_holds def add_to_cart(self, request, *args, **kwargs): """ Add a certain number of tickets to the cart @@ -2273,7 +2269,7 @@ def add_to_cart(self, request, *args, **kwargs): properties: detail: type: string - "403": + "403": content: application/json: schema: @@ -2282,13 +2278,12 @@ def add_to_cart(self, request, *args, **kwargs): type: string --- """ - self.update_holds() type = request.data.get("type") count = request.data.get("count") event = self.get_object() cart = Cart.objects.get_or_create(owner=self.request.user) - # count unowned/unheld tickets of requested type + # Count unowned/unheld tickets of requested type tickets = ( Ticket.objects.select_for_update(skip_locked=True) .filter(event=event, type=type, owner__isnull=True, holder__isnull=True) @@ -2307,6 +2302,7 @@ def add_to_cart(self, request, *args, **kwargs): @action(detail=True, methods=["post"]) @transaction.atomic + @update_holds def remove_from_cart(self, request, *args, **kwargs): """ Remove a certain type/number of tickets from the cart @@ -2332,10 +2328,9 @@ def remove_from_cart(self, request, *args, **kwargs): --- """ - self.update_holds() type = request.data.get("type") event = self.get_object() - cart = get_object_or_404(owner=self.request.user) + cart = get_object_or_404(Cart, owner=self.request.user) tickets = cart.tickets.filter(type=type, event=event) # Ensure we don't try to remove more tickets than we can @@ -2346,101 +2341,10 @@ def remove_from_cart(self, request, *args, **kwargs): return Response({"detail": "Successfully removed from cart"}) - @action(detail=False, methods=["post"]) - @transaction.atomic - def validate_cart(self, request, *args, **kwargs): - """ - Validate tickets in a cart - --- - requestBody: - content: - application/json: - schema: - responses: - "200": - content: - application/json: - schema: - allOf: - - $ref: "#/components/schemas/Ticket" - --- - """ - self.update_holds() - - cart = get_object_or_404(Cart, owner=self.request.user) - sold_out_flag = False - - for ticket in cart.tickets.all(): - # if ticket in cart has been bought, try to replace - if ticket.owner or ticket.holder: - # lock new ticket until transaction is completed - new_ticket = ( - Ticket.objects.select_for_update(skip_locked=True) - .filter( - event=ticket.event, - type=ticket.type, - owner__isnull=True, - holder__isnull=True, - ) - .first() - ) - cart.tickets.remove(ticket) - if new_ticket: - cart.tickets.add(new_ticket) - else: - sold_out_flag = True - cart.save() - - return Response( - {"detail": "Validated" + ("" if not sold_out_flag else " with changes")} - ) - - @action(detail=False, methods=["post"]) - @transaction.atomic - def checkout(self, request, *args, **kwargs): - """ - Checkout all tickets in cart, to be called after validate_cart - - NOTE: this does NOT buy tickets, it simply initiates a checkout process - which includes a 10-minute ticket hold - --- - requestBody: - content: - application/json: - schema: - type: object - - responses: - "200": - content: - application/json: - schema: - properties: - detail: - type: string - --- - """ - cart = get_object_or_404(Cart, owner=self.request.user) - - # The assumption is that this filter query should return all tickets in the cart (if run after validate_cart); - # however we cannot guarantee atomicity between validate_cart and checkout - # - # customers will be prompted to review the cart before payment - - for ticket in cart.tickets.select_for_update().filter( - owner__isnull=True, holder__isnull=True - ): - ticket.holder = self.request.user - ticket.holding_expiration = timezone.now() + datetime.timedelta(minutes=10) - ticket.save() - - return Response({"detail": "Successfully initated checkout"}) - - @action(detail=False, methods=["post"]) - @transaction.atomic - def checkout_success_callback(self, request, *args, **kwargs): + @action(detail=True, methods=["get"]) + def buyers(self, request, *args, **kwargs): """ - Callback after third party payment succeeds + Get information about ticket buyers --- requestBody: {} responses: @@ -2448,48 +2352,32 @@ def checkout_success_callback(self, request, *args, **kwargs): content: application/json: schema: - properties: - detail: - type: string - - """ - cart = get_object_or_404(Cart, owner=self.request.user) - - for ticket in cart.tickets.select_for_update().all(): - ticket.owner = request.user - ticket.carts.remove(cart) - # ticket.send_confirmation_email() - ticket.save() - - return Response({"detail": "callback successful"}) - - @action(detail=True, methods=["post"]) - @transaction.atomic - def buy(self, request, *args, **kwargs): - """ - Buy tickets in a cart - --- - requestBody: - content: {} - responses: - "200": - content: - application/json: - schema: - allOf: - - $ref: "#/components/schemas/Ticket" + type: object + properties: + buyers: + type: array + items: + type: object + properties: + fullname: + type: string + id: + type: string + owner_id: + type: int + type: + type: string --- """ + tickets = Ticket.objects.filter(event=self.get_object()).annotate( + fullname=Concat("owner__first_name", Value(" "), "owner__last_name") + ) - # Some logic here to serialize all held tickets down to whatever - # format third party asks for - - cart = get_object_or_404(Cart, owner=self.request.user) - - for ticket in cart.tickets.filter(holder=self.request.user): - pass + buyers = tickets.filter(owner__isnull=False).values( + "id", "fullname", "owner_id", "type" + ) - pass + return Response({"buyers": buyers}) @action(detail=True, methods=["get"]) def tickets(self, request, *args, **kwargs): @@ -2504,17 +2392,6 @@ def tickets(self, request, *args, **kwargs): schema: type: object properties: - buyers: - type: array - items: - type: object - properties: - fullname: - type: string - id: - type: string - type: - type: string totals: type: array items: @@ -2536,10 +2413,9 @@ def tickets(self, request, *args, **kwargs): --- """ event = self.get_object() - tickets = Ticket.objects.filter(event=event).annotate( - fullname=Concat("owner__first_name", Value(" "), "owner__last_name") - ) + tickets = Ticket.objects.filter(event=event) types = tickets.values_list("type", flat=True).distinct() + totals = [] available = [] @@ -2552,11 +2428,10 @@ def tickets(self, request, *args, **kwargs): } ) - buyers = tickets.filter(owner__isnull=False).values("id", "fullname", "type") - - return Response({"totals": totals, "available": available, "buyers": buyers}) + return Response({"totals": totals, "available": available}) @tickets.mapping.put + @transaction.atomic def create_tickets(self, request, *args, **kwargs): """ requestBody: @@ -2583,24 +2458,20 @@ def create_tickets(self, request, *args, **kwargs): properties: detail: type: string - description: Empty array for success """ event = self.get_object() quantities = request.data.get("quantities") - membership = find_membership_helper(request.user, event.club) - if membership.role <= 10: # Create tickets allowed if officer+ + Ticket.objects.filter(event=event).delete() # Idempotency + tickets = [ + Ticket(event=event, type=item["type"]) + for item in quantities + for _ in range(item["count"]) + ] - Ticket.objects.filter(event=event).delete() # Idempotency + Ticket.objects.bulk_create(tickets) - for item in quantities: - for _ in range(item["count"]): - Ticket.objects.create(event=event, type=item["type"]) - return Response([]) - else: - return Response( - {"detail": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED - ) + return Response({"detail": "success"}) @action(detail=True, methods=["post"]) def upload(self, request, *args, **kwargs): @@ -4599,11 +4470,17 @@ def get_object(self): class TicketViewSet(viewsets.ModelViewSet): """ - list: + get: List all tickets owned by user - retrieve: - Retrieve an individual ticket's data + cart: + List all unowned/unheld tickets currently in user's cart + + checkout: + Initiate a hold on the tickets in a user's cart + + buy: + Buy the tickets in a user's cart qr: Get a ticket's QR code @@ -4614,8 +4491,162 @@ class TicketViewSet(viewsets.ModelViewSet): http_method_names = ["get", "post"] lookup_field = "id" - def retrieve(self, request, *args, **kwargs): - return Response(TicketSerializer(Ticket.objects.get(id=kwargs["id"])).data) + @transaction.atomic + @update_holds + @action(detail=False, methods=["get"]) + def cart(self, request, *args, **kwargs): + """ + Validate tickets in a cart + --- + requestBody: + content: {} + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Ticket" + "204": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Ticket" + --- + """ + + cart, _ = Cart.objects.get_or_create(owner=self.request.user) + + # this flag is true when a validate operation fails to + # replace a ticket. return 200 if true, otherwise 204 + sold_out_flag = False + + for ticket in cart.tickets.all(): + # if ticket in cart has been bought, try to replace + if ticket.owner or ticket.holder: + # lock new ticket until transaction is completed + new_ticket = ( + Ticket.objects.select_for_update(skip_locked=True) + .filter( + event=ticket.event, + type=ticket.type, + owner__isnull=True, + holder__isnull=True, + ) + .first() + ) + cart.tickets.remove(ticket) + if new_ticket: + cart.tickets.add(new_ticket) + else: + sold_out_flag = True + cart.save() + + return Response( + TicketSerializer(cart.tickets.all(), many=True).data, + status=200 if sold_out_flag else 204, + ) + + @action(detail=False, methods=["post"]) + @update_holds + @transaction.atomic + def checkout(self, request, *args, **kwargs): + """ + Checkout all tickets in cart + + NOTE: this does NOT buy tickets, it simply initiates a checkout process + which includes a 10-minute ticket hold + --- + requestBody: + content: + application/json: + schema: + type: object + + responses: + "200": + content: + application/json: + schema: + properties: + detail: + type: string + --- + """ + cart = get_object_or_404(Cart, owner=self.request.user) + + # The assumption is that this filter query should return all tickets in the cart + # however we cannot guarantee atomicity between cart and checkout + # + # customers will be prompted to review the cart before payment + + tickets = cart.tickets.select_for_update().filter( + Q(holder__isnull=True) | Q(holder=self.request.user), owner__isnull=True + ) + + for ticket in tickets: + ticket.holder = self.request.user + ticket.holding_expiration = timezone.now() + datetime.timedelta(minutes=10) + + Ticket.objects.bulk_update(tickets, ["holder", "holding_expiration"]) + + return Response({"detail": "Successfully initated checkout"}) + + @action(detail=False, methods=["post"]) + @transaction.atomic + def checkout_success_callback(self, request, *args, **kwargs): + """ + Callback after third party payment succeeds + --- + requestBody: {} + responses: + "200": + content: + application/json: + schema: + properties: + detail: + type: string + + """ + cart = get_object_or_404(Cart, owner=self.request.user) + + for ticket in cart.tickets.select_for_update().all(): + ticket.owner = request.user + ticket.carts.clear() + # ticket.send_confirmation_email() + ticket.save() + + return Response({"detail": "callback successful"}) + + @action(detail=False, methods=["post"]) + @transaction.atomic + def buy(self, request, *args, **kwargs): + """ + Buy held tickets in a cart + --- + requestBody: + content: {} + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Ticket" + --- + """ + + # Some logic here to serialize all held tickets down to whatever + # format third party asks for + + cart = get_object_or_404(Cart, owner=self.request.user) + + for ticket in cart.tickets.filter(holder=self.request.user): + pass + + return Response({}) @action(detail=True, methods=["get"]) def qr(self, request, *args, **kwargs): From 961074739cdd68a5c68bdba62f689e43842dc194 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sun, 16 Oct 2022 12:40:04 -0400 Subject: [PATCH 14/44] some docstring stuff --- backend/clubs/permissions.py | 1 - backend/clubs/views.py | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/backend/clubs/permissions.py b/backend/clubs/permissions.py index 4297d9fcc..253f9d807 100644 --- a/backend/clubs/permissions.py +++ b/backend/clubs/permissions.py @@ -231,7 +231,6 @@ def has_object_permission(self, request, view, obj): return membership is not None and membership.role <= Membership.ROLE_OFFICER elif view.action in ["add_to_cart", "remove_from_cart"]: return request.user.is_authenticated - print("action", view.action) return True diff --git a/backend/clubs/views.py b/backend/clubs/views.py index e7b259fc7..e525b32f5 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2462,6 +2462,8 @@ def create_tickets(self, request, *args, **kwargs): event = self.get_object() quantities = request.data.get("quantities") + # Atomicity ensures idempotency + Ticket.objects.filter(event=event).delete() # Idempotency tickets = [ Ticket(event=event, type=item["type"]) @@ -2657,6 +2659,12 @@ class EventViewSet(ClubEventViewSet): destroy: Delete an event. + + fair: + Get information about a fair listing + + owned: + Return all events that the user has officer permissions over. """ def get_operation_id(self, **kwargs): @@ -4479,6 +4487,9 @@ class TicketViewSet(viewsets.ModelViewSet): checkout: Initiate a hold on the tickets in a user's cart + checkout_success_callback: + Callback after third party payment succeeds + buy: Buy the tickets in a user's cart @@ -4578,7 +4589,7 @@ def checkout(self, request, *args, **kwargs): # The assumption is that this filter query should return all tickets in the cart # however we cannot guarantee atomicity between cart and checkout - # + # customers will be prompted to review the cart before payment tickets = cart.tickets.select_for_update().filter( @@ -4638,6 +4649,8 @@ def buy(self, request, *args, **kwargs): --- """ + # TODO: Implement + # Some logic here to serialize all held tickets down to whatever # format third party asks for From c9fb1156b0db73fa0901338adb0c84729741ad97 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sun, 16 Oct 2022 13:06:04 -0400 Subject: [PATCH 15/44] rebase + pipfile lock --- backend/Pipfile.lock | 305 ++++++++++++++++------------------------- backend/clubs/views.py | 2 + 2 files changed, 117 insertions(+), 190 deletions(-) diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 6c195adb4..bd497c315 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1e5ed3497a97d54da1163f9def2f5aee2feaf2aa537a361d42376f30cb8cac4f" + "sha256": "ebeb34dcfd58ce86a7aee2ed18042c0fb124d534ee3b168f1943f1e294212b34" }, "pipfile-spec": 6, "requires": { @@ -16,13 +16,6 @@ ] }, "default": { - "aioredis": { - "hashes": [ - "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", - "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" - ], - "version": "==1.3.1" - }, "anyio": { "hashes": [ "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", @@ -33,11 +26,11 @@ }, "arrow": { "hashes": [ - "sha256:4bfacea734ead51495dc47df00421ecfd4ca1f2c0fbe58b9a26eaeddedc31caf", - "sha256:67f8be7c0cf420424bc62d8d7dc40b44e4bb2f7b515f9cc2954fb36e35797656" + "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1", + "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.14.7" + "markers": "python_version >= '3.6'", + "version": "==1.2.3" }, "asgiref": { "hashes": [ @@ -82,32 +75,32 @@ "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==4.11.1" }, "bleach": { "hashes": [ - "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", - "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" + "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", + "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" ], "index": "pypi", - "version": "==4.1.0" + "version": "==5.0.1" }, "boto3": { "hashes": [ - "sha256:6194763348545bb1669ce8d03ba104be1ba822daa184613aa10b9303a6a79017", - "sha256:be151711bbb4db53e85dd5bbe506002ce6f2f21fc4e45fcf6d2cf356d32cc4c6" + "sha256:3225366014949039e6687387242e73f237f0fee0a9b7c20461894f1ee40686b8", + "sha256:b295640bc1be637f8f7c8c8fca70781048d6397196109e59f20541824fab4b67" ], "index": "pypi", - "version": "==1.24.84" + "version": "==1.24.91" }, "botocore": { "hashes": [ - "sha256:11f05d2acdf9a5f722856704b7b951b180647fb4340e1b5048b27273dc323909", - "sha256:da15026329706caf83323d84996f5ff5c527837347633fca9b3b1be0efa60841" + "sha256:1d6e97bd8653f732c7078b34aa2bb438e750898957e5a0a74b6c72918bc1d0f7", + "sha256:c8fac203a391cc2e4b682877bfce70e723e33c529b35b399a1d574605fbeb1af" ], "markers": "python_version >= '3.7'", - "version": "==1.27.84" + "version": "==1.27.91" }, "bs4": { "hashes": [ @@ -211,27 +204,27 @@ }, "channels-redis": { "hashes": [ - "sha256:78e4a2f2b2a744fe5a87848ec36b5ee49f522c6808cefe6c583663d0d531faa8", - "sha256:ba7e2ad170f273c372812dd32aaac102d68d4e508172abb1cfda3160b7333890" + "sha256:122414f29f525f7b9e0c9d59cdcfc4dc1b0eecba16fbb6a1c23f1d9b58f49dcb", + "sha256:81b59d68f53313e1aa891f23591841b684abb936b42e4d1a966d9e4dc63a95ec" ], "index": "pypi", - "version": "==3.4.1" + "version": "==4.0.0" }, "charset-normalizer": { "hashes": [ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2.1.1" }, "click": { "hashes": [ - "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", - "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], - "index": "pypi", - "version": "==8.0.4" + "markers": "python_version >= '3.7'", + "version": "==8.1.3" }, "conditional": { "hashes": [ @@ -320,11 +313,11 @@ }, "django": { "hashes": [ - "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713", - "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b" + "sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793", + "sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f" ], "index": "pypi", - "version": "==3.2.15" + "version": "==4.1.2" }, "django-clone": { "hashes": [ @@ -452,53 +445,6 @@ "markers": "python_version >= '3.7'", "version": "==0.14.0" }, - "hiredis": { - "hashes": [ - "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e", - "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27", - "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163", - "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc", - "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26", - "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e", - "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579", - "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a", - "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048", - "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87", - "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63", - "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54", - "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05", - "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb", - "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea", - "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5", - "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e", - "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc", - "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99", - "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a", - "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581", - "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426", - "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db", - "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a", - "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a", - "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d", - "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443", - "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79", - "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d", - "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9", - "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d", - "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485", - "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5", - "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048", - "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0", - "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6", - "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41", - "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298", - "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce", - "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", - "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.0" - }, "httptools": { "hashes": [ "sha256:0297822cea9f90a38df29f48e40b42ac3d48a28637368f3ec6d15eebefd182f9", @@ -555,20 +501,20 @@ }, "ics": { "hashes": [ - "sha256:3b606205b9582ad27dff77f9b227a30d02fdac532731927fe39df1f1ddf8673f", - "sha256:81113a2bb3166c1afcd71cd450c968d40efc385601e9d8344733e00ad8f53429", - "sha256:bf5fbdef6e1e073afdadf1b996f0271186dd114a148e38e795919a1ae644d6ac" + "sha256:5fcf4d29ec6e7dfcb84120abd617bbba632eb77b097722b7df70e48dbcf26103", + "sha256:6743539bca10391635249b87d74fcd1094af20b82098bebf7c7521df91209f05", + "sha256:da352bdf8418619dc93611e6d251f3cefbb42664777f6e00b580a722098124b7" ], "index": "pypi", - "version": "==0.7" + "version": "==0.7.2" }, "identify": { "hashes": [ - "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6", - "sha256:ef78c0d96098a3b5fe7720be4a97e73f439af7cf088ebf47b620aeaa10fadf97" + "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245", + "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa" ], "markers": "python_version >= '3.7'", - "version": "==2.5.5" + "version": "==2.5.6" }, "idna": { "hashes": [ @@ -580,10 +526,10 @@ }, "incremental": { "hashes": [ - "sha256:02f5de5aff48f6b9f665d99d48bfc7ec03b6e3943210de7cfc88856d755d6f57", - "sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321" + "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0", + "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51" ], - "version": "==21.3.0" + "version": "==22.10.0" }, "jmespath": { "hashes": [ @@ -595,11 +541,11 @@ }, "jsonref": { "hashes": [ - "sha256:b1e82fa0b62e2c2796a13e5401fe51790b248f6d9bf9d7212a3e31a3501b291f", - "sha256:f3c45b121cf6257eafabdc3a8008763aed1cd7da06dbabc59a9e4d2a5e4e6697" + "sha256:68b330c6815dc0d490dbb3d65ccda265ddde9f7856fd2f3322f971d456ea7549", + "sha256:9480ad1b500f7e795daeb0ef29f9c55ae3a9ab38fb8d6659b6f4868acb5a5bc8" ], "index": "pypi", - "version": "==0.2" + "version": "==0.3.0" }, "lxml": { "hashes": [ @@ -768,11 +714,11 @@ }, "phonenumbers": { "hashes": [ - "sha256:80a7422cf0999a6f9b7a2e6cfbdbbfcc56ab5b75414dc3b805bbec91276b64a3", - "sha256:82a4f226c930d02dcdf6d4b29e4cfd8678991fe65c2efd5fdd143557186f0868" + "sha256:057d1966962fb86b3dc447bfac2c8e25ceed774509e49b180926a13a99910318", + "sha256:0b234c4a9519fac18d00b3c542f5b429513ea69372d4f95fbd0f716f5e2a89b5" ], "index": "pypi", - "version": "==8.12.56" + "version": "==8.12.57" }, "pillow": { "hashes": [ @@ -856,20 +802,20 @@ }, "psycopg2": { "hashes": [ - "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c", - "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf", - "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362", - "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7", - "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461", - "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126", - "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981", - "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56", - "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305", - "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2", - "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca" + "sha256:07b90a24d5056687781ddaef0ea172fd951f2f7293f6ffdd03d4f5077801f426", + "sha256:1da77c061bdaab450581458932ae5e469cc6e36e0d62f988376e9f513f11cb5c", + "sha256:46361c054df612c3cc813fdb343733d56543fb93565cff0f8ace422e4da06acb", + "sha256:839f9ea8f6098e39966d97fcb8d08548fbc57c523a1e27a1f0609addf40f777c", + "sha256:849bd868ae3369932127f0771c08d1109b254f08d48dc42493c3d1b87cb2d308", + "sha256:8de6a9fc5f42fa52f559e65120dcd7502394692490c98fed1221acf0819d7797", + "sha256:a11946bad3557ca254f17357d5a4ed63bdca45163e7a7d2bfb8e695df069cc3a", + "sha256:aa184d551a767ad25df3b8d22a0a62ef2962e0e374c04f6cbd1204947f540d61", + "sha256:aafa96f2da0071d6dd0cbb7633406d99f414b40ab0f918c9d9af7df928a1accb", + "sha256:c7fa041b4acb913f6968fce10169105af5200f296028251d817ab37847c30184", + "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f" ], "index": "pypi", - "version": "==2.9.3" + "version": "==2.9.4" }, "pyasn1": { "hashes": [ @@ -964,10 +910,10 @@ }, "pytz": { "hashes": [ - "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", - "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" + "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91", + "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174" ], - "version": "==2022.2.1" + "version": "==2022.4" }, "pyyaml": { "hashes": [ @@ -1056,11 +1002,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:d6c71d2f85710b66822adaa954af7912bab135d6c85febd5b0f3dfd4ab37e181", - "sha256:ef925b5338625448645a778428d8f22a3d17de8b28cc8e6fba60b93393ad86fe" + "sha256:2469240f6190aaebcb453033519eae69cfe8cc602065b4667e18ee14fc1e35dc", + "sha256:4fbace9a763285b608c06f01a807b51acb35f6059da6a01236654e08b0ee81ff" ], "index": "pypi", - "version": "==1.9.9" + "version": "==1.9.10" }, "service-identity": { "hashes": [ @@ -1071,11 +1017,11 @@ }, "setuptools": { "hashes": [ - "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012", - "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e" + "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17", + "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356" ], "markers": "python_version >= '3.7'", - "version": "==65.4.1" + "version": "==65.5.0" }, "six": { "hashes": [ @@ -1170,11 +1116,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", - "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" + "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", + "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], "markers": "python_version >= '3.7'", - "version": "==4.3.0" + "version": "==4.4.0" }, "unittest-xml-reporting": { "hashes": [ @@ -1433,60 +1379,46 @@ }, "zope.interface": { "hashes": [ - "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192", - "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702", - "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09", - "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4", - "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a", - "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3", - "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf", - "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c", - "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d", - "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78", - "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83", - "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531", - "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46", - "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021", - "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94", - "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc", - "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63", - "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54", - "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117", - "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25", - "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05", - "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e", - "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1", - "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004", - "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2", - "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e", - "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f", - "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f", - "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120", - "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f", - "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1", - "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9", - "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e", - "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7", - "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8", - "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b", - "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155", - "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7", - "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c", - "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325", - "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d", - "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb", - "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e", - "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959", - "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7", - "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920", - "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e", - "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48", - "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8", - "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", - "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" + "sha256:006f8dd81fae28027fc28ada214855166712bf4f0bfbc5a8788f9b70982b9437", + "sha256:03f5ae315db0d0de668125d983e2a819a554f3fdb2d53b7e934e3eb3c3c7375d", + "sha256:0eb2b3e84f48dd9cfc8621c80fba905d7e228615c67f76c7df7c716065669bb6", + "sha256:1e3495bb0cdcea212154e558082c256f11b18031f05193ae2fb85d048848db14", + "sha256:26c1456520fdcafecc5765bec4783eeafd2e893eabc636908f50ee31fe5c738c", + "sha256:2cb3003941f5f4fa577479ac6d5db2b940acb600096dd9ea9bf07007f5cab46f", + "sha256:37ec9ade9902f412cc7e7a32d71f79dec3035bad9bd0170226252eed88763c48", + "sha256:3eedf3d04179774d750e8bb4463e6da350956a50ed44d7b86098e452d7ec385e", + "sha256:3f68404edb1a4fb6aa8a94675521ca26c83ebbdbb90e894f749ae0dc4ca98418", + "sha256:423c074e404f13e6fa07f4454f47fdbb38d358be22945bc812b94289d9142374", + "sha256:43490ad65d4c64e45a30e51a2beb7a6b63e1ff395302ad22392224eb618476d6", + "sha256:47ff078734a1030c48103422a99e71a7662d20258c00306546441adf689416f7", + "sha256:58a66c2020a347973168a4a9d64317bac52f9fdfd3e6b80b252be30da881a64e", + "sha256:58a975f89e4584d0223ab813c5ba4787064c68feef4b30d600f5e01de90ae9ce", + "sha256:5c6023ae7defd052cf76986ce77922177b0c2f3913bea31b5b28fbdf6cb7099e", + "sha256:6566b3d2657e7609cd8751bcb1eab1202b1692a7af223035a5887d64bb3a2f3b", + "sha256:687cab7f9ae18d2c146f315d0ca81e5ffe89a139b88277afa70d52f632515854", + "sha256:700ebf9662cf8df70e2f0cb4988e078c53f65ee3eefd5c9d80cf988c4175c8e3", + "sha256:740f3c1b44380658777669bcc42f650f5348e53797f2cee0d93dc9b0f9d7cc69", + "sha256:7bdcec93f152e0e1942102537eed7b166d6661ae57835b20a52a2a3d6a3e1bf3", + "sha256:7d9ec1e6694af39b687045712a8ad14ddcb568670d5eb1b66b48b98b9312afba", + "sha256:85dd6dd9aaae7a176948d8bb62e20e2968588fd787c29c5d0d964ab475168d3d", + "sha256:8b9f153208d74ccfa25449a0c6cb756ab792ce0dc99d9d771d935f039b38740c", + "sha256:8c791f4c203ccdbcda588ea4c8a6e4353e10435ea48ddd3d8734a26fe9714cba", + "sha256:970661ece2029915b8f7f70892e88404340fbdefd64728380cad41c8dce14ff4", + "sha256:9cdc4e898d3b1547d018829fd4a9f403e52e51bba24be0fbfa37f3174e1ef797", + "sha256:9dc4493aa3d87591e3d2bf1453e25b98038c839ca8e499df3d7106631b66fe83", + "sha256:a69c28d85bb7cf557751a5214cb3f657b2b035c8c96d71080c1253b75b79b69b", + "sha256:aeac590cce44e68ee8ad0b8ecf4d7bf15801f102d564ca1b0eb1f12f584ee656", + "sha256:be11fce0e6af6c0e8d93c10ef17b25aa7c4acb7ec644bff2596c0d639c49e20f", + "sha256:cbbf83914b9a883ab324f728de869f4e406e0cbcd92df7e0a88decf6f9ab7d5a", + "sha256:cfa614d049667bed1c737435c609c0956c5dc0dbafdc1145ee7935e4658582cb", + "sha256:d18fb0f6c8169d26044128a2e7d3c39377a8a151c564e87b875d379dbafd3930", + "sha256:d80f6236b57a95eb19d5e47eb68d0296119e1eff6deaa2971ab8abe3af918420", + "sha256:da7912ae76e1df6a1fb841b619110b1be4c86dfb36699d7fd2f177105cdea885", + "sha256:df6593e150d13cfcce69b0aec5df7bc248cb91e4258a7374c129bb6d56b4e5ca", + "sha256:f70726b60009433111fe9928f5d89cbb18962411d33c45fb19eb81b9bbd26fcd" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.4.0" + "version": "==5.5.0" } }, "develop": { @@ -1534,16 +1466,16 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2.1.1" }, "click": { "hashes": [ - "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", - "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], - "index": "pypi", - "version": "==8.0.4" + "markers": "python_version >= '3.7'", + "version": "==8.1.3" }, "codecov": { "hashes": [ @@ -1612,11 +1544,11 @@ }, "django": { "hashes": [ - "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713", - "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b" + "sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793", + "sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f" ], "index": "pypi", - "version": "==3.2.15" + "version": "==4.1.2" }, "django-debug-toolbar": { "hashes": [ @@ -1644,11 +1576,11 @@ }, "flake8-isort": { "hashes": [ - "sha256:26571500cd54976bbc0cf1006ffbcd1a68dd102f816b7a1051b219616ba9fee0", - "sha256:5b87630fb3719bf4c1833fd11e0d9534f43efdeba524863e15d8f14a7ef6adbf" + "sha256:c73f9cbd1bf209887f602a27b827164ccfeba1676801b2aa23cb49051a1be79c", + "sha256:e336f928c7edc509684930ab124414194b7f4e237c712af8fcbdf49d8747b10c" ], "index": "pypi", - "version": "==4.2.0" + "version": "==5.0.0" }, "flake8-quotes": { "hashes": [ @@ -1781,13 +1713,6 @@ "markers": "python_version >= '3.6'", "version": "==2.5.0" }, - "pytz": { - "hashes": [ - "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", - "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" - ], - "version": "==2022.2.1" - }, "regex": { "hashes": [ "sha256:003a2e1449d425afc817b5f0b3d4c4aa9072dd5f3dfbf6c7631b8dc7b13233de", diff --git a/backend/clubs/views.py b/backend/clubs/views.py index e525b32f5..a20f0491f 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2416,6 +2416,8 @@ def tickets(self, request, *args, **kwargs): tickets = Ticket.objects.filter(event=event) types = tickets.values_list("type", flat=True).distinct() + # TODO: convert this into SQL + totals = [] available = [] From 1b0bda842025c5a52a72ee167dc162db635a096a Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sun, 16 Oct 2022 13:43:04 -0400 Subject: [PATCH 16/44] use master pipfile.lock --- backend/Pipfile.lock | 305 +++++++++++++++++++++++++++---------------- 1 file changed, 190 insertions(+), 115 deletions(-) diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index bd497c315..6c195adb4 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ebeb34dcfd58ce86a7aee2ed18042c0fb124d534ee3b168f1943f1e294212b34" + "sha256": "1e5ed3497a97d54da1163f9def2f5aee2feaf2aa537a361d42376f30cb8cac4f" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,13 @@ ] }, "default": { + "aioredis": { + "hashes": [ + "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", + "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" + ], + "version": "==1.3.1" + }, "anyio": { "hashes": [ "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", @@ -26,11 +33,11 @@ }, "arrow": { "hashes": [ - "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1", - "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2" + "sha256:4bfacea734ead51495dc47df00421ecfd4ca1f2c0fbe58b9a26eaeddedc31caf", + "sha256:67f8be7c0cf420424bc62d8d7dc40b44e4bb2f7b515f9cc2954fb36e35797656" ], - "markers": "python_version >= '3.6'", - "version": "==1.2.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.14.7" }, "asgiref": { "hashes": [ @@ -75,32 +82,32 @@ "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==4.11.1" }, "bleach": { "hashes": [ - "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", - "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" + "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", + "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" ], "index": "pypi", - "version": "==5.0.1" + "version": "==4.1.0" }, "boto3": { "hashes": [ - "sha256:3225366014949039e6687387242e73f237f0fee0a9b7c20461894f1ee40686b8", - "sha256:b295640bc1be637f8f7c8c8fca70781048d6397196109e59f20541824fab4b67" + "sha256:6194763348545bb1669ce8d03ba104be1ba822daa184613aa10b9303a6a79017", + "sha256:be151711bbb4db53e85dd5bbe506002ce6f2f21fc4e45fcf6d2cf356d32cc4c6" ], "index": "pypi", - "version": "==1.24.91" + "version": "==1.24.84" }, "botocore": { "hashes": [ - "sha256:1d6e97bd8653f732c7078b34aa2bb438e750898957e5a0a74b6c72918bc1d0f7", - "sha256:c8fac203a391cc2e4b682877bfce70e723e33c529b35b399a1d574605fbeb1af" + "sha256:11f05d2acdf9a5f722856704b7b951b180647fb4340e1b5048b27273dc323909", + "sha256:da15026329706caf83323d84996f5ff5c527837347633fca9b3b1be0efa60841" ], "markers": "python_version >= '3.7'", - "version": "==1.27.91" + "version": "==1.27.84" }, "bs4": { "hashes": [ @@ -204,27 +211,27 @@ }, "channels-redis": { "hashes": [ - "sha256:122414f29f525f7b9e0c9d59cdcfc4dc1b0eecba16fbb6a1c23f1d9b58f49dcb", - "sha256:81b59d68f53313e1aa891f23591841b684abb936b42e4d1a966d9e4dc63a95ec" + "sha256:78e4a2f2b2a744fe5a87848ec36b5ee49f522c6808cefe6c583663d0d531faa8", + "sha256:ba7e2ad170f273c372812dd32aaac102d68d4e508172abb1cfda3160b7333890" ], "index": "pypi", - "version": "==4.0.0" + "version": "==3.4.1" }, "charset-normalizer": { "hashes": [ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.1.1" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", + "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" + "index": "pypi", + "version": "==8.0.4" }, "conditional": { "hashes": [ @@ -313,11 +320,11 @@ }, "django": { "hashes": [ - "sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793", - "sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f" + "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713", + "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b" ], "index": "pypi", - "version": "==4.1.2" + "version": "==3.2.15" }, "django-clone": { "hashes": [ @@ -445,6 +452,53 @@ "markers": "python_version >= '3.7'", "version": "==0.14.0" }, + "hiredis": { + "hashes": [ + "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e", + "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27", + "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163", + "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc", + "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26", + "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e", + "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579", + "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a", + "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048", + "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87", + "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63", + "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54", + "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05", + "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb", + "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea", + "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5", + "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e", + "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc", + "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99", + "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a", + "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581", + "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426", + "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db", + "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a", + "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a", + "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d", + "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443", + "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79", + "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d", + "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9", + "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d", + "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485", + "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5", + "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048", + "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0", + "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6", + "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41", + "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298", + "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce", + "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", + "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.0" + }, "httptools": { "hashes": [ "sha256:0297822cea9f90a38df29f48e40b42ac3d48a28637368f3ec6d15eebefd182f9", @@ -501,20 +555,20 @@ }, "ics": { "hashes": [ - "sha256:5fcf4d29ec6e7dfcb84120abd617bbba632eb77b097722b7df70e48dbcf26103", - "sha256:6743539bca10391635249b87d74fcd1094af20b82098bebf7c7521df91209f05", - "sha256:da352bdf8418619dc93611e6d251f3cefbb42664777f6e00b580a722098124b7" + "sha256:3b606205b9582ad27dff77f9b227a30d02fdac532731927fe39df1f1ddf8673f", + "sha256:81113a2bb3166c1afcd71cd450c968d40efc385601e9d8344733e00ad8f53429", + "sha256:bf5fbdef6e1e073afdadf1b996f0271186dd114a148e38e795919a1ae644d6ac" ], "index": "pypi", - "version": "==0.7.2" + "version": "==0.7" }, "identify": { "hashes": [ - "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245", - "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa" + "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6", + "sha256:ef78c0d96098a3b5fe7720be4a97e73f439af7cf088ebf47b620aeaa10fadf97" ], "markers": "python_version >= '3.7'", - "version": "==2.5.6" + "version": "==2.5.5" }, "idna": { "hashes": [ @@ -526,10 +580,10 @@ }, "incremental": { "hashes": [ - "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0", - "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51" + "sha256:02f5de5aff48f6b9f665d99d48bfc7ec03b6e3943210de7cfc88856d755d6f57", + "sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321" ], - "version": "==22.10.0" + "version": "==21.3.0" }, "jmespath": { "hashes": [ @@ -541,11 +595,11 @@ }, "jsonref": { "hashes": [ - "sha256:68b330c6815dc0d490dbb3d65ccda265ddde9f7856fd2f3322f971d456ea7549", - "sha256:9480ad1b500f7e795daeb0ef29f9c55ae3a9ab38fb8d6659b6f4868acb5a5bc8" + "sha256:b1e82fa0b62e2c2796a13e5401fe51790b248f6d9bf9d7212a3e31a3501b291f", + "sha256:f3c45b121cf6257eafabdc3a8008763aed1cd7da06dbabc59a9e4d2a5e4e6697" ], "index": "pypi", - "version": "==0.3.0" + "version": "==0.2" }, "lxml": { "hashes": [ @@ -714,11 +768,11 @@ }, "phonenumbers": { "hashes": [ - "sha256:057d1966962fb86b3dc447bfac2c8e25ceed774509e49b180926a13a99910318", - "sha256:0b234c4a9519fac18d00b3c542f5b429513ea69372d4f95fbd0f716f5e2a89b5" + "sha256:80a7422cf0999a6f9b7a2e6cfbdbbfcc56ab5b75414dc3b805bbec91276b64a3", + "sha256:82a4f226c930d02dcdf6d4b29e4cfd8678991fe65c2efd5fdd143557186f0868" ], "index": "pypi", - "version": "==8.12.57" + "version": "==8.12.56" }, "pillow": { "hashes": [ @@ -802,20 +856,20 @@ }, "psycopg2": { "hashes": [ - "sha256:07b90a24d5056687781ddaef0ea172fd951f2f7293f6ffdd03d4f5077801f426", - "sha256:1da77c061bdaab450581458932ae5e469cc6e36e0d62f988376e9f513f11cb5c", - "sha256:46361c054df612c3cc813fdb343733d56543fb93565cff0f8ace422e4da06acb", - "sha256:839f9ea8f6098e39966d97fcb8d08548fbc57c523a1e27a1f0609addf40f777c", - "sha256:849bd868ae3369932127f0771c08d1109b254f08d48dc42493c3d1b87cb2d308", - "sha256:8de6a9fc5f42fa52f559e65120dcd7502394692490c98fed1221acf0819d7797", - "sha256:a11946bad3557ca254f17357d5a4ed63bdca45163e7a7d2bfb8e695df069cc3a", - "sha256:aa184d551a767ad25df3b8d22a0a62ef2962e0e374c04f6cbd1204947f540d61", - "sha256:aafa96f2da0071d6dd0cbb7633406d99f414b40ab0f918c9d9af7df928a1accb", - "sha256:c7fa041b4acb913f6968fce10169105af5200f296028251d817ab37847c30184", - "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f" + "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c", + "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf", + "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362", + "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7", + "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461", + "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126", + "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981", + "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56", + "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305", + "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2", + "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca" ], "index": "pypi", - "version": "==2.9.4" + "version": "==2.9.3" }, "pyasn1": { "hashes": [ @@ -910,10 +964,10 @@ }, "pytz": { "hashes": [ - "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91", - "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174" + "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", + "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" ], - "version": "==2022.4" + "version": "==2022.2.1" }, "pyyaml": { "hashes": [ @@ -1002,11 +1056,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:2469240f6190aaebcb453033519eae69cfe8cc602065b4667e18ee14fc1e35dc", - "sha256:4fbace9a763285b608c06f01a807b51acb35f6059da6a01236654e08b0ee81ff" + "sha256:d6c71d2f85710b66822adaa954af7912bab135d6c85febd5b0f3dfd4ab37e181", + "sha256:ef925b5338625448645a778428d8f22a3d17de8b28cc8e6fba60b93393ad86fe" ], "index": "pypi", - "version": "==1.9.10" + "version": "==1.9.9" }, "service-identity": { "hashes": [ @@ -1017,11 +1071,11 @@ }, "setuptools": { "hashes": [ - "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17", - "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356" + "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012", + "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e" ], "markers": "python_version >= '3.7'", - "version": "==65.5.0" + "version": "==65.4.1" }, "six": { "hashes": [ @@ -1116,11 +1170,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", + "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" ], "markers": "python_version >= '3.7'", - "version": "==4.4.0" + "version": "==4.3.0" }, "unittest-xml-reporting": { "hashes": [ @@ -1379,46 +1433,60 @@ }, "zope.interface": { "hashes": [ - "sha256:006f8dd81fae28027fc28ada214855166712bf4f0bfbc5a8788f9b70982b9437", - "sha256:03f5ae315db0d0de668125d983e2a819a554f3fdb2d53b7e934e3eb3c3c7375d", - "sha256:0eb2b3e84f48dd9cfc8621c80fba905d7e228615c67f76c7df7c716065669bb6", - "sha256:1e3495bb0cdcea212154e558082c256f11b18031f05193ae2fb85d048848db14", - "sha256:26c1456520fdcafecc5765bec4783eeafd2e893eabc636908f50ee31fe5c738c", - "sha256:2cb3003941f5f4fa577479ac6d5db2b940acb600096dd9ea9bf07007f5cab46f", - "sha256:37ec9ade9902f412cc7e7a32d71f79dec3035bad9bd0170226252eed88763c48", - "sha256:3eedf3d04179774d750e8bb4463e6da350956a50ed44d7b86098e452d7ec385e", - "sha256:3f68404edb1a4fb6aa8a94675521ca26c83ebbdbb90e894f749ae0dc4ca98418", - "sha256:423c074e404f13e6fa07f4454f47fdbb38d358be22945bc812b94289d9142374", - "sha256:43490ad65d4c64e45a30e51a2beb7a6b63e1ff395302ad22392224eb618476d6", - "sha256:47ff078734a1030c48103422a99e71a7662d20258c00306546441adf689416f7", - "sha256:58a66c2020a347973168a4a9d64317bac52f9fdfd3e6b80b252be30da881a64e", - "sha256:58a975f89e4584d0223ab813c5ba4787064c68feef4b30d600f5e01de90ae9ce", - "sha256:5c6023ae7defd052cf76986ce77922177b0c2f3913bea31b5b28fbdf6cb7099e", - "sha256:6566b3d2657e7609cd8751bcb1eab1202b1692a7af223035a5887d64bb3a2f3b", - "sha256:687cab7f9ae18d2c146f315d0ca81e5ffe89a139b88277afa70d52f632515854", - "sha256:700ebf9662cf8df70e2f0cb4988e078c53f65ee3eefd5c9d80cf988c4175c8e3", - "sha256:740f3c1b44380658777669bcc42f650f5348e53797f2cee0d93dc9b0f9d7cc69", - "sha256:7bdcec93f152e0e1942102537eed7b166d6661ae57835b20a52a2a3d6a3e1bf3", - "sha256:7d9ec1e6694af39b687045712a8ad14ddcb568670d5eb1b66b48b98b9312afba", - "sha256:85dd6dd9aaae7a176948d8bb62e20e2968588fd787c29c5d0d964ab475168d3d", - "sha256:8b9f153208d74ccfa25449a0c6cb756ab792ce0dc99d9d771d935f039b38740c", - "sha256:8c791f4c203ccdbcda588ea4c8a6e4353e10435ea48ddd3d8734a26fe9714cba", - "sha256:970661ece2029915b8f7f70892e88404340fbdefd64728380cad41c8dce14ff4", - "sha256:9cdc4e898d3b1547d018829fd4a9f403e52e51bba24be0fbfa37f3174e1ef797", - "sha256:9dc4493aa3d87591e3d2bf1453e25b98038c839ca8e499df3d7106631b66fe83", - "sha256:a69c28d85bb7cf557751a5214cb3f657b2b035c8c96d71080c1253b75b79b69b", - "sha256:aeac590cce44e68ee8ad0b8ecf4d7bf15801f102d564ca1b0eb1f12f584ee656", - "sha256:be11fce0e6af6c0e8d93c10ef17b25aa7c4acb7ec644bff2596c0d639c49e20f", - "sha256:cbbf83914b9a883ab324f728de869f4e406e0cbcd92df7e0a88decf6f9ab7d5a", - "sha256:cfa614d049667bed1c737435c609c0956c5dc0dbafdc1145ee7935e4658582cb", - "sha256:d18fb0f6c8169d26044128a2e7d3c39377a8a151c564e87b875d379dbafd3930", - "sha256:d80f6236b57a95eb19d5e47eb68d0296119e1eff6deaa2971ab8abe3af918420", - "sha256:da7912ae76e1df6a1fb841b619110b1be4c86dfb36699d7fd2f177105cdea885", - "sha256:df6593e150d13cfcce69b0aec5df7bc248cb91e4258a7374c129bb6d56b4e5ca", - "sha256:f70726b60009433111fe9928f5d89cbb18962411d33c45fb19eb81b9bbd26fcd" + "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192", + "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702", + "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09", + "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4", + "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a", + "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3", + "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf", + "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c", + "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d", + "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78", + "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83", + "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531", + "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46", + "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021", + "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94", + "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc", + "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63", + "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54", + "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117", + "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25", + "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05", + "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e", + "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1", + "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004", + "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2", + "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e", + "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f", + "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f", + "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120", + "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f", + "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1", + "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9", + "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e", + "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7", + "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8", + "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b", + "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155", + "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7", + "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c", + "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325", + "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d", + "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb", + "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e", + "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959", + "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7", + "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920", + "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e", + "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48", + "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8", + "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", + "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.5.0" + "version": "==5.4.0" } }, "develop": { @@ -1466,16 +1534,16 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.1.1" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", + "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" + "index": "pypi", + "version": "==8.0.4" }, "codecov": { "hashes": [ @@ -1544,11 +1612,11 @@ }, "django": { "hashes": [ - "sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793", - "sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f" + "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713", + "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b" ], "index": "pypi", - "version": "==4.1.2" + "version": "==3.2.15" }, "django-debug-toolbar": { "hashes": [ @@ -1576,11 +1644,11 @@ }, "flake8-isort": { "hashes": [ - "sha256:c73f9cbd1bf209887f602a27b827164ccfeba1676801b2aa23cb49051a1be79c", - "sha256:e336f928c7edc509684930ab124414194b7f4e237c712af8fcbdf49d8747b10c" + "sha256:26571500cd54976bbc0cf1006ffbcd1a68dd102f816b7a1051b219616ba9fee0", + "sha256:5b87630fb3719bf4c1833fd11e0d9534f43efdeba524863e15d8f14a7ef6adbf" ], "index": "pypi", - "version": "==5.0.0" + "version": "==4.2.0" }, "flake8-quotes": { "hashes": [ @@ -1713,6 +1781,13 @@ "markers": "python_version >= '3.6'", "version": "==2.5.0" }, + "pytz": { + "hashes": [ + "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", + "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" + ], + "version": "==2022.2.1" + }, "regex": { "hashes": [ "sha256:003a2e1449d425afc817b5f0b3d4c4aa9072dd5f3dfbf6c7631b8dc7b13233de", From 094ad428d3469b94456b733d12222ff9012ad0c9 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sun, 16 Oct 2022 13:50:17 -0400 Subject: [PATCH 17/44] lint and pre-commit fixes --- .pre-commit-config.yaml | 2 +- backend/clubs/models.py | 8 ++------ backend/clubs/serializers.py | 10 +++------- backend/clubs/views.py | 27 +++++++-------------------- 4 files changed, 13 insertions(+), 34 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72e754069..9f5abe424 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: black name: black - entry: black + entry: cd backend && pipenv run black language: python types: [python] require_serial: true diff --git a/backend/clubs/models.py b/backend/clubs/models.py index a9f90de55..4e7026497 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1574,9 +1574,7 @@ class ApplicationCommittee(models.Model): name = models.TextField(blank=True) application = models.ForeignKey( - ClubApplication, - related_name="committees", - on_delete=models.CASCADE, + ClubApplication, related_name="committees", on_delete=models.CASCADE, ) def get_word_limit(self): @@ -1628,9 +1626,7 @@ class ApplicationMultipleChoice(models.Model): value = models.TextField(blank=True) question = models.ForeignKey( - ApplicationQuestion, - related_name="multiple_choice", - on_delete=models.CASCADE, + ApplicationQuestion, related_name="multiple_choice", on_delete=models.CASCADE, ) diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index 9e3286308..c6f0f62de 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -2012,9 +2012,7 @@ def get_clubs(self, obj): # hide non public memberships if not superuser if user is None or not user.has_perm("clubs.manage_club"): queryset = queryset.filter( - membership__person=obj, - membership__public=True, - approved=True, + membership__person=obj, membership__public=True, approved=True, ) serializer = MembershipClubListSerializer( @@ -2415,8 +2413,7 @@ def save(self): ApplicationMultipleChoice.objects.filter(question=question_obj).delete() for choice in multiple_choice: ApplicationMultipleChoice.objects.create( - value=choice["value"], - question=question_obj, + value=choice["value"], question=question_obj, ) # manually create committee choices as Django does not @@ -2705,8 +2702,7 @@ def save(self): for name in committees: if name not in prev_committee_names: ApplicationCommittee.objects.create( - name=name, - application=application_obj, + name=name, application=application_obj, ) return application_obj diff --git a/backend/clubs/views.py b/backend/clubs/views.py index a20f0491f..2a27f7c2d 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -1080,8 +1080,7 @@ def get_queryset(self): self.request.user, get_user_model() ): SearchQuery( - person=self.request.user, - query=self.request.query_params.get("search"), + person=self.request.user, query=self.request.query_params.get("search"), ).save() # select subset of clubs if requested @@ -1673,9 +1672,7 @@ def constitutions(self, request, *args, **kwargs): query = ( Club.objects.filter(badges=badge, archived=False) .order_by(Lower("name")) - .prefetch_related( - Prefetch("asset_set", to_attr="prefetch_asset_set"), - ) + .prefetch_related(Prefetch("asset_set", to_attr="prefetch_asset_set"),) ) if request.user.is_authenticated: query = query.prefetch_related( @@ -3222,9 +3219,7 @@ def destroy(self, request, *args, **kwargs): def get_queryset(self): return MembershipRequest.objects.filter( - person=self.request.user, - withdrew=False, - club__archived=False, + person=self.request.user, withdrew=False, club__archived=False, ) @@ -4348,9 +4343,7 @@ def get(self, request): try: response = zoom_api_call( - request.user, - "GET", - "https://api.zoom.us/v2/users/{uid}/settings", + request.user, "GET", "https://api.zoom.us/v2/users/{uid}/settings", ) except requests.exceptions.HTTPError as e: raise DRFValidationError( @@ -4471,9 +4464,7 @@ def get_operation_id(self, **kwargs): def get_object(self): user = self.request.user prefetch_related_objects( - [user], - "profile__school", - "profile__major", + [user], "profile__school", "profile__major", ) return user @@ -4906,9 +4897,7 @@ def question_response(self, *args, **kwargs): } ) submission = ApplicationSubmission.objects.create( - user=self.request.user, - application=application, - committee=committee, + user=self.request.user, application=application, committee=committee, ) for question_pk in questions: question = ApplicationQuestion.objects.filter(pk=question_pk).first() @@ -4929,9 +4918,7 @@ def question_response(self, *args, **kwargs): text = question_data.get("text", None) if text is not None and text != "": obj = ApplicationQuestionResponse.objects.create( - text=text, - question=question, - submission=submission, + text=text, question=question, submission=submission, ).save() response = Response(ApplicationQuestionResponseSerializer(obj).data) elif question_type == ApplicationQuestion.MULTIPLE_CHOICE: From d67a3a67bd8638c89aee0431d9a3c455a8b8ffd7 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sun, 16 Oct 2022 19:20:17 -0400 Subject: [PATCH 18/44] minor bugs --- backend/clubs/serializers.py | 12 ++++++++---- backend/clubs/views.py | 29 +++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index c6f0f62de..1f803a32a 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -344,7 +344,7 @@ class ClubEventSerializer(serializers.ModelSerializer): creator = serializers.HiddenField(default=serializers.CurrentUserDefault()) def get_ticketed(self, obj) -> bool: - return Event.tickets.exists() + return obj.tickets.count() > 0 def get_event_url(self, obj): # if no url, return that @@ -2012,7 +2012,9 @@ def get_clubs(self, obj): # hide non public memberships if not superuser if user is None or not user.has_perm("clubs.manage_club"): queryset = queryset.filter( - membership__person=obj, membership__public=True, approved=True, + membership__person=obj, + membership__public=True, + approved=True, ) serializer = MembershipClubListSerializer( @@ -2413,7 +2415,8 @@ def save(self): ApplicationMultipleChoice.objects.filter(question=question_obj).delete() for choice in multiple_choice: ApplicationMultipleChoice.objects.create( - value=choice["value"], question=question_obj, + value=choice["value"], + question=question_obj, ) # manually create committee choices as Django does not @@ -2702,7 +2705,8 @@ def save(self): for name in committees: if name not in prev_committee_names: ApplicationCommittee.objects.create( - name=name, application=application_obj, + name=name, + application=application_obj, ) return application_obj diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 2a27f7c2d..6957425f1 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -1080,7 +1080,8 @@ def get_queryset(self): self.request.user, get_user_model() ): SearchQuery( - person=self.request.user, query=self.request.query_params.get("search"), + person=self.request.user, + query=self.request.query_params.get("search"), ).save() # select subset of clubs if requested @@ -1672,7 +1673,9 @@ def constitutions(self, request, *args, **kwargs): query = ( Club.objects.filter(badges=badge, archived=False) .order_by(Lower("name")) - .prefetch_related(Prefetch("asset_set", to_attr="prefetch_asset_set"),) + .prefetch_related( + Prefetch("asset_set", to_attr="prefetch_asset_set"), + ) ) if request.user.is_authenticated: query = query.prefetch_related( @@ -2371,7 +2374,7 @@ def buyers(self, request, *args, **kwargs): ) buyers = tickets.filter(owner__isnull=False).values( - "id", "fullname", "owner_id", "type" + "fullname", "id", "owner_id", "type" ) return Response({"buyers": buyers}) @@ -3219,7 +3222,9 @@ def destroy(self, request, *args, **kwargs): def get_queryset(self): return MembershipRequest.objects.filter( - person=self.request.user, withdrew=False, club__archived=False, + person=self.request.user, + withdrew=False, + club__archived=False, ) @@ -4343,7 +4348,9 @@ def get(self, request): try: response = zoom_api_call( - request.user, "GET", "https://api.zoom.us/v2/users/{uid}/settings", + request.user, + "GET", + "https://api.zoom.us/v2/users/{uid}/settings", ) except requests.exceptions.HTTPError as e: raise DRFValidationError( @@ -4464,7 +4471,9 @@ def get_operation_id(self, **kwargs): def get_object(self): user = self.request.user prefetch_related_objects( - [user], "profile__school", "profile__major", + [user], + "profile__school", + "profile__major", ) return user @@ -4897,7 +4906,9 @@ def question_response(self, *args, **kwargs): } ) submission = ApplicationSubmission.objects.create( - user=self.request.user, application=application, committee=committee, + user=self.request.user, + application=application, + committee=committee, ) for question_pk in questions: question = ApplicationQuestion.objects.filter(pk=question_pk).first() @@ -4918,7 +4929,9 @@ def question_response(self, *args, **kwargs): text = question_data.get("text", None) if text is not None and text != "": obj = ApplicationQuestionResponse.objects.create( - text=text, question=question, submission=submission, + text=text, + question=question, + submission=submission, ).save() response = Response(ApplicationQuestionResponseSerializer(obj).data) elif question_type == ApplicationQuestion.MULTIPLE_CHOICE: From a352b01d391a61f1eef4b7550dfc139b99a9a420 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sat, 12 Nov 2022 13:24:32 -0500 Subject: [PATCH 19/44] add to populate script --- backend/clubs/management/commands/populate.py | 34 ++++++++++++++++++- backend/clubs/views.py | 5 ++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/backend/clubs/management/commands/populate.py b/backend/clubs/management/commands/populate.py index 0ea8e7d63..5de5d06c5 100644 --- a/backend/clubs/management/commands/populate.py +++ b/backend/clubs/management/commands/populate.py @@ -15,6 +15,7 @@ ApplicationQuestion, ApplicationSubmission, Badge, + Cart, Club, ClubApplication, ClubFair, @@ -28,6 +29,7 @@ StudentType, Tag, Testimonial, + Ticket, Year, ) @@ -662,7 +664,10 @@ def get_image(url): status_counter % len(ApplicationSubmission.STATUS_TYPES) ][0] ApplicationSubmission.objects.create( - status=status, user=user, application=application, committee=None, + status=status, + user=user, + application=application, + committee=None, ) status_counter += 1 for committee in application.committees.all(): @@ -746,4 +751,31 @@ def get_image(url): first_mship.save() count += 1 + # add tickets + + hr = Club.objects.get(code="harvard-rejects") + + hr_events = Event.objects.filter(club=hr) + + for idx, e in enumerate(hr_events[:3]): + # Switch up person every so often + person = ben if idx < 2 else user_objs[1] + + # Create some unowned tickets + Ticket.objects.bulk_create( + [Ticket(event=e, type="Regular") for _ in range(10)] + ) + + Ticket.objects.bulk_create( + [Ticket(event=e, type="Premium") for _ in range(5)] + ) + + # Create some owned tickets and tickets in cart + for i in range((idx + 1) * 10): + if i % 5: + Ticket.objects.create(event=e, owner=person, type="Regular") + else: + c, _ = Cart.objects.get_or_create(owner=person) + c.tickets.add(Ticket.objects.create(event=e, type="Premium")) + self.stdout.write("Finished populating database!") diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 6957425f1..3fd0e4241 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2436,6 +2436,8 @@ def tickets(self, request, *args, **kwargs): @transaction.atomic def create_tickets(self, request, *args, **kwargs): """ + Create ticket offerings for event + --- requestBody: content: application/json: @@ -2456,12 +2458,13 @@ def create_tickets(self, request, *args, **kwargs): content: application/json: schema: - type: object properties: detail: type: string + --- """ event = self.get_object() + quantities = request.data.get("quantities") # Atomicity ensures idempotency From 7367db1c7393648d0288db9809d5fcb5bfa2939c Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sat, 12 Nov 2022 13:43:17 -0500 Subject: [PATCH 20/44] lint --- backend/clubs/management/commands/populate.py | 7 ++--- backend/clubs/serializers.py | 10 +++---- backend/clubs/views.py | 27 +++++-------------- 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/backend/clubs/management/commands/populate.py b/backend/clubs/management/commands/populate.py index 5de5d06c5..a499bacef 100644 --- a/backend/clubs/management/commands/populate.py +++ b/backend/clubs/management/commands/populate.py @@ -664,10 +664,7 @@ def get_image(url): status_counter % len(ApplicationSubmission.STATUS_TYPES) ][0] ApplicationSubmission.objects.create( - status=status, - user=user, - application=application, - committee=None, + status=status, user=user, application=application, committee=None, ) status_counter += 1 for committee in application.committees.all(): @@ -751,7 +748,7 @@ def get_image(url): first_mship.save() count += 1 - # add tickets + # Add tickets hr = Club.objects.get(code="harvard-rejects") diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index 1f803a32a..f44761caa 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -2012,9 +2012,7 @@ def get_clubs(self, obj): # hide non public memberships if not superuser if user is None or not user.has_perm("clubs.manage_club"): queryset = queryset.filter( - membership__person=obj, - membership__public=True, - approved=True, + membership__person=obj, membership__public=True, approved=True, ) serializer = MembershipClubListSerializer( @@ -2415,8 +2413,7 @@ def save(self): ApplicationMultipleChoice.objects.filter(question=question_obj).delete() for choice in multiple_choice: ApplicationMultipleChoice.objects.create( - value=choice["value"], - question=question_obj, + value=choice["value"], question=question_obj, ) # manually create committee choices as Django does not @@ -2705,8 +2702,7 @@ def save(self): for name in committees: if name not in prev_committee_names: ApplicationCommittee.objects.create( - name=name, - application=application_obj, + name=name, application=application_obj, ) return application_obj diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 3fd0e4241..550fa3c6d 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -1080,8 +1080,7 @@ def get_queryset(self): self.request.user, get_user_model() ): SearchQuery( - person=self.request.user, - query=self.request.query_params.get("search"), + person=self.request.user, query=self.request.query_params.get("search"), ).save() # select subset of clubs if requested @@ -1673,9 +1672,7 @@ def constitutions(self, request, *args, **kwargs): query = ( Club.objects.filter(badges=badge, archived=False) .order_by(Lower("name")) - .prefetch_related( - Prefetch("asset_set", to_attr="prefetch_asset_set"), - ) + .prefetch_related(Prefetch("asset_set", to_attr="prefetch_asset_set"),) ) if request.user.is_authenticated: query = query.prefetch_related( @@ -3225,9 +3222,7 @@ def destroy(self, request, *args, **kwargs): def get_queryset(self): return MembershipRequest.objects.filter( - person=self.request.user, - withdrew=False, - club__archived=False, + person=self.request.user, withdrew=False, club__archived=False, ) @@ -4351,9 +4346,7 @@ def get(self, request): try: response = zoom_api_call( - request.user, - "GET", - "https://api.zoom.us/v2/users/{uid}/settings", + request.user, "GET", "https://api.zoom.us/v2/users/{uid}/settings", ) except requests.exceptions.HTTPError as e: raise DRFValidationError( @@ -4474,9 +4467,7 @@ def get_operation_id(self, **kwargs): def get_object(self): user = self.request.user prefetch_related_objects( - [user], - "profile__school", - "profile__major", + [user], "profile__school", "profile__major", ) return user @@ -4909,9 +4900,7 @@ def question_response(self, *args, **kwargs): } ) submission = ApplicationSubmission.objects.create( - user=self.request.user, - application=application, - committee=committee, + user=self.request.user, application=application, committee=committee, ) for question_pk in questions: question = ApplicationQuestion.objects.filter(pk=question_pk).first() @@ -4932,9 +4921,7 @@ def question_response(self, *args, **kwargs): text = question_data.get("text", None) if text is not None and text != "": obj = ApplicationQuestionResponse.objects.create( - text=text, - question=question, - submission=submission, + text=text, question=question, submission=submission, ).save() response = Response(ApplicationQuestionResponseSerializer(obj).data) elif question_type == ApplicationQuestion.MULTIPLE_CHOICE: From 7f650dc7d77c8118e5db9e3efccf40a47132f750 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Sat, 12 Nov 2022 15:12:36 -0500 Subject: [PATCH 21/44] rebase, fix some documentation --- backend/clubs/migrations/0091_cart_ticket.py | 2 +- .../migrations/0092_auto_20220211_1732.py | 55 ------------------- backend/clubs/views.py | 30 ++++++---- backend/tests/clubs/test_documentation.py | 2 +- 4 files changed, 22 insertions(+), 67 deletions(-) delete mode 100644 backend/clubs/migrations/0092_auto_20220211_1732.py diff --git a/backend/clubs/migrations/0091_cart_ticket.py b/backend/clubs/migrations/0091_cart_ticket.py index 758aa2e30..74ae8cdd9 100644 --- a/backend/clubs/migrations/0091_cart_ticket.py +++ b/backend/clubs/migrations/0091_cart_ticket.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2022-10-02 16:50 +# Generated by Django 3.2.8 on 2022-11-12 20:05 import uuid diff --git a/backend/clubs/migrations/0092_auto_20220211_1732.py b/backend/clubs/migrations/0092_auto_20220211_1732.py deleted file mode 100644 index ee9adba59..000000000 --- a/backend/clubs/migrations/0092_auto_20220211_1732.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 3.2.8 on 2022-02-11 22:32 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("clubs", "0091_ticket"), - ] - - operations = [ - migrations.AddField( - model_name="ticket", - name="held", - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name="ticket", - name="holding_expiration", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.CreateModel( - name="Cart", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "owner", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="cart", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.AddField( - model_name="ticket", - name="carts", - field=models.ManyToManyField( - blank=True, related_name="tickets", to="clubs.Cart" - ), - ), - ] diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 550fa3c6d..f8e3bcefd 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -27,9 +27,17 @@ from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import validate_email from django.db import transaction -from django.db.models import Count, DurationField, ExpressionWrapper, F, Prefetch, Q +from django.db.models import ( + Count, + DurationField, + ExpressionWrapper, + F, + Prefetch, + Q, + TextField, +) from django.db.models.expressions import RawSQL, Value -from django.db.models.functions import Lower, Trunc +from django.db.models.functions import SHA1, Lower, Trunc from django.db.models.functions.text import Concat from django.db.models.query import prefetch_related_objects from django.http import HttpResponse @@ -2263,6 +2271,7 @@ def add_to_cart(self, request, *args, **kwargs): content: application/json: schema: + type: object properties: detail: type: string @@ -2270,6 +2279,7 @@ def add_to_cart(self, request, *args, **kwargs): content: application/json: schema: + type: object properties: detail: type: string @@ -2319,6 +2329,7 @@ def remove_from_cart(self, request, *args, **kwargs): content: application/json: schema: + type: object properties: detail: type: string @@ -2361,7 +2372,7 @@ def buyers(self, request, *args, **kwargs): id: type: string owner_id: - type: int + type: integer type: type: string --- @@ -2455,6 +2466,7 @@ def create_tickets(self, request, *args, **kwargs): content: application/json: schema: + type: object properties: detail: type: string @@ -2496,6 +2508,7 @@ def upload(self, request, *args, **kwargs): description: Returned if the file was successfully uploaded. content: &upload_resp application/json: + schema: type: object properties: detail: @@ -4565,17 +4578,13 @@ def checkout(self, request, *args, **kwargs): NOTE: this does NOT buy tickets, it simply initiates a checkout process which includes a 10-minute ticket hold --- - requestBody: - content: - application/json: - schema: - type: object - + requestBody: {} responses: "200": content: application/json: schema: + type: object properties: detail: type: string @@ -4612,10 +4621,11 @@ def checkout_success_callback(self, request, *args, **kwargs): content: application/json: schema: + type: object properties: detail: type: string - + --- """ cart = get_object_or_404(Cart, owner=self.request.user) diff --git a/backend/tests/clubs/test_documentation.py b/backend/tests/clubs/test_documentation.py index 01865073a..67bfe8170 100644 --- a/backend/tests/clubs/test_documentation.py +++ b/backend/tests/clubs/test_documentation.py @@ -204,8 +204,8 @@ def test_openapi_docs(self): ) if "application/json" in content["content"]: json_content = content["content"]["application/json"] - self.assertTrue("schema" in json_content) try: + self.assertTrue("schema" in json_content) self.verify_schema(json_content["schema"]) except AssertionError as e: raise AssertionError( From 03214afe42d3574029b38b47947611a622ce4fe6 Mon Sep 17 00:00:00 2001 From: Avi Upadhyayula <69180850+aviupadhyayula@users.noreply.github.com> Date: Tue, 13 Feb 2024 00:39:24 -0500 Subject: [PATCH 22/44] Update ticketing (backend) branch (#612) * Merge master into ticketing * Move ticketing migration to end --- .github/cdk/.gitignore | 3 - .github/cdk/cdkactions.yaml | 2 - .github/cdk/main.ts | 14 - .github/cdk/package.json | 25 - .github/cdk/tsconfig.json | 33 - .github/cdk/yarn.lock | 206 -- .github/dependabot.yml | 12 - .github/workflows/build-and-deploy.yaml | 122 + .../cdkactions_build-and-deploy.yaml | 241 -- .github/workflows/cdkactions_validate.yaml | 28 - .gitignore | 3 + .pre-commit-config.yaml | 5 +- README.md | 14 +- backend/Dockerfile | 2 +- backend/Pipfile | 9 +- backend/Pipfile.lock | 2087 ++++++------- backend/clubs/admin.py | 11 +- backend/clubs/management/commands/populate.py | 12 +- .../management/commands/update_club_counts.py | 36 + .../commands/wharton_council_application.py | 135 - .../{0090_adminnote.py => 0088_adminnote.py} | 2 +- ...0088_alter_applicationsubmission_status.py | 45 - ...0089_alter_applicationsubmission_status.py | 26 - .../migrations/0089_auto_20230103_1239.py | 60 + .../migrations/0090_auto_20230106_1443.py | 40 + .../migrations/0091_applicationextension.py | 47 + ...lication_application_end_time_exception.py | 18 + .../migrations/0092_merge_20240106_1117.py | 13 + .../migrations/0093_auto_20240106_1153.py | 23 + .../0094_applicationcycle_release_date.py | 18 + .../migrations/0095_rm_field_add_count.py | 34 + ...091_cart_ticket.py => 0096_cart_ticket.py} | 2 +- backend/clubs/mixins.py | 2 +- backend/clubs/models.py | 92 +- backend/clubs/serializers.py | 207 +- backend/clubs/urls.py | 17 +- backend/clubs/views.py | 947 +++++- backend/pennclubs/settings/base.py | 2 +- backend/pennclubs/settings/production.py | 7 +- .../emails/application_extension.html | 22 + backend/templates/emails/renew.html | 2 +- .../templates/emails/renewal_reminder.html | 6 +- backend/tests/clubs/test_views.py | 70 +- frontend/components/Applications.tsx | 150 +- .../ClubEditPage/ApplicationsCard.tsx | 66 +- .../ClubEditPage/ApplicationsPage.tsx | 547 +++- frontend/components/ClubPage/Actions.tsx | 37 +- frontend/components/ClubPage/InfoBox.tsx | 6 +- .../ClubPage/RenewalRequestDialog.tsx | 4 +- frontend/components/CustomOption.tsx | 69 + frontend/components/DisplayButtons.tsx | 23 +- frontend/components/FormComponents.tsx | 90 +- frontend/components/Header/Links.tsx | 8 + frontend/components/ModelForm.tsx | 32 +- frontend/components/SearchBar.tsx | 2 +- .../Settings/WhartonApplicationCycles.tsx | 327 +++ .../Settings/WhartonApplicationStatus.tsx | 15 + frontend/components/Submissions.tsx | 18 +- frontend/components/common/Table.tsx | 1 + .../application/[application]/index.tsx | 45 +- frontend/pages/club/[club]/apply.tsx | 8 +- frontend/pages/club/[club]/renew.tsx | 50 +- frontend/pages/fair.tsx | 106 +- frontend/pages/wharton/[[...slug]].tsx | 6 + frontend/types.ts | 13 + frontend/utils.tsx | 4 + frontend/utils/branding.tsx | 508 ++-- k8s/main.ts | 60 +- k8s/package.json | 57 +- k8s/yarn.lock | 2610 ++++++++++------- 70 files changed, 5855 insertions(+), 3709 deletions(-) delete mode 100644 .github/cdk/.gitignore delete mode 100644 .github/cdk/cdkactions.yaml delete mode 100644 .github/cdk/main.ts delete mode 100644 .github/cdk/package.json delete mode 100644 .github/cdk/tsconfig.json delete mode 100644 .github/cdk/yarn.lock delete mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build-and-deploy.yaml delete mode 100644 .github/workflows/cdkactions_build-and-deploy.yaml delete mode 100644 .github/workflows/cdkactions_validate.yaml create mode 100644 backend/clubs/management/commands/update_club_counts.py delete mode 100644 backend/clubs/management/commands/wharton_council_application.py rename backend/clubs/migrations/{0090_adminnote.py => 0088_adminnote.py} (95%) delete mode 100644 backend/clubs/migrations/0088_alter_applicationsubmission_status.py delete mode 100644 backend/clubs/migrations/0089_alter_applicationsubmission_status.py create mode 100644 backend/clubs/migrations/0089_auto_20230103_1239.py create mode 100644 backend/clubs/migrations/0090_auto_20230106_1443.py create mode 100644 backend/clubs/migrations/0091_applicationextension.py create mode 100644 backend/clubs/migrations/0091_clubapplication_application_end_time_exception.py create mode 100644 backend/clubs/migrations/0092_merge_20240106_1117.py create mode 100644 backend/clubs/migrations/0093_auto_20240106_1153.py create mode 100644 backend/clubs/migrations/0094_applicationcycle_release_date.py create mode 100644 backend/clubs/migrations/0095_rm_field_add_count.py rename backend/clubs/migrations/{0091_cart_ticket.py => 0096_cart_ticket.py} (98%) create mode 100644 backend/templates/emails/application_extension.html create mode 100644 frontend/components/CustomOption.tsx create mode 100644 frontend/components/Settings/WhartonApplicationCycles.tsx diff --git a/.github/cdk/.gitignore b/.github/cdk/.gitignore deleted file mode 100644 index 794fce505..000000000 --- a/.github/cdk/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -main.js -main.d.ts diff --git a/.github/cdk/cdkactions.yaml b/.github/cdk/cdkactions.yaml deleted file mode 100644 index 626767947..000000000 --- a/.github/cdk/cdkactions.yaml +++ /dev/null @@ -1,2 +0,0 @@ -language: typescript -app: node main.js diff --git a/.github/cdk/main.ts b/.github/cdk/main.ts deleted file mode 100644 index 8a675c301..000000000 --- a/.github/cdk/main.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { App } from "cdkactions"; -import { LabsApplicationStack } from '@pennlabs/kraken'; - - -const app = new App(); -new LabsApplicationStack(app, { - djangoProjectName: 'pennclubs', - dockerImageBaseName: 'penn-clubs', - integrationTests: true, - integrationProps: { - testCommand: 'docker-compose -f docker-compose.test.yaml exec -T frontend yarn integration', - }, -}); -app.synth(); diff --git a/.github/cdk/package.json b/.github/cdk/package.json deleted file mode 100644 index 7606d7034..000000000 --- a/.github/cdk/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "cdk", - "version": "0.1.0", - "main": "main.js", - "types": "main.ts", - "license": "Apache-2.0", - "private": true, - "scripts": { - "synth": "cdkactions synth", - "compile": "tsc", - "watch": "tsc -w", - "build": "yarn compile && yarn synth", - "upgrade-cdk": "yarn upgrade cdkactions@latest cdkactions-cli@latest" - }, - "dependencies": { - "@pennlabs/kraken": "^0.8.6", - "cdkactions": "^0.2.1", - "constructs": "^3.3.136" - }, - "devDependencies": { - "@types/node": "^16.7.5", - "cdkactions-cli": "^0.2.3", - "typescript": "^4.4.2" - } -} diff --git a/.github/cdk/tsconfig.json b/.github/cdk/tsconfig.json deleted file mode 100644 index 936e05cef..000000000 --- a/.github/cdk/tsconfig.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "compilerOptions": { - "alwaysStrict": true, - "charset": "utf8", - "declaration": true, - "experimentalDecorators": true, - "inlineSourceMap": true, - "inlineSources": true, - "lib": [ - "es2018" - ], - "module": "CommonJS", - "noEmitOnError": true, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "resolveJsonModule": true, - "strict": true, - "strictNullChecks": true, - "strictPropertyInitialization": true, - "stripInternal": true, - "target": "ES2018" - }, - "include": [ - "**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/.github/cdk/yarn.lock b/.github/cdk/yarn.lock deleted file mode 100644 index 04c1005e0..000000000 --- a/.github/cdk/yarn.lock +++ /dev/null @@ -1,206 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@pennlabs/kraken@^0.8.6": - version "0.8.6" - resolved "https://registry.yarnpkg.com/@pennlabs/kraken/-/kraken-0.8.6.tgz#79a9d10bed36b699c526556cd69b6d81341847d1" - integrity sha512-aBblQa/661DJ2GP3Dq1KEzCZ72ZV/Jw7z4HNZoWPxGWn+tSPwvaPkSNDpK7tT+nJmu427giGU8DLyciU79hKbA== - dependencies: - cdkactions "^0.2.3" - constructs "^3.2.80" - ts-dedent "^2.2.0" - -"@types/node@^16.7.5": - version "16.7.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.5.tgz#96142b243977b03d99c338fdb09241d286102711" - integrity sha512-E7SpxDXoHEpmZ9C1gSqwadhE6zPRtf3g0gJy9Y51DsImnR5TcDs3QEiV/3Q7zOM8LWaZp5Gph71NK6ElVMG1IQ== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -cdkactions-cli@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/cdkactions-cli/-/cdkactions-cli-0.2.3.tgz#2393682b37ab0b04c6964160b393e8d71b08118f" - integrity sha512-qYPbzuQ1M5gQGa8NRnaWwm3iXmdqMoiHR7YTh6oYROpfBGER7kwBBb6ydFlSwKK62hE0B++by43hbEBXlHvr8A== - dependencies: - cdkactions "^0.2.3" - constructs "^3.2.109" - fs-extra "^8.1.0" - sscaff "^1.2.0" - yaml "^1.10.0" - yargs "^16.2.0" - -cdkactions@^0.2.1, cdkactions@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/cdkactions/-/cdkactions-0.2.3.tgz#aa27bf720962376d54f8ef95cdfb0ab46458b966" - integrity sha512-/DYQ2qsT6fzgZB+cmQjtPqR4aAWCqAytWbFpJK+iJLQ4jQrl6l4uMf01TLiWY3mAILS0YGlwPcoBbGvq9Jnz5g== - dependencies: - js-yaml "^4.0.0" - ts-dedent "^2.0.0" - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -constructs@^3.2.109, constructs@^3.2.80, constructs@^3.3.136: - version "3.3.136" - resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.136.tgz#9a311737bb802f7931a1f38c5223d82fa3efd08d" - integrity sha512-8qGuZTTXxsV3uUtqbajcQhcuu28bcuYG5jODXlXWLpd4bSo+2dMg5vKLr0dsRm/Q3B7op7io1P0opMzaGdCO5A== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -js-yaml@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -sscaff@^1.2.0: - version "1.2.57" - resolved "https://registry.yarnpkg.com/sscaff/-/sscaff-1.2.57.tgz#ebd5b58ec6567f8a6684e5e2245fe2ece24b4c53" - integrity sha512-nQwKlWrf7fls8TJibFM8rMXVZYvcfHc2pSMKNO641p83U5/Aof1KyrnF39X5m2baX9/uZfPxe6SRHBWSpLMfyA== - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -ts-dedent@^2.0.0, ts-dedent@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" - integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== - -typescript@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86" - integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ== - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f81e0b01f..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/frontend" - schedule: - interval: "weekly" - open-pull-requests-limit: 0 - - package-ecosystem: "pip" - directory: "/backend" - schedule: - interval: "weekly" - open-pull-requests-limit: 0 diff --git a/.github/workflows/build-and-deploy.yaml b/.github/workflows/build-and-deploy.yaml new file mode 100644 index 000000000..358e3e6af --- /dev/null +++ b/.github/workflows/build-and-deploy.yaml @@ -0,0 +1,122 @@ +# ======================================== +# Note: If you make changes to this CI/CD, please include someone from DevOps in the list of reviewers for the PR. +# ======================================== +name: Build and Deploy Clubs + +on: push + +jobs: + backend-check: + name: "Backend Check" + uses: pennlabs/shared-actions/.github/workflows/django.yaml@8785a7d7b9158d8d5705a0202f5695db2c0beb97 + with: + projectName: pennclubs + path: backend + flake: true + black: true + + frontend-check: + name: "Frontend Check" + uses: pennlabs/shared-actions/.github/workflows/react-check.yaml@v0.1 + with: + path: frontend + + build-backend: + name: Build backend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: docker/setup-qemu-action@v1 + - uses: docker/setup-buildx-action@v1 + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: buildx-build-backend + - name: Build/Publish + uses: docker/build-push-action@v2 + with: + context: backend + file: backend/Dockerfile + push: false + cache-from: type=local,src=/tmp/.buildx-cache,type=registry,ref=pennlabs/penn-clubs-backend:latest + cache-to: type=local,dest=/tmp/.buildx-cache + tags: pennlabs/penn-clubs-backend:latest,pennlabs/penn-clubs-backend:${{ github.sha }} + outputs: type=docker,dest=/tmp/image.tar + - uses: actions/upload-artifact@v2 + with: + name: build-backend + path: /tmp/image.tar + needs: backend-check + + build-frontend: + name: Build frontend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: docker/setup-qemu-action@v1 + - uses: docker/setup-buildx-action@v1 + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: buildx-build-frontend + - name: Build/Publish + uses: docker/build-push-action@v2 + with: + context: frontend + file: frontend/Dockerfile + push: false + cache-from: type=local,src=/tmp/.buildx-cache,type=registry,ref=pennlabs/penn-clubs-frontend:latest + cache-to: type=local,dest=/tmp/.buildx-cache + tags: pennlabs/penn-clubs-frontend:latest,pennlabs/penn-clubs-frontend:${{ github.sha }} + outputs: type=docker,dest=/tmp/image.tar + - uses: actions/upload-artifact@v2 + with: + name: build-frontend + path: /tmp/image.tar + needs: frontend-check + + publish: + name: Publish Images + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + - uses: geekyeggo/delete-artifact@v1 + with: + name: |- + build-backend + build-frontend + - name: Load docker images + run: |- + docker load --input build-backend/image.tar + docker load --input build-frontend/image.tar + - uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Push docker images + run: |- + docker push -a pennlabs/penn-clubs-backend + docker push -a pennlabs/penn-clubs-frontend + needs: + - build-backend + - build-frontend + + deploy: + name: "Deploy" + uses: pennlabs/shared-actions/.github/workflows/deployment.yaml@v0.1 + + with: + githubRef: ${{ github.ref }} + gitSha: ${{ github.sha }} + + secrets: + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + GH_AWS_ACCESS_KEY_ID: ${{ secrets.GH_AWS_ACCESS_KEY_ID }} + GH_AWS_SECRET_ACCESS_KEY: ${{ secrets.GH_AWS_SECRET_ACCESS_KEY }} + + needs: + - publish diff --git a/.github/workflows/cdkactions_build-and-deploy.yaml b/.github/workflows/cdkactions_build-and-deploy.yaml deleted file mode 100644 index 46ae7ee89..000000000 --- a/.github/workflows/cdkactions_build-and-deploy.yaml +++ /dev/null @@ -1,241 +0,0 @@ -# Generated by cdkactions. Do not modify -# Generated as part of the 'application' stack. -name: Build and Deploy -on: push -jobs: - django-check: - name: Django Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.local/share/virtualenvs - key: v0-${{ hashFiles('backend/Pipfile.lock') }} - - name: Install Dependencies - run: |- - cd backend - pip install pipenv - pipenv install --deploy --dev - - name: Lint (flake8) - run: |- - cd backend - pipenv run flake8 . - - name: Lint (black) - run: |- - cd backend - pipenv run black --check . - - name: Test (run in parallel) - run: |- - cd backend - pipenv run coverage run --concurrency=multiprocessing manage.py test --settings=pennclubs.settings.ci --parallel - pipenv run coverage combine - - name: Upload Code Coverage - run: |- - ROOT=$(pwd) - cd backend - pipenv run codecov --root $ROOT --flags backend - container: - image: python:3.8-buster - env: - DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres - services: - postgres: - image: postgres:12 - env: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - options: "--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5" - build-backend: - name: Build backend - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: docker/setup-qemu-action@v1 - - uses: docker/setup-buildx-action@v1 - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: /tmp/.buildx-cache - key: buildx-build-backend - - name: Build/Publish - uses: docker/build-push-action@v2 - with: - context: backend - file: backend/Dockerfile - push: false - cache-from: type=local,src=/tmp/.buildx-cache,type=registry,ref=pennlabs/penn-clubs-backend:latest - cache-to: type=local,dest=/tmp/.buildx-cache - tags: pennlabs/penn-clubs-backend:latest,pennlabs/penn-clubs-backend:${{ github.sha }} - outputs: type=docker,dest=/tmp/image.tar - - uses: actions/upload-artifact@v2 - with: - name: build-backend - path: /tmp/image.tar - needs: django-check - react-check: - name: React Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Cache - uses: actions/cache@v2 - with: - path: "**/node_modules" - key: v0-${{ hashFiles('frontend/yarn.lock') }} - - name: Install Dependencies - run: |- - cd frontend - yarn install --frozen-lockfile - - name: Lint - run: |- - cd frontend - yarn lint - - name: Test - run: |- - cd frontend - yarn test - - name: Upload Code Coverage - run: |- - ROOT=$(pwd) - cd frontend - yarn run codecov -p $ROOT -F frontend - container: - image: node:14 - build-frontend: - name: Build frontend - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: docker/setup-qemu-action@v1 - - uses: docker/setup-buildx-action@v1 - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: /tmp/.buildx-cache - key: buildx-build-frontend - - name: Build/Publish - uses: docker/build-push-action@v2 - with: - context: frontend - file: frontend/Dockerfile - push: false - cache-from: type=local,src=/tmp/.buildx-cache,type=registry,ref=pennlabs/penn-clubs-frontend:latest - cache-to: type=local,dest=/tmp/.buildx-cache - tags: pennlabs/penn-clubs-frontend:latest,pennlabs/penn-clubs-frontend:${{ github.sha }} - outputs: type=docker,dest=/tmp/image.tar - - uses: actions/upload-artifact@v2 - with: - name: build-frontend - path: /tmp/image.tar - needs: react-check - integration-tests: - name: Integration Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/download-artifact@v2 - - name: Load docker images - run: |- - docker load --input build-backend/image.tar - docker load --input build-frontend/image.tar - - name: Run docker compose - run: |- - mkdir -p /tmp/test-results - docker-compose -f docker-compose.test.yaml up -d - - name: Wait for backend - run: |- - for try in {1..20}; do - docker-compose -f docker-compose.test.yaml exec -T backend python manage.py migrate --check && break - sleep 5 - done - - name: Populate backend - run: docker-compose -f docker-compose.test.yaml exec -T backend python manage.py populate - - name: Run integration tests - run: docker-compose -f docker-compose.test.yaml exec -T frontend yarn integration - - name: Delete artifacts when no longer needed - if: failure() || github.ref != 'refs/heads/master' - uses: geekyeggo/delete-artifact@v1 - with: - name: |- - build-backend - build-frontend - - name: Print logs on failure - if: failure() - run: docker-compose -f docker-compose.test.yaml logs - - name: Upload artifacts on failure - if: failure() - uses: actions/upload-artifact@v2 - with: - name: cypress-output - path: /tmp/test-results - env: - GIT_SHA: ${{ github.sha }} - needs: - - build-backend - - build-frontend - post-integration-publish: - name: Publish Images - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' - steps: - - uses: actions/checkout@v2 - - uses: actions/download-artifact@v2 - - uses: geekyeggo/delete-artifact@v1 - with: - name: |- - build-backend - build-frontend - - name: Load docker images - run: |- - docker load --input build-backend/image.tar - docker load --input build-frontend/image.tar - - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Push docker images - run: |- - docker push -a pennlabs/penn-clubs-backend - docker push -a pennlabs/penn-clubs-frontend - needs: integration-tests - deploy: - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' - steps: - - uses: actions/checkout@v2 - - id: synth - name: Synth cdk8s manifests - run: |- - cd k8s - yarn install --frozen-lockfile - - # get repo name (by removing owner/organization) - export RELEASE_NAME=${REPOSITORY#*/} - - # Export RELEASE_NAME as an output - echo "::set-output name=RELEASE_NAME::$RELEASE_NAME" - - yarn build - env: - GIT_SHA: ${{ github.sha }} - REPOSITORY: ${{ github.repository }} - AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} - - name: Deploy - run: |- - aws eks --region us-east-1 update-kubeconfig --name production --role-arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/kubectl - - # get repo name from synth step - RELEASE_NAME=${{ steps.synth.outputs.RELEASE_NAME }} - - # Deploy - kubectl apply -f k8s/dist/ -l app.kubernetes.io/component=certificate - kubectl apply -f k8s/dist/ --prune -l app.kubernetes.io/part-of=$RELEASE_NAME - env: - AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} - AWS_ACCESS_KEY_ID: ${{ secrets.GH_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.GH_AWS_SECRET_ACCESS_KEY }} - needs: - - post-integration-publish diff --git a/.github/workflows/cdkactions_validate.yaml b/.github/workflows/cdkactions_validate.yaml deleted file mode 100644 index 73834d779..000000000 --- a/.github/workflows/cdkactions_validate.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by cdkactions. Do not modify -# Generated as part of the 'validate' stack. -name: Validate cdkactions manifests -on: push -jobs: - validate: - name: Validate cdkactions manifests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - token: ${{ github.token }} - - name: Validate manifests - run: |- - cd .github/cdk - yarn install - yarn build - git --no-pager diff ../workflows - git diff-index --quiet HEAD -- ../workflows - - name: Push updated manifests - if: "false" - run: |- - cd .github/workflows - git config user.name github-actions - git config user.email github-actions[bot]@users.noreply.github.com - git add . - git commit -m "Update cdkactions manifests" || exit 0 - git push diff --git a/.gitignore b/.gitignore index 3cd5296e7..35a1877f2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Python files __pycache__/ *.pyc +.python-version # Distribution /frontend/public/storybook/ @@ -27,6 +28,8 @@ db.sqlite3 # React node_modules/ +.yarn +.yarnrc.yml .next/ # Development Enviroment diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f5abe424..64a465e47 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,11 +4,12 @@ repos: hooks: - id: black name: black - entry: cd backend && pipenv run black + entry: bash -c "export PIPENV_IGNORE_VIRTUALENVS=1 && cd backend && pipenv run black ." language: python types: [python] require_serial: true files: ^backend/ + pass_filenames: false - id: isort name: isort entry: isort @@ -26,7 +27,7 @@ repos: args: [--config, backend/setup.cfg] - id: frontend name: Yarn Linter - entry: yarn --cwd frontend lint + entry: bash -c "cd frontend && yarn lint" language: system files: ^frontend/ require_serial: false diff --git a/README.md b/README.md index e9552e195..ff2f12a9b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Official React-based website for Penn Labs' club directory and events listings. The REST API written in Django for Penn Clubs infrastructure. ## Installation + You will need to start both the backend and the frontend to do Penn Clubs development. Questions? Check out our [extended guide](https://github.com/pennlabs/penn-clubs/wiki/Development-Guide#windows-development) for FAQs for both Mac and Windows. @@ -16,6 +17,7 @@ Questions? Check out our [extended guide](https://github.com/pennlabs/penn-clubs Running the backend requires [Python 3](https://www.python.org/downloads/). In production, you will need to set the following environment variables: + - `NEXT_PUBLIC_SITE_NAME` (optional, defaults to `clubs`) - `SECRET_KEY` - `SENTRY_URL` @@ -27,6 +29,7 @@ In production, you will need to set the following environment variables: - `LABS_CLIENT_SECRET` (from Platform) To run the server, `cd` to the folder where you cloned `penn-clubs`. Then run: + - `cd backend` Setting up `psycopg2` (this is necessary if you want to be able to modify @@ -42,14 +45,14 @@ dependencies, you can revisit later if not) - Windows - `$ apt-get install gcc python3-dev libpq-dev` -Now, you can run +Now, you can run - `$ pipenv install` to install Python dependencies. This may take a few minutes. Optionally include the `--dev` argument if you are installing locally for development. If you skipped installing `psycopg2` earlier, you might see an error with locking -- this is expected! - `$ pipenv shell` -- `$ pre-commit install` +- `$ pre-commit install` - `$ ./manage.py migrate` OR `$ python3 manage.py migrate` - `$ ./manage.py populate` OR `$ python3 manage.py populate` (in development, to populate the database with dummy data) @@ -57,9 +60,14 @@ Now, you can run ### Frontend -Running the frontend requires [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/getting-started/install). +Running the frontend requires [Node.js](https://nodejs.org/en/) and +[Yarn](https://yarnpkg.com/getting-started/install). + +**Please ensure you are using Node 14**. Our codebase does not support other +versions of Node (v14.21.3 is stable). You will need to set the following environment variables on the frontend: + - `NEXT_PUBLIC_GOOGLE_API_KEY` - `NEXT_PUBLIC_SITE_NAME` (Optional) - Specify `clubs` to show Penn Clubs and `fyh` to show Hub@Penn. diff --git a/backend/Dockerfile b/backend/Dockerfile index 9c9f86b52..b58dc4d80 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM pennlabs/django-base:a142aa6975ee293bbc8a09ef0b81998ce7063dd3 +FROM pennlabs/django-base:9c4f31bf1af44219d0f9019271a0033a222291c2-3.8.5 LABEL maintainer="Penn Labs" diff --git a/backend/Pipfile b/backend/Pipfile index 01c5b61a8..15e55c7a9 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -4,10 +4,9 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -codecov = "*" black = "==19.10b0" unittest-xml-reporting = ">=3.0.2" -flake8 = "*" +flake8 = "==5.0.3" flake8-isort = "*" isort = "*" flake8-quotes = "*" @@ -32,7 +31,6 @@ pillow = "*" django-phonenumber-field = "*" phonenumbers = "*" qrcode = "*" -drf-renderer-xlsx = "==0.3.9" python-dateutil = "*" psycopg2 = "*" django-simple-history = "*" @@ -54,6 +52,11 @@ tblib = "*" pre-commit = "*" django-clone = "*" click = "==8.0.4" +jinja2 = "*" +pandas = "*" +drf-excel = "*" +numpy = "*" +coverage = "*" [requires] python_version = "3" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 6c195adb4..3831da635 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1e5ed3497a97d54da1163f9def2f5aee2feaf2aa537a361d42376f30cb8cac4f" + "sha256": "d2ae42ea3dc4d5de0bc9f2be6e0603264bfe0f248a0209f224b40ef941770a35" }, "pipfile-spec": 6, "requires": { @@ -16,20 +16,13 @@ ] }, "default": { - "aioredis": { - "hashes": [ - "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", - "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" - ], - "version": "==1.3.1" - }, "anyio": { "hashes": [ - "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", - "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" + "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421", + "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.6.1" + "version": "==3.6.2" }, "arrow": { "hashes": [ @@ -41,49 +34,49 @@ }, "asgiref": { "hashes": [ - "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", - "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" + "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac", + "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506" ], "markers": "python_version >= '3.7'", - "version": "==3.5.2" + "version": "==3.6.0" }, "async-timeout": { "hashes": [ "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version <= '3.11.2'", "version": "==4.0.2" }, "attrs": { "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" + "markers": "python_version >= '3.7'", + "version": "==23.1.0" }, "autobahn": { "hashes": [ - "sha256:8b462ea2e6aad6b4dc0ed45fb800b6cbfeb0325e7fe6983907f122f2be4a1fe9" + "sha256:c5ef8ca7422015a1af774a883b8aef73d4954c9fcd182c9b5244e08e973f7c3a" ], "markers": "python_version >= '3.7'", - "version": "==22.7.1" + "version": "==23.1.2" }, "automat": { "hashes": [ - "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33", - "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111" + "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180", + "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e" ], - "version": "==20.2.0" + "version": "==22.10.0" }, "beautifulsoup4": { "hashes": [ - "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", - "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" + "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", + "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a" ], - "markers": "python_full_version >= '3.6.0'", - "version": "==4.11.1" + "markers": "python_version >= '3.6'", + "version": "==4.12.2" }, "bleach": { "hashes": [ @@ -95,19 +88,19 @@ }, "boto3": { "hashes": [ - "sha256:6194763348545bb1669ce8d03ba104be1ba822daa184613aa10b9303a6a79017", - "sha256:be151711bbb4db53e85dd5bbe506002ce6f2f21fc4e45fcf6d2cf356d32cc4c6" + "sha256:1ff703152553f4d5fc9774071d114dbf06ec661eb1b29b6051f6b1f9d0c24873", + "sha256:d0ed43228952b55c9f44d1c733f74656418c39c55dbe36bc37feeef6aa583ded" ], "index": "pypi", - "version": "==1.24.84" + "version": "==1.26.118" }, "botocore": { "hashes": [ - "sha256:11f05d2acdf9a5f722856704b7b951b180647fb4340e1b5048b27273dc323909", - "sha256:da15026329706caf83323d84996f5ff5c527837347633fca9b3b1be0efa60841" + "sha256:44cb088a73b02dd716c5c5715143a64d5f10388957285246e11f3cc893eebf9d", + "sha256:b51fc5d50cbc43edaf58b3ec4fa933b82755801c453bf8908c8d3e70ae1142c1" ], "markers": "python_version >= '3.7'", - "version": "==1.27.84" + "version": "==1.29.118" }, "bs4": { "hashes": [ @@ -118,11 +111,11 @@ }, "certifi": { "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" ], "markers": "python_version >= '3.6'", - "version": "==2022.9.24" + "version": "==2022.12.7" }, "cffi": { "hashes": [ @@ -211,19 +204,92 @@ }, "channels-redis": { "hashes": [ - "sha256:78e4a2f2b2a744fe5a87848ec36b5ee49f522c6808cefe6c583663d0d531faa8", - "sha256:ba7e2ad170f273c372812dd32aaac102d68d4e508172abb1cfda3160b7333890" + "sha256:3696f5b9fe367ea495d402ba83d7c3c99e8ca0e1354ff8d913535976ed0abf73", + "sha256:6bd4f75f4ab4a7db17cee495593ace886d7e914c66f8214a1f247ff6659c073a" ], "index": "pypi", - "version": "==3.4.1" + "version": "==4.1.0" }, "charset-normalizer": { "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2.1.1" + "markers": "python_version >= '3.7'", + "version": "==3.1.0" }, "click": { "hashes": [ @@ -248,37 +314,88 @@ ], "version": "==15.1.0" }, + "coverage": { + "hashes": [ + "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34", + "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e", + "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7", + "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b", + "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3", + "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985", + "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95", + "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2", + "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a", + "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74", + "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd", + "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af", + "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54", + "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865", + "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214", + "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54", + "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe", + "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0", + "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321", + "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446", + "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e", + "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527", + "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12", + "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f", + "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f", + "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84", + "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479", + "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e", + "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873", + "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70", + "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0", + "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977", + "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51", + "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28", + "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1", + "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254", + "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1", + "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd", + "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689", + "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d", + "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543", + "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9", + "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637", + "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071", + "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482", + "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1", + "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b", + "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5", + "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a", + "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393", + "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a", + "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba" + ], + "index": "pypi", + "version": "==7.3.0" + }, "cryptography": { "hashes": [ - "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", - "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", - "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", - "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", - "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", - "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", - "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", - "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", - "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", - "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", - "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", - "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", - "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", - "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", - "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", - "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", - "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", - "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", - "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", - "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", - "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", - "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", - "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", - "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", - "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", - "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" + "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440", + "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288", + "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b", + "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958", + "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b", + "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d", + "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a", + "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404", + "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b", + "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e", + "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2", + "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c", + "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b", + "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9", + "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b", + "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636", + "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99", + "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e", + "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9" ], "markers": "python_version >= '3.6'", - "version": "==38.0.1" + "version": "==40.0.2" }, "daphne": { "hashes": [ @@ -312,51 +429,51 @@ }, "dj-database-url": { "hashes": [ - "sha256:ccf3e8718f75ddd147a1e212fca88eecdaa721759ee48e38b485481c77bca3dc", - "sha256:cd354a3b7a9136d78d64c17b2aec369e2ae5616fbca6bfbe435ef15bb372ce39" + "sha256:80a115bd7675c9fe14a900b2f8b5c8b1822b5a279b333bf9b2804de681656c7c", + "sha256:87be5f7c4c83d9b3d8ce94b834f96cea14b3986f3629aac097afdd9318d7b098" ], "index": "pypi", - "version": "==1.0.0" + "version": "==1.3.0" }, "django": { "hashes": [ - "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713", - "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b" + "sha256:08208dfe892eb64fff073ca743b3b952311104f939e7f6dae954fe72dcc533ba", + "sha256:4d492d9024c7b3dfababf49f94511ab6a58e2c9c3c7207786f1ba4eb77750706" ], "index": "pypi", - "version": "==3.2.15" + "version": "==3.2.18" }, "django-clone": { "hashes": [ - "sha256:0d12b4d976ed9be5ac19108f8551f017f79042b6676307c92fb8cfc530df861a", - "sha256:f3bc1e06799a82770da6c8fd6abcdbf1fd41390e7ea14a0cc5a8f12c0765d36e" + "sha256:4be26e42bccd14e0ce24e3fa8d06744f904194e6ed7669d01034f7e81865e2e4", + "sha256:d44521a1680a0adbfff77429d1a4f9906ead3b31e5ee206d849883e32a6fd61f" ], "index": "pypi", - "version": "==3.0.6" + "version": "==5.3.1" }, "django-cors-headers": { "hashes": [ - "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4", - "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf" + "sha256:5fbd58a6fb4119d975754b2bc090f35ec160a8373f276612c675b00e8a138739", + "sha256:684180013cc7277bdd8702b80a3c5a4b3fcae4abb2bf134dceb9f5dfe300228e" ], "index": "pypi", - "version": "==3.13.0" + "version": "==3.14.0" }, "django-labs-accounts": { "hashes": [ - "sha256:32cf0f705c53fb4624eea7326c77e37706f496462bb4125fb488547a3af187b2", - "sha256:fa537531f019c7668251455b1e586bcd6aa1dfb748198a6c1a5ef96972b6eae4" + "sha256:59e90ad8cb5201da5bbdbf68fab99eb25ca9a5d1ab577e0772ecbffcb470e15f", + "sha256:d360e13776a56998871289a66e855637ca740873f540da6923195e79d098491d" ], "index": "pypi", - "version": "==0.8.0" + "version": "==0.9.2" }, "django-phonenumber-field": { "hashes": [ - "sha256:dab78094e83f4b1276effca9903e6728e940d055b00cc8589ad5b8a22cb6a03b", - "sha256:f1aaee276b18a8f0bf503d52eda183965ca164a6379c1e70f73718bcc8a91345" + "sha256:9edad2b2602af25f2aefc73c4cf53eaf7abf9e17d73c1c4372bd3052bebb26f9", + "sha256:de3e47b986b4959949762c16fd8fe26b3e462ef3e5531ed00950bd20c698576a" ], "index": "pypi", - "version": "==7.0.0" + "version": "==7.0.2" }, "django-redis": { "hashes": [ @@ -376,18 +493,19 @@ }, "django-simple-history": { "hashes": [ - "sha256:eab6dbddb7da756cc5579f7d6f28a32b2e8bb1697a5c37794d96309793ffec38" + "sha256:2313d2d346f15a1e7a92adb3b6696b226f1cd0c1d920869ec40c4c4076614c41", + "sha256:dc1f98e558a0a1e0b6371c3b8efb85f86e02a6db56e83d0ec198343b7408d00a" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.3.0" }, "django-storages": { "hashes": [ - "sha256:3540b45618b04be2c867c0982e8d2bd8e34f84dae922267fcebe4691fb93daf0", - "sha256:b3d98ecc09f1b1627c2b2cf430964322ce4e08617dbf9b4236c16a32875a1e0b" + "sha256:31dc5a992520be571908c4c40d55d292660ece3a55b8141462b4e719aa38eab3", + "sha256:cbadd15c909ceb7247d4ffc503f12a9bec36999df8d0bef7c31e57177d512688" ], "index": "pypi", - "version": "==1.13.1" + "version": "==1.13.2" }, "djangorestframework": { "hashes": [ @@ -397,21 +515,21 @@ "index": "pypi", "version": "==3.14.0" }, - "drf-nested-routers": { + "drf-excel": { "hashes": [ - "sha256:01aa556b8c08608bb74fb34f6ca065a5183f2cda4dc0478192cc17a2581d71b0", - "sha256:996b77f3f4dfaf64569e7b8f04e3919945f90f95366838ca5b8bed9dd709d6c5" + "sha256:01905346446f699a03ffefe97edeff7e0d83e707028df2d593bf18381a148872", + "sha256:3ef1ce054c52f9850c46f4d4d3344e7fc8e6a95d9d0e0fb666d0bb6c37c38666" ], "index": "pypi", - "version": "==0.93.4" + "version": "==2.3.0" }, - "drf-renderer-xlsx": { + "drf-nested-routers": { "hashes": [ - "sha256:2aca180c71b088b1f8fa9adecb675f369f88cb89d46fd36f267ae166dc58332f", - "sha256:da19cbd973c59f8b5a0aa3d46f5888d3eddccf1c7a424a42d6748c81d4e1148f" + "sha256:01aa556b8c08608bb74fb34f6ca065a5183f2cda4dc0478192cc17a2581d71b0", + "sha256:996b77f3f4dfaf64569e7b8f04e3919945f90f95366838ca5b8bed9dd709d6c5" ], "index": "pypi", - "version": "==0.3.9" + "version": "==0.93.4" }, "et-xmlfile": { "hashes": [ @@ -421,20 +539,21 @@ "markers": "python_version >= '3.6'", "version": "==1.1.0" }, - "filelock": { + "exceptiongroup": { "hashes": [ - "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc", - "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4" + "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", + "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" ], - "markers": "python_version >= '3.7'", - "version": "==3.8.0" + "markers": "python_version < '3.11'", + "version": "==1.1.3" }, - "future": { + "filelock": { "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9", + "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.2" + "markers": "python_version >= '3.7'", + "version": "==3.12.0" }, "gunicorn": { "hashes": [ @@ -452,53 +571,6 @@ "markers": "python_version >= '3.7'", "version": "==0.14.0" }, - "hiredis": { - "hashes": [ - "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e", - "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27", - "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163", - "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc", - "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26", - "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e", - "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579", - "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a", - "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048", - "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87", - "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63", - "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54", - "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05", - "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb", - "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea", - "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5", - "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e", - "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc", - "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99", - "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a", - "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581", - "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426", - "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db", - "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a", - "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a", - "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d", - "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443", - "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79", - "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d", - "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9", - "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d", - "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485", - "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5", - "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048", - "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0", - "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6", - "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41", - "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298", - "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce", - "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", - "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.0" - }, "httptools": { "hashes": [ "sha256:0297822cea9f90a38df29f48e40b42ac3d48a28637368f3ec6d15eebefd182f9", @@ -564,26 +636,34 @@ }, "identify": { "hashes": [ - "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6", - "sha256:ef78c0d96098a3b5fe7720be4a97e73f439af7cf088ebf47b620aeaa10fadf97" + "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f", + "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e" ], "markers": "python_version >= '3.7'", - "version": "==2.5.5" + "version": "==2.5.22" }, "idna": { "hashes": [ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==3.4" }, "incremental": { "hashes": [ - "sha256:02f5de5aff48f6b9f665d99d48bfc7ec03b6e3943210de7cfc88856d755d6f57", - "sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321" + "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0", + "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51" ], - "version": "==21.3.0" + "version": "==22.10.0" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "index": "pypi", + "version": "==3.1.2" }, "jmespath": { "hashes": [ @@ -595,144 +675,225 @@ }, "jsonref": { "hashes": [ - "sha256:b1e82fa0b62e2c2796a13e5401fe51790b248f6d9bf9d7212a3e31a3501b291f", - "sha256:f3c45b121cf6257eafabdc3a8008763aed1cd7da06dbabc59a9e4d2a5e4e6697" + "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", + "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9" ], "index": "pypi", - "version": "==0.2" + "version": "==1.1.0" + }, + "jwcrypto": { + "hashes": [ + "sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b" + ], + "markers": "python_version >= '3.6'", + "version": "==1.4.2" }, "lxml": { "hashes": [ - "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318", - "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c", - "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b", - "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000", - "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73", - "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d", - "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb", - "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8", - "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2", - "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345", - "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94", - "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e", - "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b", - "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc", - "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a", - "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9", - "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc", - "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387", - "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb", - "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7", - "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4", - "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97", - "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67", - "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627", - "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7", - "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd", - "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3", - "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7", - "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130", - "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b", - "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036", - "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785", - "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca", - "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91", - "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc", - "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536", - "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391", - "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3", - "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d", - "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21", - "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3", - "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d", - "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29", - "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715", - "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed", - "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25", - "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c", - "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785", - "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837", - "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4", - "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b", - "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2", - "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067", - "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448", - "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d", - "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2", - "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc", - "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c", - "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5", - "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84", - "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8", - "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf", - "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7", - "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e", - "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb", - "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b", - "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3", - "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad", - "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8", - "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f" + "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7", + "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726", + "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03", + "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140", + "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a", + "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05", + "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03", + "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419", + "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4", + "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e", + "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67", + "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50", + "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894", + "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf", + "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947", + "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1", + "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd", + "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3", + "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92", + "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3", + "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457", + "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74", + "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf", + "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1", + "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4", + "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975", + "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5", + "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe", + "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7", + "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1", + "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2", + "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409", + "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f", + "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f", + "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5", + "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24", + "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e", + "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4", + "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a", + "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c", + "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de", + "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f", + "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b", + "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5", + "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7", + "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a", + "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c", + "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9", + "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e", + "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab", + "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941", + "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5", + "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45", + "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7", + "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892", + "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746", + "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c", + "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53", + "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe", + "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184", + "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38", + "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df", + "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9", + "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b", + "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2", + "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0", + "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda", + "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b", + "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5", + "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380", + "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33", + "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8", + "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1", + "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889", + "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9", + "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f", + "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.9.1" + "version": "==4.9.2" + }, + "markupsafe": { + "hashes": [ + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" }, "msgpack": { "hashes": [ - "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467", - "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae", - "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92", - "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef", - "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624", - "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227", - "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88", - "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9", - "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8", - "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd", - "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6", - "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55", - "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e", - "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2", - "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44", - "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6", - "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9", - "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab", - "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae", - "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa", - "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9", - "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e", - "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250", - "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce", - "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075", - "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236", - "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae", - "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e", - "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f", - "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08", - "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6", - "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d", - "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43", - "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1", - "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6", - "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0", - "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c", - "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff", - "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db", - "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243", - "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661", - "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba", - "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e", - "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb", - "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52", - "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6", - "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1", - "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f", - "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da", - "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f", - "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c", - "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8" - ], - "version": "==1.0.4" + "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164", + "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b", + "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c", + "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf", + "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd", + "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d", + "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c", + "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a", + "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e", + "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd", + "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025", + "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5", + "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705", + "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a", + "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d", + "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb", + "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11", + "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f", + "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c", + "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d", + "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea", + "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba", + "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87", + "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a", + "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c", + "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080", + "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198", + "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9", + "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a", + "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b", + "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f", + "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437", + "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f", + "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7", + "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2", + "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0", + "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48", + "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898", + "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0", + "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57", + "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8", + "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282", + "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1", + "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82", + "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc", + "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb", + "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6", + "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7", + "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9", + "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c", + "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1", + "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed", + "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c", + "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c", + "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77", + "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81", + "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a", + "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3", + "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086", + "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9", + "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f", + "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b", + "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d" + ], + "version": "==1.0.5" }, "nodeenv": { "hashes": [ @@ -742,170 +903,225 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==1.7.0" }, + "numpy": { + "hashes": [ + "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187", + "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812", + "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7", + "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4", + "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6", + "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0", + "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4", + "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570", + "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4", + "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f", + "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80", + "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289", + "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385", + "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078", + "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c", + "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463", + "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3", + "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950", + "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155", + "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7", + "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c", + "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096", + "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17", + "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf", + "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4", + "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02", + "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c", + "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b" + ], + "index": "pypi", + "version": "==1.24.3" + }, "oauthlib": { "hashes": [ - "sha256:1565237372795bf6ee3e5aba5e2a85bd5a65d0e2aa5c628b9a97b7d7a0da3721", - "sha256:88e912ca1ad915e1dcc1c06fc9259d19de8deacd6fd17cc2df266decc2e49066" + "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", + "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" ], "markers": "python_version >= '3.6'", - "version": "==3.2.1" + "version": "==3.2.2" }, "openpyxl": { "hashes": [ - "sha256:0ab6d25d01799f97a9464630abacbb34aafecdcaa0ef3cba6d6b3499867d0355", - "sha256:e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449" + "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", + "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], "markers": "python_version >= '3.6'", - "version": "==3.0.10" + "version": "==3.1.2" }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" ], - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, + "pandas": { + "hashes": [ + "sha256:0778ab54c8f399d83d98ffb674d11ec716449956bc6f6821891ab835848687f2", + "sha256:24472cfc7ced511ac90608728b88312be56edc8f19b9ed885a7d2e47ffaf69c0", + "sha256:2d1d138848dd71b37e3cbe7cd952ff84e2ab04d8988972166e18567dcc811245", + "sha256:3bb9d840bf15656805f6a3d87eea9dcb7efdf1314a82adcf7f00b820427c5570", + "sha256:425705cee8be54db2504e8dd2a730684790b15e5904b750c367611ede49098ab", + "sha256:4f3320bb55f34af4193020158ef8118ee0fb9aec7cc47d2084dbfdd868a0a24f", + "sha256:4ffb14f50c74ee541610668137830bb93e9dfa319b1bef2cedf2814cd5ac9c70", + "sha256:52c858de9e9fc422d25e67e1592a6e6135d7bcf9a19fcaf4d0831a0be496bf21", + "sha256:57c34b79c13249505e850d0377b722961b99140f81dafbe6f19ef10239f6284a", + "sha256:6ded51f7e3dd9b4f8b87f2ceb7bd1a8df2491f7ee72f7074c6927a512607199e", + "sha256:70db5c278bbec0306d32bf78751ff56b9594c05a5098386f6c8a563659124f91", + "sha256:78425ca12314b23356c28b16765639db10ebb7d8983f705d6759ff7fe41357fa", + "sha256:8318de0f886e4dcb8f9f36e45a3d6a6c3d1cfdc508354da85e739090f0222991", + "sha256:8f987ec26e96a8490909bc5d98c514147236e49830cba7df8690f6087c12bbae", + "sha256:9253edfd015520ce77a9343eb7097429479c039cd3ebe81d7810ea11b4b24695", + "sha256:977326039bd1ded620001a1889e2ed4798460a6bc5a24fbaebb5f07a41c32a55", + "sha256:a4f789b7c012a608c08cda4ff0872fd979cb18907a37982abe884e6f529b8793", + "sha256:b3ba8f5dd470d8bfbc4259829589f4a32881151c49e36384d9eb982b35a12020", + "sha256:b5337c87c4e963f97becb1217965b6b75c6fe5f54c4cf09b9a5ac52fc0bd03d3", + "sha256:bbb2c5e94d6aa4e632646a3bacd05c2a871c3aa3e85c9bec9be99cb1267279f2", + "sha256:c24c7d12d033a372a9daf9ff2c80f8b0af6f98d14664dbb0a4f6a029094928a7", + "sha256:cda9789e61b44463c1c4fe17ef755de77bcd13b09ba31c940d20f193d63a5dc8", + "sha256:d08e41d96bc4de6f500afe80936c68fce6099d5a434e2af7c7fd8e7c72a3265d", + "sha256:d93b7fcfd9f3328072b250d6d001dcfeec5d3bb66c1b9c8941e109a46c0c01a8", + "sha256:fcd471c9d9f60926ab2f15c6c29164112f458acb42280365fbefa542d0c2fc74" + ], + "index": "pypi", + "version": "==2.0.0" }, "phonenumbers": { "hashes": [ - "sha256:80a7422cf0999a6f9b7a2e6cfbdbbfcc56ab5b75414dc3b805bbec91276b64a3", - "sha256:82a4f226c930d02dcdf6d4b29e4cfd8678991fe65c2efd5fdd143557186f0868" + "sha256:421b69fd6d6650372000a6c47ab5b5c5d7b438b33f7b317739e728eff1ec1886", + "sha256:fe071b8324473e72a54b52e602d059c15b999ec9900fff9e42c01b422aeca662" ], "index": "pypi", - "version": "==8.12.56" + "version": "==8.13.10" }, "pillow": { "hashes": [ - "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927", - "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14", - "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc", - "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58", - "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60", - "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76", - "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c", - "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac", - "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490", - "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1", - "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f", - "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d", - "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", - "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", - "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", - "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437", - "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", - "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", - "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", - "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff", - "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da", - "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004", - "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f", - "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20", - "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d", - "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c", - "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544", - "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3", - "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04", - "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c", - "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5", - "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4", - "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb", - "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4", - "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c", - "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467", - "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e", - "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421", - "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b", - "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", - "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", - "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", - "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc", - "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", - "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", - "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", - "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28", - "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0", - "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1", - "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8", - "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd", - "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4", - "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8", - "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f", - "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013", - "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59", - "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc", - "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4" + "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1", + "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba", + "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a", + "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799", + "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51", + "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb", + "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5", + "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270", + "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6", + "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47", + "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf", + "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e", + "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b", + "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66", + "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865", + "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec", + "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c", + "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1", + "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38", + "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906", + "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705", + "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef", + "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc", + "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f", + "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf", + "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392", + "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d", + "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe", + "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32", + "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5", + "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7", + "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44", + "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d", + "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3", + "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625", + "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e", + "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829", + "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089", + "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3", + "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78", + "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96", + "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964", + "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597", + "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99", + "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a", + "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140", + "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7", + "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16", + "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903", + "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1", + "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296", + "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572", + "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115", + "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a", + "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd", + "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4", + "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1", + "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb", + "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa", + "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a", + "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569", + "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c", + "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf", + "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082", + "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062", + "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579" ], "index": "pypi", - "version": "==9.2.0" + "version": "==9.5.0" }, "platformdirs": { "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" + "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08", + "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e" ], "markers": "python_version >= '3.7'", - "version": "==2.5.2" + "version": "==3.2.0" }, "pre-commit": { "hashes": [ - "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7", - "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959" + "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4", + "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d" ], "index": "pypi", - "version": "==2.20.0" + "version": "==3.2.2" }, "psycopg2": { "hashes": [ - "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c", - "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf", - "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362", - "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7", - "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461", - "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126", - "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981", - "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56", - "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305", - "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2", - "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca" + "sha256:11aca705ec888e4f4cea97289a0bf0f22a067a32614f6ef64fcf7b8bfbc53744", + "sha256:1861a53a6a0fd248e42ea37c957d36950da00266378746588eab4f4b5649e95f", + "sha256:2362ee4d07ac85ff0ad93e22c693d0f37ff63e28f0615a16b6635a645f4b9214", + "sha256:36c941a767341d11549c0fbdbb2bf5be2eda4caf87f65dfcd7d146828bd27f39", + "sha256:53f4ad0a3988f983e9b49a5d9765d663bbe84f508ed655affdb810af9d0972ad", + "sha256:869776630c04f335d4124f120b7fb377fe44b0a7645ab3c34b4ba42516951889", + "sha256:a8ad4a47f42aa6aec8d061fdae21eaed8d864d4bb0f0cade5ad32ca16fcd6258", + "sha256:b81fcb9ecfc584f661b71c889edeae70bae30d3ef74fa0ca388ecda50b1222b7", + "sha256:d24ead3716a7d093b90b27b3d73459fe8cd90fd7065cf43b3c40966221d8c394", + "sha256:ded2faa2e6dfb430af7713d87ab4abbfc764d8d7fb73eafe96a24155f906ebf5", + "sha256:f15158418fd826831b28585e2ab48ed8df2d0d98f502a2b4fe619e7d5ca29011", + "sha256:f75001a1cbbe523e00b0ef896a5a1ada2da93ccd752b7636db5a99bc57c44494", + "sha256:f7a7a5ee78ba7dc74265ba69e010ae89dae635eea0e97b055fb641a01a31d2b1" ], "index": "pypi", - "version": "==2.9.3" + "version": "==2.9.6" }, "pyasn1": { "hashes": [ - "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", - "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", - "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", - "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", - "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", - "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", - "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", - "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", - "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", - "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" - ], - "version": "==0.4.8" + "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57", + "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.5.0" }, "pyasn1-modules": { "hashes": [ - "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", - "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", - "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", - "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", - "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", - "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", - "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", - "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", - "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", - "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", - "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", - "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" - ], - "version": "==0.2.8" + "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c", + "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.3.0" }, "pycparser": { "hashes": [ @@ -917,27 +1133,26 @@ }, "pyjwt": { "hashes": [ - "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80", - "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b" + "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd", + "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14" ], "markers": "python_version >= '3.7'", - "version": "==2.5.0" + "version": "==2.6.0" }, "pyopenssl": { "hashes": [ - "sha256:7a83b7b272dd595222d672f5ce29aa030f1fb837630ef229f62e72e395ce8968", - "sha256:b28437c9773bb6c6958628cf9c3bebe585de661dba6f63df17111966363dd15e" + "sha256:841498b9bec61623b1b6c47ebbc02367c07d60e0e195f19790817f10cc8db0b7", + "sha256:9e0c526404a210df9d2b18cd33364beadb0dc858a739b885677bc65e105d4a4c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==22.1.0" + "version": "==23.1.1" }, - "pyparsing": { + "pypng": { "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c", + "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1" ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" + "version": "==0.20220715.0" }, "python-dateutil": { "hashes": [ @@ -949,10 +1164,10 @@ }, "python-dotenv": { "hashes": [ - "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5", - "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045" + "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", + "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" ], - "version": "==0.21.0" + "version": "==1.0.0" }, "python3-openid": { "hashes": [ @@ -964,10 +1179,10 @@ }, "pytz": { "hashes": [ - "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", - "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" + "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588", + "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb" ], - "version": "==2022.2.1" + "version": "==2023.3" }, "pyyaml": { "hashes": [ @@ -1017,26 +1232,27 @@ }, "qrcode": { "hashes": [ - "sha256:375a6ff240ca9bd41adc070428b5dfc1dcfbb0f2507f1ac848f6cded38956578" + "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a", + "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845" ], "index": "pypi", - "version": "==7.3.1" + "version": "==7.4.2" }, "redis": { "hashes": [ - "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54", - "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880" + "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2", + "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893" ], - "markers": "python_version >= '3.6'", - "version": "==4.3.4" + "markers": "python_version >= '3.7'", + "version": "==4.5.4" }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==2.28.2" }, "requests-oauthlib": { "hashes": [ @@ -1056,11 +1272,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:d6c71d2f85710b66822adaa954af7912bab135d6c85febd5b0f3dfd4ab37e181", - "sha256:ef925b5338625448645a778428d8f22a3d17de8b28cc8e6fba60b93393ad86fe" + "sha256:0ad6bbbe78057b8031a07de7aca6d2a83234e51adc4d436eaf8d8c697184db71", + "sha256:a3410381ae769a436c0852cce140a5e5e49f566a07fb7c2ab445af1302f6ad89" ], "index": "pypi", - "version": "==1.9.9" + "version": "==1.20.0" }, "service-identity": { "hashes": [ @@ -1071,11 +1287,11 @@ }, "setuptools": { "hashes": [ - "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012", - "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e" + "sha256:6f0839fbdb7e3cfef1fc38d7954f5c1c26bf4eebb155a55c9bf8faf997b9fb67", + "sha256:bb16732e8eb928922eabaa022f881ae2b7cdcfaf9993ef1f5e841a96d32b8e0c" ], "markers": "python_version >= '3.7'", - "version": "==65.4.1" + "version": "==67.7.1" }, "six": { "hashes": [ @@ -1095,35 +1311,35 @@ }, "social-auth-app-django": { "hashes": [ - "sha256:52241a25445a010ab1c108bafff21fc5522d5c8cd0d48a92c39c7371824b065d", - "sha256:b6e3132ce087cdd6e1707aeb1b588be41d318408fcf6395435da0bc6fe9a9795" + "sha256:0347ca4cd23ea9d15a665da9d22950552fb66b95600e6c2ebae38ca883b3a4ed", + "sha256:4a5dae406f3874b4003708ff120c02cb1a4c8eeead56cd163646347309fcd0f8" ], "index": "pypi", - "version": "==5.0.0" + "version": "==5.2.0" }, "social-auth-core": { "hashes": [ - "sha256:1e3440d104f743b02dfe258c9d4dba5b4065abf24b2f7eb362b47054d21797df", - "sha256:4686f0e43cf12954216875a32e944847bb1dc69e7cd9573d16a9003bb05ca477" + "sha256:9791d7c7aee2ac8517fe7a2ea2f942a8a5492b3a4ccb44a9b0dacc87d182f2aa", + "sha256:ea7a19c46b791b767e95f467881b53c5fd0d1efb40048d9ed3dbc46daa05c954" ], "markers": "python_version >= '3.6'", - "version": "==4.3.0" + "version": "==4.4.2" }, "soupsieve": { "hashes": [ - "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", - "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" + "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8", + "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea" ], - "markers": "python_version >= '3.6'", - "version": "==2.3.2.post1" + "markers": "python_version >= '3.7'", + "version": "==2.4.1" }, "sqlparse": { "hashes": [ - "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34", - "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268" + "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", + "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" ], - "markers": "python_version >= '3.5'", - "version": "==0.4.3" + "markers": "python_full_version >= '3.5.0'", + "version": "==0.4.4" }, "tatsu": { "hashes": [ @@ -1141,40 +1357,40 @@ "index": "pypi", "version": "==1.7.0" }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, "twisted": { "extras": [ "tls" ], "hashes": [ - "sha256:8d4718d1e48dcc28933f8beb48dc71cfe77a125e37ad1eb7a3d0acc49baf6c99", - "sha256:e5b60de39f2d1da153fbe1874d885fe3fcbdb21fcc446fa759a53e8fc3513bed" + "sha256:32acbd40a94f5f46e7b42c109bfae2b302250945561783a8b7a059048f2d4d31", + "sha256:86c55f712cc5ab6f6d64e02503352464f0400f66d4f079096d744080afcccbd0" ], "markers": "python_full_version >= '3.7.1'", - "version": "==22.8.0" + "version": "==22.10.0" }, "txaio": { "hashes": [ - "sha256:2e4582b70f04b2345908254684a984206c0d9b50e3074a24a4c55aba21d24d01", - "sha256:41223af4a9d5726e645a8ee82480f413e5e300dd257db94bc38ae12ea48fb2e5" + "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490", + "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704" ], - "markers": "python_version >= '3.6'", - "version": "==22.2.1" + "markers": "python_version >= '3.7'", + "version": "==23.1.1" }, "typing-extensions": { "hashes": [ - "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", - "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "markers": "python_version >= '3.7'", - "version": "==4.3.0" + "version": "==4.5.0" + }, + "tzdata": { + "hashes": [ + "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a", + "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda" + ], + "markers": "python_version >= '2'", + "version": "==2023.3" }, "unittest-xml-reporting": { "hashes": [ @@ -1194,38 +1410,38 @@ }, "uritools": { "hashes": [ - "sha256:420d94c1ff4bf90c678fca9c17b8314243bbcaa992c400a95e327f7f622e1edf", - "sha256:9a5a1495c55072093216f79931ca45fd81b59208aa64caae50ab68333514f97e" + "sha256:d122d394ed6e6e15ac0fddba6a5b19e9fa204e7797507815cbfb0e1455ac0475", + "sha256:efc5c3a6de05404850685a8d3f34da8476b56aa3516fbf8eff5c8704c7a2826f" ], "markers": "python_version ~= '3.7'", - "version": "==4.0.0" + "version": "==4.0.1" }, "urlextract": { "hashes": [ - "sha256:574f0d8c562d377336a5154840c7e4eecf54c102a538971c897f7313075a8887", - "sha256:c22a9645cae1e1390cc512fd4f4d1f37b72538eecfdc3365905a2e616e7b6b88" + "sha256:3573f6b812814efe06ca46e91e82d984edaa3cd07daaaaa296a467ad9881a037", + "sha256:98b38aca4a555116e8b46e5a134b9e4e54e351b8e37169d2857730d1d0ce42c7" ], "index": "pypi", - "version": "==1.6.0" + "version": "==1.8.0" }, "urllib3": { "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", + "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" ], "markers": "python_version >= '3.6'", - "version": "==1.26.12" + "version": "==1.26.15" }, "uvicorn": { "extras": [ "standard" ], "hashes": [ - "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af", - "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b" + "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032", + "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742" ], "index": "pypi", - "version": "==0.18.3" + "version": "==0.21.1" }, "uvloop": { "hashes": [ @@ -1265,41 +1481,45 @@ }, "uwsgi": { "hashes": [ - "sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9" + "sha256:35a30d83791329429bc04fe44183ce4ab512fcf6968070a7bfba42fc5a0552a9" ], "markers": "sys_platform == 'linux'", - "version": "==2.0.20" + "version": "==2.0.21" }, "virtualenv": { "hashes": [ - "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da", - "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27" + "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3", + "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a" ], - "markers": "python_version >= '3.6'", - "version": "==20.16.5" + "markers": "python_version >= '3.7'", + "version": "==20.22.0" }, "watchfiles": { "hashes": [ - "sha256:00e5f307a58752ec1478eeb738863544bde21cc7a2728bd1c216060406bde9c1", - "sha256:1dd1e3181ad5d83ca35e9147c72e24f39437fcdf570c9cdc532016399fb62957", - "sha256:204950f1d6083539af5c8b7d4f5f8039c3ce36fa692da12d9743448f3199cb15", - "sha256:4056398d8f6d4972fe0918707b59d4cb84470c91d3c37f0e11e5a66c2a598760", - "sha256:539bcdb55a487126776c9d8c011094214d1df3f9a2321a6c0b1583197309405a", - "sha256:53a2faeb121bc51bb6b960984f46901227e2e2475acc5a8d4c905a600436752d", - "sha256:58dc3140dcf02a8aa76464a77a093016f10e89306fec21a4814922a64f3e8b9f", - "sha256:6a3d6c699f3ce238dfa90bcef501f331a69b0d9b076f14459ed8eab26ba2f4cf", - "sha256:92675f379a9d5adbc6a52179f3e39aa56944c6eecb80384608fff2ed2619103a", - "sha256:a53cb6c06e5c1f216c792fbb432ce315239d432cb8b68d508547100939ec0399", - "sha256:a7f4271af86569bdbf131dd5c7c121c45d0ed194f3c88b88326e48a3b6a2db12", - "sha256:ad2bdcae4c0f07ca6c090f5a2c30188cc6edba011b45e7c96eb1896648092367", - "sha256:adcf15ecc2182ea9d2358c1a8c2b53203c3909484918776929b7bbe205522c0e", - "sha256:ae7c57ef920589a40270d5ef3216d693f4e6f8864d8fc8b6cb7885ca98ad2a61", - "sha256:afd35a1bd3b9e68efe384ae7538481ae725597feb66f56f4bd23ecdbda726da0", - "sha256:b5c334cd3bc88aa4a8a1e08ec9f702b63c947211275defdc2dd79dc037fcb500", - "sha256:c7e1ffbd03cbcb46d1b7833e10e7d6b678ab083b4e4b80db06cfff5baca3c93f", - "sha256:ffff3418dc753a2aed2d00200a4daeaac295c40458f8012836a65555f288be8b" - ], - "version": "==0.17.0" + "sha256:0089c6dc24d436b373c3c57657bf4f9a453b13767150d17284fc6162b2791911", + "sha256:09ea3397aecbc81c19ed7f025e051a7387feefdb789cf768ff994c1228182fda", + "sha256:176a9a7641ec2c97b24455135d58012a5be5c6217fc4d5fef0b2b9f75dbf5154", + "sha256:18b28f6ad871b82df9542ff958d0c86bb0d8310bb09eb8e87d97318a3b5273af", + "sha256:20b44221764955b1e703f012c74015306fb7e79a00c15370785f309b1ed9aa8d", + "sha256:3d7d267d27aceeeaa3de0dd161a0d64f0a282264d592e335fff7958cc0cbae7c", + "sha256:5471582658ea56fca122c0f0d0116a36807c63fefd6fdc92c71ca9a4491b6b48", + "sha256:5569fc7f967429d4bc87e355cdfdcee6aabe4b620801e2cf5805ea245c06097c", + "sha256:68dce92b29575dda0f8d30c11742a8e2b9b8ec768ae414b54f7453f27bdf9545", + "sha256:79c533ff593db861ae23436541f481ec896ee3da4e5db8962429b441bbaae16e", + "sha256:7f3920b1285a7d3ce898e303d84791b7bf40d57b7695ad549dc04e6a44c9f120", + "sha256:91633e64712df3051ca454ca7d1b976baf842d7a3640b87622b323c55f3345e7", + "sha256:945be0baa3e2440151eb3718fd8846751e8b51d8de7b884c90b17d271d34cae8", + "sha256:9afd0d69429172c796164fd7fe8e821ade9be983f51c659a38da3faaaaac44dc", + "sha256:9c75eff897786ee262c9f17a48886f4e98e6cfd335e011c591c305e5d083c056", + "sha256:b538014a87f94d92f98f34d3e6d2635478e6be6423a9ea53e4dd96210065e193", + "sha256:b6577b8c6c8701ba8642ea9335a129836347894b666dd1ec2226830e263909d3", + "sha256:c0376deac92377817e4fb8f347bf559b7d44ff556d9bc6f6208dd3f79f104aaf", + "sha256:cae3dde0b4b2078f31527acff6f486e23abed307ba4d3932466ba7cdd5ecec79", + "sha256:cb5d45c4143c1dd60f98a16187fd123eda7248f84ef22244818c18d531a249d1", + "sha256:d9b073073e048081e502b6c6b0b88714c026a1a4c890569238d04aca5f9ca74b", + "sha256:fac19dc9cbc34052394dbe81e149411a62e71999c0a19e1e09ce537867f95ae0" + ], + "version": "==0.19.0" }, "webencodings": { "hashes": [ @@ -1310,183 +1530,195 @@ }, "websockets": { "hashes": [ - "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af", - "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c", - "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76", - "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47", - "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69", - "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079", - "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c", - "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55", - "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02", - "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559", - "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3", - "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e", - "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978", - "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98", - "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae", - "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755", - "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d", - "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991", - "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1", - "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680", - "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247", - "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f", - "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2", - "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7", - "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4", - "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667", - "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb", - "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094", - "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36", - "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79", - "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500", - "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e", - "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582", - "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442", - "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd", - "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6", - "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731", - "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4", - "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d", - "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8", - "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f", - "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677", - "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8", - "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9", - "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e", - "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b", - "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916", - "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4" - ], - "version": "==10.3" + "sha256:0fb4480556825e4e6bf2eebdbeb130d9474c62705100c90e59f2f56459ddab42", + "sha256:13bd5bebcd16a4b5e403061b8b9dcc5c77e7a71e3c57e072d8dff23e33f70fba", + "sha256:143782041e95b63083b02107f31cda999f392903ae331de1307441f3a4557d51", + "sha256:1b52def56d2a26e0e9c464f90cadb7e628e04f67b0ff3a76a4d9a18dfc35e3dd", + "sha256:1df2413266bf48430ef2a752c49b93086c6bf192d708e4a9920544c74cd2baa6", + "sha256:2174a75d579d811279855df5824676d851a69f52852edb0e7551e0eeac6f59a4", + "sha256:220d5b93764dd70d7617f1663da64256df7e7ea31fc66bc52c0e3750ee134ae3", + "sha256:232b6ba974f5d09b1b747ac232f3a3d8f86de401d7b565e837cc86988edf37ac", + "sha256:25aae96c1060e85836552a113495db6d857400288161299d77b7b20f2ac569f2", + "sha256:25e265686ea385f22a00cc2b719b880797cd1bb53b46dbde969e554fb458bfde", + "sha256:2abeeae63154b7f63d9f764685b2d299e9141171b8b896688bd8baec6b3e2303", + "sha256:2acdc82099999e44fa7bd8c886f03c70a22b1d53ae74252f389be30d64fd6004", + "sha256:2eb042734e710d39e9bc58deab23a65bd2750e161436101488f8af92f183c239", + "sha256:3178d965ec204773ab67985a09f5696ca6c3869afeed0bb51703ea404a24e975", + "sha256:320ddceefd2364d4afe6576195201a3632a6f2e6d207b0c01333e965b22dbc84", + "sha256:34a6f8996964ccaa40da42ee36aa1572adcb1e213665e24aa2f1037da6080909", + "sha256:3565a8f8c7bdde7c29ebe46146bd191290413ee6f8e94cf350609720c075b0a1", + "sha256:392d409178db1e46d1055e51cc850136d302434e12d412a555e5291ab810f622", + "sha256:3a09cce3dacb6ad638fdfa3154d9e54a98efe7c8f68f000e55ca9c716496ca67", + "sha256:3a2100b02d1aaf66dc48ff1b2a72f34f6ebc575a02bc0350cc8e9fbb35940166", + "sha256:3b87cd302f08ea9e74fdc080470eddbed1e165113c1823fb3ee6328bc40ca1d3", + "sha256:3e79065ff6549dd3c765e7916067e12a9c91df2affea0ac51bcd302aaf7ad207", + "sha256:3ffe251a31f37e65b9b9aca5d2d67fd091c234e530f13d9dce4a67959d5a3fba", + "sha256:46388a050d9e40316e58a3f0838c63caacb72f94129eb621a659a6e49bad27ce", + "sha256:46dda4bc2030c335abe192b94e98686615f9274f6b56f32f2dd661fb303d9d12", + "sha256:4c54086b2d2aec3c3cb887ad97e9c02c6be9f1d48381c7419a4aa932d31661e4", + "sha256:5004c087d17251938a52cce21b3dbdabeecbbe432ce3f5bbbf15d8692c36eac9", + "sha256:502683c5dedfc94b9f0f6790efb26aa0591526e8403ad443dce922cd6c0ec83b", + "sha256:518ed6782d9916c5721ebd61bb7651d244178b74399028302c8617d0620af291", + "sha256:580cc95c58118f8c39106be71e24d0b7e1ad11a155f40a2ee687f99b3e5e432e", + "sha256:58477b041099bb504e1a5ddd8aa86302ed1d5c6995bdd3db2b3084ef0135d277", + "sha256:5875f623a10b9ba154cb61967f940ab469039f0b5e61c80dd153a65f024d9fb7", + "sha256:5c7de298371d913824f71b30f7685bb07ad13969c79679cca5b1f7f94fec012f", + "sha256:634239bc844131863762865b75211a913c536817c0da27f691400d49d256df1d", + "sha256:6d872c972c87c393e6a49c1afbdc596432df8c06d0ff7cd05aa18e885e7cfb7c", + "sha256:752fbf420c71416fb1472fec1b4cb8631c1aa2be7149e0a5ba7e5771d75d2bb9", + "sha256:7742cd4524622cc7aa71734b51294644492a961243c4fe67874971c4d3045982", + "sha256:808b8a33c961bbd6d33c55908f7c137569b09ea7dd024bce969969aa04ecf07c", + "sha256:87c69f50281126dcdaccd64d951fb57fbce272578d24efc59bce72cf264725d0", + "sha256:8df63dcd955eb6b2e371d95aacf8b7c535e482192cff1b6ce927d8f43fb4f552", + "sha256:8f24cd758cbe1607a91b720537685b64e4d39415649cac9177cd1257317cf30c", + "sha256:8f392587eb2767afa8a34e909f2fec779f90b630622adc95d8b5e26ea8823cb8", + "sha256:954eb789c960fa5daaed3cfe336abc066941a5d456ff6be8f0e03dd89886bb4c", + "sha256:955fcdb304833df2e172ce2492b7b47b4aab5dcc035a10e093d911a1916f2c87", + "sha256:95c09427c1c57206fe04277bf871b396476d5a8857fa1b99703283ee497c7a5d", + "sha256:a4fe2442091ff71dee0769a10449420fd5d3b606c590f78dd2b97d94b7455640", + "sha256:aa7b33c1fb2f7b7b9820f93a5d61ffd47f5a91711bc5fa4583bbe0c0601ec0b2", + "sha256:adf6385f677ed2e0b021845b36f55c43f171dab3a9ee0ace94da67302f1bc364", + "sha256:b1a69701eb98ed83dd099de4a686dc892c413d974fa31602bc00aca7cb988ac9", + "sha256:b2a573c8d71b7af937852b61e7ccb37151d719974146b5dc734aad350ef55a02", + "sha256:b444366b605d2885f0034dd889faf91b4b47668dd125591e2c64bfde611ac7e1", + "sha256:b985ba2b9e972cf99ddffc07df1a314b893095f62c75bc7c5354a9c4647c6503", + "sha256:c78ca3037a954a4209b9f900e0eabbc471fb4ebe96914016281df2c974a93e3e", + "sha256:ca9b2dced5cbbc5094678cc1ec62160f7b0fe4defd601cd28a36fde7ee71bbb5", + "sha256:cb46d2c7631b2e6f10f7c8bac7854f7c5e5288f024f1c137d4633c79ead1e3c0", + "sha256:ce69f5c742eefd039dce8622e99d811ef2135b69d10f9aa79fbf2fdcc1e56cd7", + "sha256:cf45d273202b0c1cec0f03a7972c655b93611f2e996669667414557230a87b88", + "sha256:d1881518b488a920434a271a6e8a5c9481a67c4f6352ebbdd249b789c0467ddc", + "sha256:d3cc3e48b6c9f7df8c3798004b9c4b92abca09eeea5e1b0a39698f05b7a33b9d", + "sha256:d6b2bfa1d884c254b841b0ff79373b6b80779088df6704f034858e4d705a4802", + "sha256:d70a438ef2a22a581d65ad7648e949d4ccd20e3c8ed7a90bbc46df4e60320891", + "sha256:daa1e8ea47507555ed7a34f8b49398d33dff5b8548eae3de1dc0ef0607273a33", + "sha256:dca9708eea9f9ed300394d4775beb2667288e998eb6f542cdb6c02027430c599", + "sha256:dd906b0cdc417ea7a5f13bb3c6ca3b5fd563338dc596996cb0fdd7872d691c0a", + "sha256:e0eeeea3b01c97fd3b5049a46c908823f68b59bf0e18d79b231d8d6764bc81ee", + "sha256:e37a76ccd483a6457580077d43bc3dfe1fd784ecb2151fcb9d1c73f424deaeba", + "sha256:e8b967a4849db6b567dec3f7dd5d97b15ce653e3497b8ce0814e470d5e074750", + "sha256:ec00401846569aaf018700249996143f567d50050c5b7b650148989f956547af", + "sha256:ede13a6998ba2568b21825809d96e69a38dc43184bdeebbde3699c8baa21d015", + "sha256:f97e03d4d5a4f0dca739ea274be9092822f7430b77d25aa02da6775e490f6846" + ], + "version": "==11.0.2" }, "wrapt": { "hashes": [ - "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", - "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", - "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", - "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", - "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", - "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", - "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", - "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", - "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", - "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", - "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", - "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", - "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", - "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", - "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", - "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", - "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", - "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", - "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", - "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", - "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", - "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", - "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", - "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", - "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", - "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", - "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", - "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", - "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", - "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", - "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", - "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", - "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", - "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", - "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", - "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", - "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", - "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", - "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", - "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", - "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", - "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", - "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", - "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", - "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", - "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", - "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", - "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", - "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", - "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", - "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", - "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", - "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", - "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", - "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", - "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", - "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", - "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", - "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", - "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", - "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", - "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", - "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", - "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" + "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", + "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", + "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", + "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", + "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", + "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", + "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", + "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", + "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", + "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", + "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", + "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", + "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", + "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", + "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", + "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", + "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", + "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", + "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", + "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", + "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", + "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", + "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", + "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", + "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", + "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", + "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", + "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", + "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", + "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", + "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", + "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", + "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", + "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", + "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", + "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", + "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", + "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", + "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", + "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", + "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", + "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", + "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", + "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", + "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", + "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", + "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", + "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", + "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", + "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", + "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", + "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", + "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", + "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", + "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", + "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", + "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", + "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", + "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", + "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", + "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", + "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", + "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", + "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", + "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", + "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", + "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", + "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", + "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", + "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", + "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", + "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", + "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", + "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", + "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.14.1" + "version": "==1.15.0" }, "zope.interface": { "hashes": [ - "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192", - "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702", - "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09", - "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4", - "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a", - "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3", - "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf", - "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c", - "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d", - "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78", - "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83", - "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531", - "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46", - "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021", - "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94", - "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc", - "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63", - "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54", - "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117", - "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25", - "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05", - "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e", - "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1", - "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004", - "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2", - "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e", - "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f", - "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f", - "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120", - "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f", - "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1", - "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9", - "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e", - "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7", - "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8", - "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b", - "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155", - "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7", - "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c", - "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325", - "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d", - "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb", - "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e", - "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959", - "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7", - "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920", - "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e", - "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48", - "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8", - "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", - "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" + "sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373", + "sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb", + "sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446", + "sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8", + "sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c", + "sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8", + "sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2", + "sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f", + "sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f", + "sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5", + "sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85", + "sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc", + "sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788", + "sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518", + "sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410", + "sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464", + "sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5", + "sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d", + "sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52", + "sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca", + "sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8", + "sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2", + "sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f", + "sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58", + "sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a", + "sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d", + "sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28", + "sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990", + "sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995", + "sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.4.0" + "markers": "python_version >= '3.7'", + "version": "==6.0" } }, "develop": { @@ -1499,19 +1731,19 @@ }, "asgiref": { "hashes": [ - "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", - "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" + "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac", + "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506" ], "markers": "python_version >= '3.7'", - "version": "==3.5.2" + "version": "==3.6.0" }, "attrs": { "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" + "markers": "python_version >= '3.7'", + "version": "==23.1.0" }, "black": { "hashes": [ @@ -1521,22 +1753,6 @@ "index": "pypi", "version": "==19.10b0" }, - "certifi": { - "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.9.24" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2.1.1" - }, "click": { "hashes": [ "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", @@ -1545,86 +1761,21 @@ "index": "pypi", "version": "==8.0.4" }, - "codecov": { - "hashes": [ - "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47", - "sha256:782a8e5352f22593cbc5427a35320b99490eb24d9dcfa2155fd99d2b75cfb635", - "sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1" - ], - "index": "pypi", - "version": "==2.1.12" - }, - "coverage": { - "hashes": [ - "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", - "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", - "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", - "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", - "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", - "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", - "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", - "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", - "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", - "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", - "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", - "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", - "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", - "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", - "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", - "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", - "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", - "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", - "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", - "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", - "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", - "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", - "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", - "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", - "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", - "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", - "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", - "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", - "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", - "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", - "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", - "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", - "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", - "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", - "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", - "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", - "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", - "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", - "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", - "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", - "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", - "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", - "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", - "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", - "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", - "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", - "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", - "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", - "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", - "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" - ], - "markers": "python_version >= '3.7'", - "version": "==6.5.0" - }, "django": { "hashes": [ - "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713", - "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b" + "sha256:08208dfe892eb64fff073ca743b3b952311104f939e7f6dae954fe72dcc533ba", + "sha256:4d492d9024c7b3dfababf49f94511ab6a58e2c9c3c7207786f1ba4eb77750706" ], "index": "pypi", - "version": "==3.2.15" + "version": "==3.2.18" }, "django-debug-toolbar": { "hashes": [ - "sha256:1e3acad24e3d351ba45c6fa2072e4164820307332a776b16c9f06d1f89503465", - "sha256:80de23066b624d3970fd296cf02d61988e5d56c31aa0dc4a428970b46e2883a8" + "sha256:89619f6e0ea1057dca47bfc429ed99b237ef70074dabc065a7faa5f00e1459cf", + "sha256:bad339d68520652ddc1580c76f136fcbc3e020fd5ed96510a89a02ec81bb3fb1" ], "index": "pypi", - "version": "==3.7.0" + "version": "==4.0.0" }, "django-extensions": { "hashes": [ @@ -1636,118 +1787,119 @@ }, "flake8": { "hashes": [ - "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", - "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248" + "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", + "sha256:93aa565ae2f0316b95bb57a354f2b2d55ee8508e1fe1cb13b77b9c195b4a2537", + "sha256:b27fd7faa8d90aaae763664a489012292990388e5d3604f383b290caefbbc922", + "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" ], "index": "pypi", - "version": "==5.0.4" + "version": "==5.0.3" }, "flake8-isort": { "hashes": [ - "sha256:26571500cd54976bbc0cf1006ffbcd1a68dd102f816b7a1051b219616ba9fee0", - "sha256:5b87630fb3719bf4c1833fd11e0d9534f43efdeba524863e15d8f14a7ef6adbf" + "sha256:537f453a660d7e903f602ecfa36136b140de279df58d02eb1b6a0c84e83c528c", + "sha256:aa0cac02a62c7739e370ce6b9c31743edac904bae4b157274511fc8a19c75bbc" ], "index": "pypi", - "version": "==4.2.0" + "version": "==6.0.0" }, "flake8-quotes": { "hashes": [ - "sha256:633adca6fb8a08131536af0d750b44d6985b9aba46f498871e21588c3e6f525a" + "sha256:6e26892b632dacba517bf27219c459a8396dcfac0f5e8204904c5a4ba9b480e1" ], "index": "pypi", - "version": "==3.3.1" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" + "version": "==3.3.2" }, "isort": { "hashes": [ - "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", - "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" + "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", + "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" ], "index": "pypi", - "version": "==5.10.1" + "version": "==5.12.0" }, "lxml": { "hashes": [ - "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318", - "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c", - "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b", - "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000", - "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73", - "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d", - "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb", - "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8", - "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2", - "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345", - "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94", - "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e", - "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b", - "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc", - "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a", - "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9", - "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc", - "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387", - "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb", - "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7", - "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4", - "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97", - "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67", - "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627", - "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7", - "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd", - "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3", - "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7", - "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130", - "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b", - "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036", - "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785", - "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca", - "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91", - "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc", - "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536", - "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391", - "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3", - "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d", - "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21", - "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3", - "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d", - "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29", - "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715", - "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed", - "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25", - "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c", - "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785", - "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837", - "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4", - "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b", - "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2", - "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067", - "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448", - "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d", - "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2", - "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc", - "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c", - "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5", - "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84", - "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8", - "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf", - "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7", - "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e", - "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb", - "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b", - "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3", - "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad", - "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8", - "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f" + "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7", + "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726", + "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03", + "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140", + "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a", + "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05", + "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03", + "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419", + "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4", + "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e", + "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67", + "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50", + "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894", + "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf", + "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947", + "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1", + "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd", + "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3", + "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92", + "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3", + "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457", + "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74", + "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf", + "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1", + "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4", + "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975", + "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5", + "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe", + "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7", + "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1", + "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2", + "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409", + "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f", + "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f", + "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5", + "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24", + "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e", + "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4", + "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a", + "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c", + "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de", + "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f", + "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b", + "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5", + "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7", + "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a", + "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c", + "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9", + "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e", + "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab", + "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941", + "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5", + "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45", + "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7", + "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892", + "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746", + "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c", + "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53", + "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe", + "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184", + "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38", + "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df", + "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9", + "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b", + "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2", + "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0", + "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda", + "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b", + "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5", + "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380", + "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33", + "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8", + "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1", + "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889", + "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9", + "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f", + "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.9.1" + "version": "==4.9.2" }, "mccabe": { "hashes": [ @@ -1759,11 +1911,11 @@ }, "pathspec": { "hashes": [ - "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93", - "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d" + "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", + "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" ], "markers": "python_version >= '3.7'", - "version": "==0.10.1" + "version": "==0.11.1" }, "pycodestyle": { "hashes": [ @@ -1781,122 +1933,79 @@ "markers": "python_version >= '3.6'", "version": "==2.5.0" }, - "pytz": { - "hashes": [ - "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", - "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" - ], - "version": "==2022.2.1" - }, "regex": { "hashes": [ - "sha256:003a2e1449d425afc817b5f0b3d4c4aa9072dd5f3dfbf6c7631b8dc7b13233de", - "sha256:0385d66e73cdd4462f3cc42c76a6576ddcc12472c30e02a2ae82061bff132c32", - "sha256:0394265391a86e2bbaa7606e59ac71bd9f1edf8665a59e42771a9c9adbf6fd4f", - "sha256:03ff695518482b946a6d3d4ce9cbbd99a21320e20d94913080aa3841f880abcd", - "sha256:079c182f99c89524069b9cd96f5410d6af437e9dca576a7d59599a574972707e", - "sha256:091efcfdd4178a7e19a23776dc2b1fafb4f57f4d94daf340f98335817056f874", - "sha256:0b664a4d33ffc6be10996606dfc25fd3248c24cc589c0b139feb4c158053565e", - "sha256:14216ea15efc13f28d0ef1c463d86d93ca7158a79cd4aec0f9273f6d4c6bb047", - "sha256:14a7ab070fa3aec288076eed6ed828587b805ef83d37c9bfccc1a4a7cfbd8111", - "sha256:14c71437ffb89479c89cc7022a5ea2075a842b728f37205e47c824cc17b30a42", - "sha256:18e503b1e515a10282b3f14f1b3d856194ecece4250e850fad230842ed31227f", - "sha256:19a4da6f513045f5ba00e491215bd00122e5bd131847586522463e5a6b2bd65f", - "sha256:1a901ce5cd42658ab8f8eade51b71a6d26ad4b68c7cfc86b87efc577dfa95602", - "sha256:26df88c9636a0c3f3bd9189dd435850a0c49d0b7d6e932500db3f99a6dd604d1", - "sha256:2dda4b096a6f630d6531728a45bd12c67ec3badf44342046dc77d4897277d4f2", - "sha256:322bd5572bed36a5b39952d88e072738926759422498a96df138d93384934ff8", - "sha256:360ffbc9357794ae41336b681dff1c0463193199dfb91fcad3ec385ea4972f46", - "sha256:37e5a26e76c46f54b3baf56a6fdd56df9db89758694516413757b7d127d4c57b", - "sha256:3d64e1a7e6d98a4cdc8b29cb8d8ed38f73f49e55fbaa737bdb5933db99b9de22", - "sha256:3f3b4594d564ed0b2f54463a9f328cf6a5b2a32610a90cdff778d6e3e561d08b", - "sha256:4146cb7ae6029fc83b5c905ec6d806b7e5568dc14297c423e66b86294bad6c39", - "sha256:4318f69b79f9f7d84a7420e97d4bfe872dc767c72f891d4fea5fa721c74685f7", - "sha256:4cdbfa6d2befeaee0c899f19222e9b20fc5abbafe5e9c43a46ef819aeb7b75e5", - "sha256:50e764ffbd08b06aa8c4e86b8b568b6722c75d301b33b259099f237c46b2134e", - "sha256:518272f25da93e02af4f1e94985f5042cec21557ef3591027d0716f2adda5d0a", - "sha256:592b9e2e1862168e71d9e612bfdc22c451261967dbd46681f14e76dfba7105fd", - "sha256:59a786a55d00439d8fae4caaf71581f2aaef7297d04ee60345c3594efef5648a", - "sha256:59bac44b5a07b08a261537f652c26993af9b1bbe2a29624473968dd42fc29d56", - "sha256:5d0dd8b06896423211ce18fba0c75dacc49182a1d6514c004b535be7163dca0f", - "sha256:67a4c625361db04ae40ef7c49d3cbe2c1f5ff10b5a4491327ab20f19f2fb5d40", - "sha256:6adfe300848d61a470ec7547adc97b0ccf86de86a99e6830f1d8c8d19ecaf6b3", - "sha256:6b32b45433df1fad7fed738fe15200b6516da888e0bd1fdd6aa5e50cc16b76bc", - "sha256:6c57d50d4d5eb0c862569ca3c840eba2a73412f31d9ecc46ef0d6b2e621a592b", - "sha256:6d43bd402b27e0e7eae85c612725ba1ce7798f20f6fab4e8bc3de4f263294f03", - "sha256:6e521d9db006c5e4a0f8acfef738399f72b704913d4e083516774eb51645ad7c", - "sha256:6fe1dd1021e0f8f3f454ce2811f1b0b148f2d25bb38c712fec00316551e93650", - "sha256:73b985c9fc09a7896846e26d7b6f4d1fd5a20437055f4ef985d44729f9f928d0", - "sha256:7681c49da1a2d4b905b4f53d86c9ba4506e79fba50c4a664d9516056e0f7dfcc", - "sha256:77c2879d3ba51e5ca6c2b47f2dcf3d04a976a623a8fc8236010a16c9e0b0a3c7", - "sha256:7b0c5cc3d1744a67c3b433dce91e5ef7c527d612354c1f1e8576d9e86bc5c5e2", - "sha256:7fcf7f94ccad19186820ac67e2ec7e09e0ac2dac39689f11cf71eac580503296", - "sha256:83cc32a1a2fa5bac00f4abc0e6ce142e3c05d3a6d57e23bd0f187c59b4e1e43b", - "sha256:8418ee2cb857b83881b8f981e4c636bc50a0587b12d98cb9b947408a3c484fe7", - "sha256:86df2049b18745f3cd4b0f4c4ef672bfac4b80ca488e6ecfd2bbfe68d2423a2c", - "sha256:880dbeb6bdde7d926b4d8e41410b16ffcd4cb3b4c6d926280fea46e2615c7a01", - "sha256:8aba0d01e3dfd335f2cb107079b07fdddb4cd7fb2d8c8a1986f9cb8ce9246c24", - "sha256:8dcbcc9e72a791f622a32d17ff5011326a18996647509cac0609a7fc43adc229", - "sha256:944567bb08f52268d8600ee5bdf1798b2b62ea002cc692a39cec113244cbdd0d", - "sha256:995e70bb8c91d1b99ed2aaf8ec44863e06ad1dfbb45d7df95f76ef583ec323a9", - "sha256:99945ddb4f379bb9831c05e9f80f02f079ba361a0fb1fba1fc3b267639b6bb2e", - "sha256:9a165a05979e212b2c2d56a9f40b69c811c98a788964e669eb322de0a3e420b4", - "sha256:9bc8edc5f8ef0ebb46f3fa0d02bd825bbe9cc63d59e428ffb6981ff9672f6de1", - "sha256:a1aec4ae549fd7b3f52ceaf67e133010e2fba1538bf4d5fc5cd162a5e058d5df", - "sha256:a1c4d17879dd4c4432c08a1ca1ab379f12ab54af569e945b6fc1c4cf6a74ca45", - "sha256:a2b39ee3b280e15824298b97cec3f7cbbe6539d8282cc8a6047a455b9a72c598", - "sha256:a2effeaf50a6838f3dd4d3c5d265f06eabc748f476e8441892645ae3a697e273", - "sha256:a59d0377e58d96a6f11636e97992f5b51b7e1e89eb66332d1c01b35adbabfe8a", - "sha256:a926339356fe29595f8e37af71db37cd87ff764e15da8ad5129bbaff35bcc5a6", - "sha256:a9eb9558e1d0f78e07082d8a70d5c4d631c8dd75575fae92105df9e19c736730", - "sha256:ab07934725e6f25c6f87465976cc69aef1141e86987af49d8c839c3ffd367c72", - "sha256:ad75173349ad79f9d21e0d0896b27dcb37bfd233b09047bc0b4d226699cf5c87", - "sha256:b7b701dbc124558fd2b1b08005eeca6c9160e209108fbcbd00091fcfac641ac7", - "sha256:b7bee775ff05c9d519195bd9e8aaaccfe3971db60f89f89751ee0f234e8aeac5", - "sha256:b86548b8234b2be3985dbc0b385e35f5038f0f3e6251464b827b83ebf4ed90e5", - "sha256:b9d68eb704b24bc4d441b24e4a12653acd07d2c39940548761e0985a08bc1fff", - "sha256:c0b7cb9598795b01f9a3dd3f770ab540889259def28a3bf9b2fa24d52edecba3", - "sha256:cab548d6d972e1de584161487b2ac1aa82edd8430d1bde69587ba61698ad1cfb", - "sha256:ce331b076b2b013e7d7f07157f957974ef0b0881a808e8a4a4b3b5105aee5d04", - "sha256:cfa4c956ff0a977c4823cb3b930b0a4e82543b060733628fec7ab3eb9b1abe37", - "sha256:d23ac6b4bf9e32fcde5fcdb2e1fd5e7370d6693fcac51ee1d340f0e886f50d1f", - "sha256:d2885ec6eea629c648ecc9bde0837ec6b92208b7f36381689937fe5d64a517e8", - "sha256:d2a1371dc73e921f3c2e087c05359050f3525a9a34b476ebc8130e71bec55e97", - "sha256:d3102ab9bf16bf541ca228012d45d88d2a567c9682a805ae2c145a79d3141fdd", - "sha256:d5b003d248e6f292475cd24b04e5f72c48412231961a675edcb653c70730e79e", - "sha256:d5edd3eb877c9fc2e385173d4a4e1d792bf692d79e25c1ca391802d36ecfaa01", - "sha256:d7430f041755801b712ec804aaf3b094b9b5facbaa93a6339812a8e00d7bd53a", - "sha256:d837ccf3bd2474feabee96cd71144e991472e400ed26582edc8ca88ce259899c", - "sha256:dab81cc4d58026861445230cfba27f9825e9223557926e7ec22156a1a140d55c", - "sha256:db45016364eec9ddbb5af93c8740c5c92eb7f5fc8848d1ae04205a40a1a2efc6", - "sha256:df8fe00b60e4717662c7f80c810ba66dcc77309183c76b7754c0dff6f1d42054", - "sha256:e6e6e61e9a38b6cc60ca3e19caabc90261f070f23352e66307b3d21a24a34aaf", - "sha256:ee7045623a5ace70f3765e452528b4c1f2ce669ed31959c63f54de64fe2f6ff7", - "sha256:f06cc1190f3db3192ab8949e28f2c627e1809487e2cfc435b6524c1ce6a2f391", - "sha256:f07373b6e56a6f3a0df3d75b651a278ca7bd357a796078a26a958ea1ce0588fd", - "sha256:f6e0321921d2fdc082ef90c1fd0870f129c2e691bfdc4937dcb5cd308aba95c4", - "sha256:f6e167d1ccd41d27b7b6655bb7a2dcb1b1eb1e0d2d662043470bd3b4315d8b2b", - "sha256:fcbd1edff1473d90dc5cf4b52d355cf1f47b74eb7c85ba6e45f45d0116b8edbd", - "sha256:fe428822b7a8c486bcd90b334e9ab541ce6cc0d6106993d59f201853e5e14121" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.9.13" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" + "sha256:086afe222d58b88b62847bdbd92079b4699350b4acab892f88a935db5707c790", + "sha256:0b8eb1e3bca6b48dc721818a60ae83b8264d4089a4a41d62be6d05316ec38e15", + "sha256:11d00c31aeab9a6e0503bc77e73ed9f4527b3984279d997eb145d7c7be6268fd", + "sha256:11d1f2b7a0696dc0310de0efb51b1f4d813ad4401fe368e83c0c62f344429f98", + "sha256:1b1fc2632c01f42e06173d8dd9bb2e74ab9b0afa1d698058c867288d2c7a31f3", + "sha256:20abe0bdf03630fe92ccafc45a599bca8b3501f48d1de4f7d121153350a2f77d", + "sha256:22720024b90a6ba673a725dcc62e10fb1111b889305d7c6b887ac7466b74bedb", + "sha256:2472428efc4127374f494e570e36b30bb5e6b37d9a754f7667f7073e43b0abdd", + "sha256:25f0532fd0c53e96bad84664171969de9673b4131f2297f1db850d3918d58858", + "sha256:2848bf76673c83314068241c8d5b7fa9ad9bed866c979875a0e84039349e8fa7", + "sha256:37ae17d3be44c0b3f782c28ae9edd8b47c1f1776d4cabe87edc0b98e1f12b021", + "sha256:3cd9f5dd7b821f141d3a6ca0d5d9359b9221e4f051ca3139320adea9f1679691", + "sha256:4479f9e2abc03362df4045b1332d4a2b7885b245a30d4f4b051c4083b97d95d8", + "sha256:4c49552dc938e3588f63f8a78c86f3c9c75301e813bca0bef13bdb4b87ccf364", + "sha256:539dd010dc35af935b32f248099e38447bbffc10b59c2b542bceead2bed5c325", + "sha256:54c3fa855a3f7438149de3211738dd9b5f0c733f48b54ae05aa7fce83d48d858", + "sha256:55ae114da21b7a790b90255ea52d2aa3a0d121a646deb2d3c6a3194e722fc762", + "sha256:5ccfafd98473e007cebf7da10c1411035b7844f0f204015efd050601906dbb53", + "sha256:5fc33b27b1d800fc5b78d7f7d0f287e35079ecabe68e83d46930cf45690e1c8c", + "sha256:6560776ec19c83f3645bbc5db64a7a5816c9d8fb7ed7201c5bcd269323d88072", + "sha256:6572ff287176c0fb96568adb292674b421fa762153ed074d94b1d939ed92c253", + "sha256:6b190a339090e6af25f4a5fd9e77591f6d911cc7b96ecbb2114890b061be0ac1", + "sha256:7304863f3a652dab5e68e6fb1725d05ebab36ec0390676d1736e0571ebb713ef", + "sha256:75f288c60232a5339e0ff2fa05779a5e9c74e9fc085c81e931d4a264501e745b", + "sha256:7868b8f218bf69a2a15402fde08b08712213a1f4b85a156d90473a6fb6b12b09", + "sha256:787954f541ab95d8195d97b0b8cf1dc304424adb1e07365967e656b92b38a699", + "sha256:78ac8dd8e18800bb1f97aad0d73f68916592dddf233b99d2b5cabc562088503a", + "sha256:79e29fd62fa2f597a6754b247356bda14b866131a22444d67f907d6d341e10f3", + "sha256:845a5e2d84389c4ddada1a9b95c055320070f18bb76512608374aca00d22eca8", + "sha256:86b036f401895e854de9fefe061518e78d506d8a919cc250dc3416bca03f6f9a", + "sha256:87d9951f5a538dd1d016bdc0dcae59241d15fa94860964833a54d18197fcd134", + "sha256:8a9c63cde0eaa345795c0fdeb19dc62d22e378c50b0bc67bf4667cd5b482d98b", + "sha256:93f3f1aa608380fe294aa4cb82e2afda07a7598e828d0341e124b8fd9327c715", + "sha256:9bf4a5626f2a0ea006bf81e8963f498a57a47d58907eaa58f4b3e13be68759d8", + "sha256:9d764514d19b4edcc75fd8cb1423448ef393e8b6cbd94f38cab983ab1b75855d", + "sha256:a610e0adfcb0fc84ea25f6ea685e39e74cbcd9245a72a9a7aab85ff755a5ed27", + "sha256:a81c9ec59ca2303acd1ccd7b9ac409f1e478e40e96f8f79b943be476c5fdb8bb", + "sha256:b7006105b10b59971d3b248ad75acc3651c7e4cf54d81694df5a5130a3c3f7ea", + "sha256:c07ce8e9eee878a48ebeb32ee661b49504b85e164b05bebf25420705709fdd31", + "sha256:c125a02d22c555e68f7433bac8449992fa1cead525399f14e47c2d98f2f0e467", + "sha256:c37df2a060cb476d94c047b18572ee2b37c31f831df126c0da3cd9227b39253d", + "sha256:c869260aa62cee21c5eb171a466c0572b5e809213612ef8d495268cd2e34f20d", + "sha256:c88e8c226473b5549fe9616980ea7ca09289246cfbdf469241edf4741a620004", + "sha256:cd1671e9d5ac05ce6aa86874dd8dfa048824d1dbe73060851b310c6c1a201a96", + "sha256:cde09c4fdd070772aa2596d97e942eb775a478b32459e042e1be71b739d08b77", + "sha256:cf86b4328c204c3f315074a61bc1c06f8a75a8e102359f18ce99fbcbbf1951f0", + "sha256:d5bbe0e1511b844794a3be43d6c145001626ba9a6c1db8f84bdc724e91131d9d", + "sha256:d895b4c863059a4934d3e874b90998df774644a41b349ebb330f85f11b4ef2c0", + "sha256:db034255e72d2995cf581b14bb3fc9c00bdbe6822b49fcd4eef79e1d5f232618", + "sha256:dbb3f87e15d3dd76996d604af8678316ad2d7d20faa394e92d9394dfd621fd0c", + "sha256:dc80df325b43ffea5cdea2e3eaa97a44f3dd298262b1c7fe9dbb2a9522b956a7", + "sha256:dd7200b4c27b68cf9c9646da01647141c6db09f48cc5b51bc588deaf8e98a797", + "sha256:df45fac182ebc3c494460c644e853515cc24f5ad9da05f8ffb91da891bfee879", + "sha256:e152461e9a0aedec7d37fc66ec0fa635eca984777d3d3c3e36f53bf3d3ceb16e", + "sha256:e2396e0678167f2d0c197da942b0b3fb48fee2f0b5915a0feb84d11b6686afe6", + "sha256:e76b6fc0d8e9efa39100369a9b3379ce35e20f6c75365653cf58d282ad290f6f", + "sha256:ea3c0cb56eadbf4ab2277e7a095676370b3e46dbfc74d5c383bd87b0d6317910", + "sha256:ef3f528fe1cc3d139508fe1b22523745aa77b9d6cb5b0bf277f48788ee0b993f", + "sha256:fdf7ad455f1916b8ea5cdbc482d379f6daf93f3867b4232d14699867a5a13af7", + "sha256:fffe57312a358be6ec6baeb43d253c36e5790e436b7bf5b7a38df360363e88e9" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==2023.3.23" }, "sqlparse": { "hashes": [ - "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34", - "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268" + "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", + "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" ], - "markers": "python_version >= '3.5'", - "version": "==0.4.3" + "markers": "python_full_version >= '3.5.0'", + "version": "==0.4.4" }, "toml": { "hashes": [ @@ -1936,6 +2045,14 @@ "markers": "python_version >= '3.6'", "version": "==1.5.4" }, + "typing-extensions": { + "hashes": [ + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + ], + "markers": "python_version >= '3.7'", + "version": "==4.5.0" + }, "unittest-xml-reporting": { "hashes": [ "sha256:edd8d3170b40c3a81b8cf910f46c6a304ae2847ec01036d02e9c0f9b85762d28", @@ -1943,14 +2060,6 @@ ], "index": "pypi", "version": "==3.2.0" - }, - "urllib3": { - "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" - ], - "markers": "python_version >= '3.6'", - "version": "==1.26.12" } } } diff --git a/backend/clubs/admin.py b/backend/clubs/admin.py index 9dcd1c7c5..24557da10 100644 --- a/backend/clubs/admin.py +++ b/backend/clubs/admin.py @@ -14,6 +14,8 @@ AdminNote, Advisor, ApplicationCommittee, + ApplicationCycle, + ApplicationExtension, ApplicationMultipleChoice, ApplicationQuestion, ApplicationQuestionResponse, @@ -406,12 +408,18 @@ class ZoomMeetingVisitAdmin(admin.ModelAdmin): list_filter = (("leave_time", admin.EmptyFieldListFilter),) +class ApplicationSubmissionAdmin(admin.ModelAdmin): + search_fields = ("user__username",) + list_display = ("user", "id", "created_at", "status") + + admin.site.register(Asset) admin.site.register(ApplicationCommittee) +admin.site.register(ApplicationExtension) admin.site.register(ApplicationMultipleChoice) admin.site.register(ApplicationQuestion) admin.site.register(ApplicationQuestionResponse) -admin.site.register(ApplicationSubmission) +admin.site.register(ApplicationSubmission, ApplicationSubmissionAdmin) admin.site.register(Advisor, AdvisorAdmin) admin.site.register(Club, ClubAdmin) admin.site.register(ClubFair, ClubFairAdmin) @@ -445,5 +453,6 @@ class ZoomMeetingVisitAdmin(admin.ModelAdmin): admin.site.register(Year, YearAdmin) admin.site.register(ZoomMeetingVisit, ZoomMeetingVisitAdmin) admin.site.register(AdminNote) +admin.site.register(ApplicationCycle) admin.site.register(Ticket) admin.site.register(Cart) diff --git a/backend/clubs/management/commands/populate.py b/backend/clubs/management/commands/populate.py index a499bacef..57c8f3236 100644 --- a/backend/clubs/management/commands/populate.py +++ b/backend/clubs/management/commands/populate.py @@ -226,7 +226,7 @@ members and to answer questions.

- If you would like to particpate in the Fall 2020 SAC fair, check + If you would like to particpate in the SAC fair, check the box below. If you check the box below, your club information will be shared with the Student Activites Council and more details will be sent to you at a later date. @@ -396,6 +396,12 @@ def get_image(url): tag_undergrad, _ = Tag.objects.get_or_create(name="Undergraduate") tag_generic, _ = Tag.objects.get_or_create(name="Generic") + wharton_badge, _ = Badge.objects.get_or_create( + label="Wharton Council", + purpose="Dummy badge to mock Wharton-affiliated clubs", + visible=True, + ) + for i in range(1, 50): club, created = Club.objects.get_or_create( code="z-club-{}".format(i), @@ -408,6 +414,10 @@ def get_image(url): }, ) + if 10 <= i <= 15: + # Make some clubs Wharton-affiliated + club.badges.add(wharton_badge) + if created: club.available_virtually = i % 2 == 0 club.appointment_needed = i % 3 == 0 diff --git a/backend/clubs/management/commands/update_club_counts.py b/backend/clubs/management/commands/update_club_counts.py new file mode 100644 index 000000000..01ff17676 --- /dev/null +++ b/backend/clubs/management/commands/update_club_counts.py @@ -0,0 +1,36 @@ +from django.core.management.base import BaseCommand +from django.db.models import Count, Q + +from clubs.models import Club + + +class Command(BaseCommand): + help = "Update stored favorite and membership counts." + + def handle(self, *args, **kwargs): + try: + queryset = Club.objects.all().annotate( + temp_favorite_count=Count("favorite", distinct=True), + temp_membership_count=Count( + "membership", distinct=True, filter=Q(active=True) + ), + ) + + for club in queryset: + club.favorite_count = club.temp_favorite_count + club.membership_count = club.temp_membership_count + Club.objects.bulk_update(queryset, ["favorite_count", "membership_count"]) + + self.stdout.write( + self.style.SUCCESS( + "Successfully updated all club favorite and membership counts!" + ) + ) + except Exception as e: + self.stdout.write( + self.style.ERROR( + "An error was encountered while updating" + + "club favorite and membership counts!" + ) + ) + self.stdout.write(e) diff --git a/backend/clubs/management/commands/wharton_council_application.py b/backend/clubs/management/commands/wharton_council_application.py deleted file mode 100644 index 3fe8c75af..000000000 --- a/backend/clubs/management/commands/wharton_council_application.py +++ /dev/null @@ -1,135 +0,0 @@ -from datetime import datetime - -from django.core.management.base import BaseCommand - -from clubs.models import ( - ApplicationMultipleChoice, - ApplicationQuestion, - Badge, - Club, - ClubApplication, -) - - -class Command(BaseCommand): - help = "Helper to automatically create the Wharton council club applications." - web_execute = True - - def add_arguments(self, parser): - parser.add_argument( - "application_start_time", - type=str, - help="Date and time at which the centralized application opens.", - ) - parser.add_argument( - "application_end_time", - type=str, - help="Date and time at which the centralized application closes.", - ) - parser.add_argument( - "result_release_time", - type=str, - help="Date and time at which the centralized application results " - "are released.", - ) - parser.add_argument( - "--dry-run", - dest="dry_run", - action="store_true", - help="Do not actually create applications.", - ) - parser.add_argument( - "--clubs", - dest="clubs", - type=str, - help="The comma separated list of club codes for which to create the " - "centralized applications.", - ) - parser.set_defaults( - application_start_time="2021-09-04 00:00:00", - application_end_time="2021-09-04 00:00:00", - result_release_time="2021-09-04 00:00:00", - dry_run=False, - clubs="", - ) - - def handle(self, *args, **kwargs): - dry_run = kwargs["dry_run"] - club_names = list(map(lambda x: x.strip(), kwargs["clubs"].split(","))) - clubs = [] - - if club_names == [] or all(not name for name in club_names): - wc_badge = Badge.objects.filter( - label="Wharton Council", purpose="org", - ).first() - for club in Club.objects.all(): - if wc_badge in club.badges.all(): - clubs.append(club) - else: - for code in club_names: - target_club = Club.objects.filter(code=code).first() - if target_club is not None: - clubs.append(target_club) - - application_start_time = datetime.strptime( - kwargs["application_start_time"], "%Y-%m-%d %H:%M:%S" - ) - application_end_time = datetime.strptime( - kwargs["application_end_time"], "%Y-%m-%d %H:%M:%S" - ) - result_release_time = datetime.strptime( - kwargs["result_release_time"], "%Y-%m-%d %H:%M:%S" - ) - - prompt_one = ( - "Tell us about a time you took " "initiative or demonstrated leadership" - ) - prompt_two = "Tell us about a time you faced a challenge and how you solved it" - prompt_three = "Tell us about a time you collaborated well in a team" - - if len(clubs) == 0: - self.stdout.write("No valid club codes provided, returning...") - - for club in clubs: - name = f"{club.name} Application" - if dry_run: - self.stdout.write(f"Would have created application for {club.name}") - else: - self.stdout.write(f"Creating application for {club.name}") - application = ClubApplication.objects.create( - name=name, - club=club, - application_start_time=application_start_time, - application_end_time=application_end_time, - result_release_time=result_release_time, - is_wharton_council=True, - ) - external_url = ( - f"https://pennclubs.com/club/{club.code}/" - f"application/{application.pk}" - ) - application.external_url = external_url - application.save() - prompt = ( - "Choose one of the following " "prompts for your personal statement" - ) - prompt_question = ApplicationQuestion.objects.create( - question_type=ApplicationQuestion.MULTIPLE_CHOICE, - application=application, - prompt=prompt, - ) - ApplicationMultipleChoice.objects.create( - value=prompt_one, question=prompt_question - ) - ApplicationMultipleChoice.objects.create( - value=prompt_two, question=prompt_question - ) - ApplicationMultipleChoice.objects.create( - value=prompt_three, question=prompt_question - ) - ApplicationQuestion.objects.create( - question_type=ApplicationQuestion.FREE_RESPONSE, - prompt="Answer the prompt you selected", - word_limit=150, - application=application, - ) diff --git a/backend/clubs/migrations/0090_adminnote.py b/backend/clubs/migrations/0088_adminnote.py similarity index 95% rename from backend/clubs/migrations/0090_adminnote.py rename to backend/clubs/migrations/0088_adminnote.py index d75043e60..e6ad481cc 100644 --- a/backend/clubs/migrations/0090_adminnote.py +++ b/backend/clubs/migrations/0088_adminnote.py @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("clubs", "0089_alter_applicationsubmission_status"), + ("clubs", "0087_questionanswer_users_liked"), ] operations = [ diff --git a/backend/clubs/migrations/0088_alter_applicationsubmission_status.py b/backend/clubs/migrations/0088_alter_applicationsubmission_status.py deleted file mode 100644 index 5710f001a..000000000 --- a/backend/clubs/migrations/0088_alter_applicationsubmission_status.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 3.2.6 on 2021-10-06 21:33 - -from django.db import migrations, models - -from clubs.models import ApplicationSubmission - - -def reassign_status(apps, schema_editor): - submissions = ApplicationSubmission.objects.all() - for submission in submissions: - if submission.status == 1: - pass - elif submission.status == 2: - submission.status = ApplicationSubmission.PENDING - elif submission.status == 3: - submission.status = ApplicationSubmission.PENDING - elif submission.status == 4: - pass - elif submission.status == 5: - submission.status = ApplicationSubmission.REJECTED_AFTER_WRITTEN - submission.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("clubs", "0087_questionanswer_users_liked"), - ] - - operations = [ - migrations.AlterField( - model_name="applicationsubmission", - name="status", - field=models.IntegerField( - choices=[ - (1, "Pending"), - (2, "Rejected after interview(s)"), - (3, "Rejected after written application"), - (4, "Accepted"), - ], - default=1, - ), - ), - migrations.RunPython(reassign_status), - ] diff --git a/backend/clubs/migrations/0089_alter_applicationsubmission_status.py b/backend/clubs/migrations/0089_alter_applicationsubmission_status.py deleted file mode 100644 index 9db95bf25..000000000 --- a/backend/clubs/migrations/0089_alter_applicationsubmission_status.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 3.2.6 on 2021-10-06 22:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("clubs", "0088_alter_applicationsubmission_status"), - ] - - operations = [ - migrations.AlterField( - model_name="applicationsubmission", - name="status", - field=models.IntegerField( - choices=[ - (1, "Pending"), - (2, "Rejected after written application"), - (3, "Rejected after interview(s)"), - (4, "Accepted"), - ], - default=1, - ), - ), - ] diff --git a/backend/clubs/migrations/0089_auto_20230103_1239.py b/backend/clubs/migrations/0089_auto_20230103_1239.py new file mode 100644 index 000000000..f027204f2 --- /dev/null +++ b/backend/clubs/migrations/0089_auto_20230103_1239.py @@ -0,0 +1,60 @@ +# Generated by Django 3.2.16 on 2023-01-03 17:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0088_adminnote"), + ] + + operations = [ + migrations.AlterModelOptions( + name="historicalclub", + options={ + "get_latest_by": ("history_date", "history_id"), + "ordering": ("-history_date", "-history_id"), + "verbose_name": "historical club", + "verbose_name_plural": "historical clubs", + }, + ), + migrations.AddField( + model_name="applicationsubmission", + name="notified", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="applicationsubmission", + name="reason", + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name="clubapplication", + name="acceptance_email", + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name="clubapplication", + name="rejection_email", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="applicationsubmission", + name="status", + field=models.IntegerField( + choices=[ + (1, "Pending"), + (2, "Rejected after written application"), + (3, "Rejected after interview(s)"), + (4, "Accepted"), + ], + default=1, + ), + ), + migrations.AlterField( + model_name="historicalclub", + name="history_date", + field=models.DateTimeField(db_index=True), + ), + ] diff --git a/backend/clubs/migrations/0090_auto_20230106_1443.py b/backend/clubs/migrations/0090_auto_20230106_1443.py new file mode 100644 index 000000000..d7ce13226 --- /dev/null +++ b/backend/clubs/migrations/0090_auto_20230106_1443.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.16 on 2023-01-06 19:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0089_auto_20230103_1239"), + ] + + operations = [ + migrations.CreateModel( + name="ApplicationCycle", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("start_date", models.DateTimeField(null=True)), + ("end_date", models.DateTimeField(null=True)), + ], + ), + migrations.AddField( + model_name="clubapplication", + name="application_cycle", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="clubs.applicationcycle", + ), + ), + ] diff --git a/backend/clubs/migrations/0091_applicationextension.py b/backend/clubs/migrations/0091_applicationextension.py new file mode 100644 index 000000000..43cd96efd --- /dev/null +++ b/backend/clubs/migrations/0091_applicationextension.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.18 on 2023-11-25 03:58 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("clubs", "0090_auto_20230106_1443"), + ] + + operations = [ + migrations.CreateModel( + name="ApplicationExtension", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("end_time", models.DateTimeField()), + ( + "application", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="extensions", + to="clubs.clubapplication", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={"unique_together": {("user", "application")}}, + ), + ] diff --git a/backend/clubs/migrations/0091_clubapplication_application_end_time_exception.py b/backend/clubs/migrations/0091_clubapplication_application_end_time_exception.py new file mode 100644 index 000000000..92377bdb5 --- /dev/null +++ b/backend/clubs/migrations/0091_clubapplication_application_end_time_exception.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-11-17 22:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0090_auto_20230106_1443"), + ] + + operations = [ + migrations.AddField( + model_name="clubapplication", + name="application_end_time_exception", + field=models.BooleanField(blank=True, default=False), + ), + ] diff --git a/backend/clubs/migrations/0092_merge_20240106_1117.py b/backend/clubs/migrations/0092_merge_20240106_1117.py new file mode 100644 index 000000000..9656c2c26 --- /dev/null +++ b/backend/clubs/migrations/0092_merge_20240106_1117.py @@ -0,0 +1,13 @@ +# Generated by Django 3.2.18 on 2024-01-06 16:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0091_applicationextension"), + ("clubs", "0091_clubapplication_application_end_time_exception"), + ] + + operations = [] diff --git a/backend/clubs/migrations/0093_auto_20240106_1153.py b/backend/clubs/migrations/0093_auto_20240106_1153.py new file mode 100644 index 000000000..1e2ab1a0f --- /dev/null +++ b/backend/clubs/migrations/0093_auto_20240106_1153.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.18 on 2024-01-06 16:53 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("clubs", "0092_merge_20240106_1117"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="applicationquestionresponse", + unique_together={("question", "submission")}, + ), + migrations.AlterUniqueTogether( + name="applicationsubmission", + unique_together={("user", "application", "committee")}, + ), + ] diff --git a/backend/clubs/migrations/0094_applicationcycle_release_date.py b/backend/clubs/migrations/0094_applicationcycle_release_date.py new file mode 100644 index 000000000..62ad134f9 --- /dev/null +++ b/backend/clubs/migrations/0094_applicationcycle_release_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2024-01-11 14:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0093_auto_20240106_1153"), + ] + + operations = [ + migrations.AddField( + model_name="applicationcycle", + name="release_date", + field=models.DateTimeField(null=True), + ), + ] diff --git a/backend/clubs/migrations/0095_rm_field_add_count.py b/backend/clubs/migrations/0095_rm_field_add_count.py new file mode 100644 index 000000000..f3ada60c1 --- /dev/null +++ b/backend/clubs/migrations/0095_rm_field_add_count.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.18 on 2024-02-03 22:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0094_applicationcycle_release_date"), + ] + + operations = [ + migrations.RemoveField(model_name="applicationsubmission", name="archived",), + migrations.AddField( + model_name="club", + name="favorite_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="club", + name="membership_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="historicalclub", + name="favorite_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="historicalclub", + name="membership_count", + field=models.IntegerField(default=0), + ), + ] diff --git a/backend/clubs/migrations/0091_cart_ticket.py b/backend/clubs/migrations/0096_cart_ticket.py similarity index 98% rename from backend/clubs/migrations/0091_cart_ticket.py rename to backend/clubs/migrations/0096_cart_ticket.py index 74ae8cdd9..54b1afa63 100644 --- a/backend/clubs/migrations/0091_cart_ticket.py +++ b/backend/clubs/migrations/0096_cart_ticket.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("clubs", "0090_adminnote"), + ("clubs", "0095_rm_field_add_count"), ] operations = [ diff --git a/backend/clubs/mixins.py b/backend/clubs/mixins.py index 22afe3964..16fc46137 100644 --- a/backend/clubs/mixins.py +++ b/backend/clubs/mixins.py @@ -148,7 +148,7 @@ def get_xlsx_column_name(self, key): if hasattr(serializer_class, "get_xlsx_column_name"): val = serializer_class.get_xlsx_column_name(key) if val is None: - val = key.replace("_", " ").title() + val = key.replace("_", " ") self._column_cache[key] = val return val diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 4e7026497..cf622c0ae 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -22,7 +22,9 @@ from django.template.loader import render_to_string from django.utils import timezone from django.utils.crypto import get_random_string +from django.utils.functional import cached_property from ics import Calendar +from jinja2 import Environment, meta from model_clone.models import CloneModel from phonenumber_field.modelfields import PhoneNumberField from simple_history.models import HistoricalRecords @@ -323,6 +325,13 @@ class Club(models.Model): appointment_needed = models.BooleanField(default=False) signature_events = models.TextField(blank=True) # html + # cache club aggregation counts + favorite_count = models.IntegerField(default=0) + membership_count = models.IntegerField(default=0) + + # cache club rankings + rank = models.IntegerField(default=0) + # cache club rankings rank = models.IntegerField(default=0) @@ -338,6 +347,11 @@ def __str__(self): def create_thumbnail(self, request=None): return create_thumbnail_helper(self, request, 200) + @cached_property + def is_wharton(self): + wc_badge = Badge.objects.filter(label="Wharton Council").first() + return wc_badge in self.badges.all() + def add_ics_events(self): """ Fetch the ICS events from the club's calendar URL @@ -1526,22 +1540,43 @@ def __str__(self): return self.user.username +class ApplicationCycle(models.Model): + """ + Represents an application cycle attached to club applications + """ + + name = models.CharField(max_length=255) + start_date = models.DateTimeField(null=True) + end_date = models.DateTimeField(null=True) + release_date = models.DateTimeField(null=True) + + def __str__(self): + return self.name + + class ClubApplication(CloneModel): """ Represents custom club application. """ DEFAULT_COMMITTEE = "General Member" + VALID_TEMPLATE_TOKENS = {"name", "reason", "committee"} club = models.ForeignKey(Club, on_delete=models.CASCADE) description = models.TextField(blank=True) application_start_time = models.DateTimeField() application_end_time = models.DateTimeField() + application_end_time_exception = models.BooleanField(default=False, blank=True) name = models.TextField(blank=True) result_release_time = models.DateTimeField() + application_cycle = models.ForeignKey( + ApplicationCycle, on_delete=models.SET_NULL, null=True + ) external_url = models.URLField(blank=True) is_active = models.BooleanField(default=False, blank=True) is_wharton_council = models.BooleanField(default=False, blank=True) + acceptance_email = models.TextField(blank=True) + rejection_email = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -1559,12 +1594,53 @@ def __str__(self): self.application_end_time, ) - @property + @cached_property def season(self): - semester = "Fall" if 8 <= self.application_start_time.month <= 11 else "Spring" + semester = "Fall" if 8 <= self.application_start_time.month <= 12 else "Spring" year = str(self.application_start_time.year) return f"{semester} {year}" + @classmethod + def validate_template(cls, template): + environment = Environment() + j2_template = environment.parse(template) + tokens = meta.find_undeclared_variables(j2_template) + return all(t in cls.VALID_TEMPLATE_TOKENS for t in tokens) + + +class ApplicationExtension(models.Model): + """ + Represents an individual club application extension. + """ + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + application = models.ForeignKey( + ClubApplication, related_name="extensions", on_delete=models.CASCADE + ) + end_time = models.DateTimeField() + + def send_extension_mail(self): + context = { + "name": self.user.first_name, + "application_name": self.application.name, + "end_time": self.end_time, + "club": self.application.club.name, + "url": ( + f"https://pennclubs.com/club/{self.application.club.code}" + f"/application/{self.application.pk}/" + ), + } + + send_mail_helper( + name="application_extension", + subject=f"Application Extension for {self.application.name}", + emails=[self.user.email], + context=context, + ) + + class Meta: + unique_together = (("user", "application"),) + class ApplicationCommittee(models.Model): """ @@ -1648,6 +1724,7 @@ class ApplicationSubmission(models.Model): ) status = models.IntegerField(choices=STATUS_TYPES, default=PENDING) user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, null=False) + reason = models.TextField(blank=True) application = models.ForeignKey( ClubApplication, related_name="submissions", @@ -1660,10 +1737,16 @@ class ApplicationSubmission(models.Model): on_delete=models.SET_NULL, null=True, ) - archived = models.BooleanField(default=False) + notified = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) + def __str__(self): + return f"{self.user.first_name}: {self.application.name}" + + class Meta: + unique_together = (("user", "application", "committee"),) + class ApplicationQuestionResponse(models.Model): """ @@ -1692,6 +1775,9 @@ class ApplicationQuestionResponse(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + class Meta: + unique_together = (("question", "submission"),) + class QuestionResponse(models.Model): """ diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index f44761caa..573a58e4f 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -1,13 +1,12 @@ -import datetime import json import re from collections import OrderedDict from urllib.parse import parse_qs, urlparse import bleach -import pytz from django.conf import settings from django.contrib.auth import get_user_model +from django.core.cache import cache from django.core.exceptions import ValidationError as DjangoValidationError from django.core.validators import URLValidator from django.db import models @@ -22,6 +21,8 @@ AdminNote, Advisor, ApplicationCommittee, + ApplicationCycle, + ApplicationExtension, ApplicationMultipleChoice, ApplicationQuestion, ApplicationQuestionResponse, @@ -96,6 +97,28 @@ def save(self): return super().save() +class ApplicationCycleSerializer(serializers.ModelSerializer): + class Meta: + model = ApplicationCycle + fields = ["id", "name", "start_date", "end_date", "release_date"] + + def validate(self, data): + """ + Check that start_date <= end_date <= release_date + """ + start_date = data.get("start_date") + end_date = data.get("end_date") + release_date = data.get("release_date") + + if start_date and end_date and start_date >= end_date: + raise serializers.ValidationError("Start must be before end.") + + if end_date and release_date and end_date >= release_date: + raise serializers.ValidationError("End must be before release.") + + return data + + class TagSerializer(serializers.ModelSerializer): clubs = serializers.IntegerField(read_only=True) @@ -1015,6 +1038,7 @@ class Meta: "is_favorite", "is_member", "is_subscribe", + "is_wharton", "membership_count", "recruiting_cycle", "name", @@ -2196,6 +2220,7 @@ class Meta(ClubSerializer.Meta): "terms", "owners", "officers", + "approved_on", ] @@ -2444,6 +2469,86 @@ class Meta: fields = ("text", "multiple_choice", "question_type", "question") +class ApplicationExtensionSerializer(serializers.ModelSerializer): + first_name = serializers.CharField(source="user.first_name", read_only=True) + last_name = serializers.CharField(source="user.last_name", read_only=True) + username = serializers.CharField(source="user.username", read_only=False) + graduation_year = serializers.CharField( + source="user.profile.graduation_year", read_only=True + ) + + class Meta: + model = ApplicationExtension + fields = ( + "id", + "username", + "first_name", + "last_name", + "graduation_year", + "end_time", + ) + + def create(self, validated_data): + username = validated_data.get("user").pop("username") + validated_data["user"] = get_user_model().objects.get(username=username) + + application_pk = self.context["view"].kwargs.get("application_pk") + validated_data["application"] = ClubApplication.objects.filter( + pk=application_pk + ).first() + + return super().create(validated_data) + + def update(self, instance, validated_data): + if user_field := validated_data.pop("user", None): + username = user_field.pop("username") + user = get_user_model().objects.get(username=username) + instance.user = user + return super().update(instance, validated_data) + + def validate(self, data): + username = None + if user_field := data.get("user") or not self.instance: + username = user_field.get("username") + user = get_user_model().objects.filter(username=username).first() + if not user: + raise serializers.ValidationError("Please provide a valid username!") + + application_pk = self.context["view"].kwargs.get("application_pk") + application = ClubApplication.objects.filter(pk=application_pk).first() + + if not application: + raise serializers.ValidationError("Invalid application id!") + + extension_exists = ApplicationExtension.objects.filter( + user=user, application=application + ).exists() + modify_username = not self.instance or ( + username and self.instance.user.username != username + ) + + if modify_username and extension_exists: + raise serializers.ValidationError( + "An extension for this user and application already exists!" + ) + + extension_end_time = data.get("end_time") + if ( + extension_end_time + and extension_end_time <= application.application_end_time + ): + raise serializers.ValidationError( + "Extension end time must be greater than the application end time!" + ) + + return data + + def save(self): + extension_obj = super().save() + extension_obj.send_extension_mail() + return extension_obj + + class ApplicationSubmissionSerializer(serializers.ModelSerializer): committee = ApplicationCommitteeSerializer(required=False, read_only=True) responses = ApplicationQuestionResponseSerializer( @@ -2472,23 +2577,6 @@ def get_application_link(self, obj): # cannot link to the application if the application has been deleted return "#" - def validate(self, data): - application_start_time = data["application_start_time"] - application_end_time = data["application_end_time"] - now = pytz.UTC.localize(datetime.datetime.now()) - - if now < application_start_time: - raise serializers.ValidationError( - "You cannot submit before the application has opened." - ) - - if now > application_end_time: - raise serializers.ValidationError( - "You cannot submit after the application deadline." - ) - - return data - class Meta: model = ApplicationSubmission fields = ( @@ -2500,6 +2588,8 @@ class Meta: "status", "responses", "club", + "notified", + "reason", "name", "application_link", "first_name", @@ -2508,6 +2598,7 @@ class Meta: "code", "graduation_year", ) + read_only_fields = fields class ApplicationSubmissionUserSerializer(ApplicationSubmissionSerializer): @@ -2542,6 +2633,10 @@ class ApplicationSubmissionCSVSerializer(serializers.ModelSerializer): email = serializers.CharField(source="user.email") graduation_year = serializers.CharField(source="user.profile.graduation_year") committee = serializers.SerializerMethodField("get_committee") + status = serializers.SerializerMethodField("get_status") + + def get_status(self, obj): + return dict(ApplicationSubmission.STATUS_TYPES).get(obj, "Unknown") def get_name(self, obj): """ @@ -2617,11 +2712,15 @@ class Meta: "email", "graduation_year", "committee", + "notified", + "reason", + "status", ) class ClubApplicationSerializer(ClubRouteMixin, serializers.ModelSerializer): name = serializers.SerializerMethodField("get_name") + cycle = serializers.SerializerMethodField("get_cycle") committees = ApplicationCommitteeSerializer( many=True, required=False, read_only=True ) @@ -2629,9 +2728,16 @@ class ClubApplicationSerializer(ClubRouteMixin, serializers.ModelSerializer): club = serializers.SlugRelatedField(slug_field="code", read_only=True) updated_at = serializers.SerializerMethodField("get_updated_time", read_only=True) club_image_url = serializers.SerializerMethodField("get_image_url", read_only=True) - season = serializers.CharField(read_only=True) + external_url = serializers.SerializerMethodField("get_external_url") active = serializers.SerializerMethodField("get_active", read_only=True) + def get_external_url(self, obj): + default_url = f"https://pennclubs.com/club/{obj.club.code}/application/{obj.pk}" + return obj.external_url if obj.external_url else default_url + + def get_cycle(self, obj): + return obj.application_cycle.name if obj.application_cycle else obj.season + def get_active(self, obj): now = timezone.now() return obj.application_end_time >= now @@ -2661,20 +2767,45 @@ def get_image_url(self, obj): return image.url def validate(self, data): - application_start_time = data["application_start_time"] - application_end_time = data["application_end_time"] - result_release_time = data["result_release_time"] + acceptance_template = data.get("acceptance_email", "") + rejection_template = data.get("rejection_email", "") + request = self.context["request"].data - if application_start_time > application_end_time: + if "committees" in request and data["application_start_time"] < timezone.now(): raise serializers.ValidationError( - "Your application start time must be less than the end time!" + "You cannot edit committees once the application is open" ) - if application_end_time > result_release_time: + if not ClubApplication.validate_template( + acceptance_template + ) or not ClubApplication.validate_template(rejection_template): raise serializers.ValidationError( - "Your application end time must be less than the result release time!" + "Your application email templates contain invalid variables!" ) + if all( + field in data + for field in [ + "application_start_time", + "application_end_time", + "result_release_time", + ] + ): + application_start_time = data["application_start_time"] + application_end_time = data["application_end_time"] + result_release_time = data["result_release_time"] + + if application_start_time > application_end_time: + raise serializers.ValidationError( + "Your application start time must be less than the end time!" + ) + + if application_end_time > result_release_time: + raise serializers.ValidationError( + """Your application end time must be less than + the result release time!""" + ) + return data def save(self): @@ -2684,7 +2815,7 @@ def save(self): request = self.context["request"].data # only allow modifications to committees if the application is not yet open - now = pytz.timezone("America/New_York").localize(datetime.datetime.now()) + now = timezone.now() if "committees" in request and application_obj.application_start_time > now: committees = map( lambda x: x["value"] if "value" in x else x["name"], @@ -2694,16 +2825,17 @@ def save(self): application=application_obj ) # nasty hack for idempotency + prev_committee_names = prev_committees.values("name") for prev_committee in prev_committees: if prev_committee.name not in committees: prev_committee.delete() - prev_committee_names = prev_committees.values("name") for name in committees: if name not in prev_committee_names: ApplicationCommittee.objects.create( name=name, application=application_obj, ) + cache.delete(f"clubapplication:{application_obj.id}") return application_obj @@ -2711,11 +2843,14 @@ class Meta: model = ClubApplication fields = ( "id", - "season", "active", "name", + "cycle", + "acceptance_email", + "rejection_email", "application_start_time", "application_end_time", + "application_end_time_exception", "result_release_time", "external_url", "committees", @@ -2734,6 +2869,18 @@ class Meta(ClubApplicationSerializer.Meta): pass +class ManagedClubApplicationSerializer(ClubApplicationSerializer): + name = serializers.CharField(required=False, allow_blank=True) + + class Meta(ClubApplicationSerializer.Meta): + read_only_fields = ( + "external_url", + "application_start_time", + "application_end_time", + "result_release_time", + ) + + class NoteSerializer(ManyToManySaveMixin, serializers.ModelSerializer): creator = serializers.HiddenField(default=serializers.CurrentUserDefault()) creating_club = serializers.SlugRelatedField( diff --git a/backend/clubs/urls.py b/backend/clubs/urls.py index 1a740ccb2..7da672904 100644 --- a/backend/clubs/urls.py +++ b/backend/clubs/urls.py @@ -4,6 +4,7 @@ from clubs.views import ( AdminNoteViewSet, AdvisorViewSet, + ApplicationExtensionViewSet, ApplicationQuestionViewSet, ApplicationSubmissionUserViewSet, ApplicationSubmissionViewSet, @@ -51,6 +52,7 @@ UserZoomAPIView, WhartonApplicationAPIView, WhartonApplicationStatusAPIView, + WhartonCyclesView, YearViewSet, email_preview, ) @@ -79,6 +81,12 @@ router.register( r"external/members/(?P.+)", ExternalMemberListViewSet, basename="external" ) +router.register( + r"cycles", WhartonCyclesView, basename="wharton-applications-create", +) +router.register( + r"whartonapplications", WhartonApplicationAPIView, basename="wharton", +) router.register(r"submissions", ApplicationSubmissionUserViewSet, basename="submission") clubs_router = routers.NestedSimpleRouter(router, r"clubs", lookup="club") @@ -115,6 +123,10 @@ basename="club-application-submissions", ) +applications_router.register( + r"extensions", ApplicationExtensionViewSet, basename="club-application-extensions" +) + router.register(r"booths", ClubBoothsViewSet, basename="club-booth") urlpatterns = [ @@ -153,11 +165,6 @@ MeetingZoomWebhookAPIView.as_view(), name="webhooks-meeting", ), - path( - r"whartonapplications/", - WhartonApplicationAPIView.as_view(), - name="wharton-applications", - ), path( r"whartonapplications/status/", WhartonApplicationStatusAPIView.as_view(), diff --git a/backend/clubs/views.py b/backend/clubs/views.py index f8e3bcefd..931d3ad1c 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -11,6 +11,7 @@ from functools import wraps from urllib.parse import urlparse +import pandas as pd import pytz import qrcode import requests @@ -20,9 +21,11 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission +from django.core import mail from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.files.uploadedfile import UploadedFile +from django.core.mail import EmailMultiAlternatives from django.core.management import call_command, get_commands, load_command_class from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import validate_email @@ -36,18 +39,21 @@ Q, TextField, ) -from django.db.models.expressions import RawSQL, Value -from django.db.models.functions import SHA1, Lower, Trunc -from django.db.models.functions.text import Concat +from django.db.models.expressions import Value +from django.db.models.functions import SHA1, Concat, Lower, Trunc from django.db.models.query import prefetch_related_objects from django.http import HttpResponse from django.shortcuts import get_object_or_404, render from django.template.loader import render_to_string from django.utils import timezone +from django.utils.decorators import method_decorator from django.utils.text import slugify +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_cookie from ics import Calendar as ICSCal from ics import Event as ICSEvent from ics import parse as ICSParse +from jinja2 import Template from options.models import Option from rest_framework import filters, generics, parsers, serializers, status, viewsets from rest_framework.decorators import action @@ -65,6 +71,8 @@ from clubs.models import ( AdminNote, Advisor, + ApplicationCycle, + ApplicationExtension, ApplicationMultipleChoice, ApplicationQuestion, ApplicationQuestionResponse, @@ -122,6 +130,8 @@ from clubs.serializers import ( AdminNoteSerializer, AdvisorSerializer, + ApplicationCycleSerializer, + ApplicationExtensionSerializer, ApplicationQuestionResponseSerializer, ApplicationQuestionSerializer, ApplicationSubmissionCSVSerializer, @@ -146,6 +156,7 @@ FavoriteWriteSerializer, FavouriteEventSerializer, MajorSerializer, + ManagedClubApplicationSerializer, MembershipInviteSerializer, MembershipRequestSerializer, MembershipSerializer, @@ -1021,13 +1032,7 @@ class ClubViewSet(XLSXFormatterMixin, viewsets.ModelViewSet): """ queryset = ( - Club.objects.all() - .annotate( - favorite_count=Count("favorite", distinct=True), - membership_count=Count("membership", distinct=True, filter=Q(active=True)), - ) - .prefetch_related("tags") - .order_by("-favorite_count", "name") + Club.objects.all().prefetch_related("tags").order_by("-favorite_count", "name") ) permission_classes = [ClubPermission | IsSuperuser] filter_backends = [filters.SearchFilter, ClubsSearchFilter, ClubsOrderingFilter] @@ -1152,6 +1157,8 @@ def upload(self, request, *args, **kwargs): """ # ensure user is allowed to upload image club = self.get_object() + key = f"clubs:{club.id}" + cache.delete(key) # reset approval status after upload resp = upload_endpoint_helper(request, club, "file", "image", save=False) @@ -1938,20 +1945,54 @@ def check_approval_permission(self, request): "or deregistering for the SAC fair." ) - def partial_update(self, request, *args, **kwargs): - self.check_approval_permission(request) - return super().partial_update(request, *args, **kwargs) + def list(self, *args, **kwargs): + """ + Return a list of all clubs. Responses cached for 1 hour + + Responses are only cached for people with specific permissions + """ + key = self.request.build_absolute_uri() + cached_object = cache.get(key) + if ( + cached_object + and not self.request.user.groups.filter(name="Approvers").exists() + ): + return Response(cached_object) + + resp = super().list(*args, **kwargs) + cache.set(key, resp.data, 60 * 60) + return resp + + def retrieve(self, *args, **kwargs): + """ + Retrieve data about a specific club. Responses cached for 1 hour + """ + key = f"clubs:{self.get_object().id}" + cached = cache.get(key) + if cached: + return Response(cached) + + resp = super().retrieve(*args, **kwargs) + cache.set(key, resp.data, 60 * 60) + return resp def update(self, request, *args, **kwargs): + """ + Invalidate caches + """ self.check_approval_permission(request) + key = f"clubs:{self.get_object().id}" + cache.delete(key) return super().update(request, *args, **kwargs) - def list(self, request, *args, **kwargs): + def partial_update(self, request, *args, **kwargs): """ - Return a list of all clubs. - Note that some fields are removed in order to improve response time. + Invalidate caches """ - return super().list(request, *args, **kwargs) + self.check_approval_permission(request) + key = f"clubs:{self.get_object().id}" + cache.delete(key) + return super().partial_update(request, *args, **kwargs) def perform_destroy(self, instance): """ @@ -2044,7 +2085,10 @@ def get_serializer_class(self): self.request.accepted_renderer.format == "xlsx" or self.action == "fields" ): - if self.request.user.has_perm("clubs.generate_reports"): + if ( + self.request.user.has_perm("clubs.generate_reports") + or self.request.user.is_superuser + ): return ReportClubSerializer else: return ClubSerializer @@ -3945,9 +3989,8 @@ def post(self, request): meeting_id = ( request.data.get("payload", {}).get("object", {}).get("id", None) ) - regex = ( - rf"https?:\/\/([A-z]*.)?zoom.us/[^\/]*\/{meeting_id}(\?pwd=[A-z,0-9]*)?" - ) + regex = rf"""https?:\/\/([A-z]*\.)?zoom\.us/[^\/]*\/ + {meeting_id}(\?pwd=[A-z,0-9]*)?""" event = Event.objects.filter(url__regex=regex).first() participant_id = ( @@ -4888,12 +4931,24 @@ def question_response(self, *args, **kwargs): user=self.request.user, committee__isnull=False, application=application, - archived=False, ) .values_list("committee__name", flat=True) .distinct() ) + # prevent submissions outside of the open duration + now = timezone.now() + extension = application.extensions.filter(user=self.request.user).first() + end_time = ( + max(extension.end_time, application.application_end_time) + if extension + else application.application_end_time + ) + if now > end_time or now < application.application_start_time: + return Response( + {"success": False, "detail": "This application is not currently open!"} + ) + # limit applicants to 2 committees if ( committee @@ -4909,9 +4964,13 @@ def question_response(self, *args, **kwargs): submissions page""", } ) - submission = ApplicationSubmission.objects.create( + submission, _ = ApplicationSubmission.objects.get_or_create( user=self.request.user, application=application, committee=committee, ) + + key = f"applicationsubmissions:{application.id}" + cache.delete(key) + for question_pk in questions: question = ApplicationQuestion.objects.filter(pk=question_pk).first() question_type = question.question_type @@ -4930,9 +4989,11 @@ def question_response(self, *args, **kwargs): ): text = question_data.get("text", None) if text is not None and text != "": - obj = ApplicationQuestionResponse.objects.create( - text=text, question=question, submission=submission, - ).save() + obj, _ = ApplicationQuestionResponse.objects.update_or_create( + question=question, + submission=submission, + defaults={"text": text}, + ) response = Response(ApplicationQuestionResponseSerializer(obj).data) elif question_type == ApplicationQuestion.MULTIPLE_CHOICE: multiple_choice_value = question_data.get("multipleChoice", None) @@ -4940,13 +5001,12 @@ def question_response(self, *args, **kwargs): multiple_choice_obj = ApplicationMultipleChoice.objects.filter( question=question, value=multiple_choice_value ).first() - obj = ApplicationQuestionResponse.objects.create( - multiple_choice=multiple_choice_obj, + obj, _ = ApplicationQuestionResponse.objects.update_or_create( question=question, submission=submission, - ).save() + defaults={"multiple_choice": multiple_choice_obj}, + ) response = Response(ApplicationQuestionResponseSerializer(obj).data) - submission.save() return response @action(detail=False, methods=["get"]) @@ -5014,10 +5074,10 @@ def questions(self, *args, **kwargs): response = ( ApplicationQuestionResponse.objects.filter( - submission__user=self.request.user + question=question, submission__user=self.request.user, ) - .filter(question__prompt=question.prompt) - .order_by("-updated_at") + .select_related("submission", "multiple_choice", "question") + .prefetch_related("question__committees", "question__multiple_choice") .first() ) @@ -5037,12 +5097,199 @@ class ClubApplicationViewSet(viewsets.ModelViewSet): create: Create an application for the club. list: Retrieve a list of applications of the club. + + retrieve: Retrieve information about a single application + + current: Retrieve a list of active applications of the club. + + send_emails: Send out acceptance/rejection emails """ permission_classes = [ClubItemPermission | IsSuperuser] serializer_class = ClubApplicationSerializer http_method_names = ["get", "post", "put", "patch", "delete"] + def destroy(self, *args, **kwargs): + """ + Invalidate cache before deleting + """ + app = self.get_object() + key = f"clubapplication:{app.id}" + cache.delete(key) + return super().destroy(*args, **kwargs) + + def update(self, *args, **kwargs): + """ + Invalidate cache before updating + """ + app = self.get_object() + key = f"clubapplication:{app.id}" + cache.delete(key) + return super().update(*args, **kwargs) + + def retrieve(self, *args, **kwargs): + """ + Cache responses for one hour. This is what people + see when viewing an individual club's application + """ + + pk = self.kwargs["pk"] + key = f"clubapplication:{pk}" + cached = cache.get(key) + if cached: + return Response(cached) + app = self.get_object() + data = ClubApplicationSerializer(app).data + cache.set(key, data, 60 * 60) + return Response(data) + + @action(detail=True, methods=["post"]) + def send_emails(self, *args, **kwargs): + """ + Send out acceptance/rejection emails for a particular application + + Dry run will validate that all emails have nonempty variables + + Allow resend will renotify submissions that have already been emailed + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + allow_resend: + type: boolean + dry_run: + type: boolean + email_type: + type: object + properties: + id: + type: string + name: + type: string + responses: + "200": + content: + application/json: + schema: + type: object + properties: + detail: + type: string + + --- + + """ + + app = self.get_object() + + # Query for recent submissions with user and committee joined + submissions = ApplicationSubmission.objects.filter( + application=app + ).select_related("user", "committee") + + dry_run = self.request.data.get("dry_run") + + if not dry_run: + # Invalidate submission viewset cache + key = f"applicationsubmissions:{app.id}" + cache.delete(key) + + email_type = self.request.data.get("email_type")["id"] + + subject = f"Application Update for {app.name}" + n, skip = 0, 0 + + allow_resend = self.request.data.get("allow_resend") + + acceptance_template = Template(app.acceptance_email) + rejection_template = Template(app.rejection_email) + + mass_emails = [] + for submission in submissions: + if ( + (not allow_resend and submission.notified) + or submission.status == ApplicationSubmission.PENDING + or not (submission.reason and submission.user.email) + ): + skip += 1 + continue + elif ( + submission.status == ApplicationSubmission.ACCEPTED + and email_type == "acceptance" + ): + template = acceptance_template + elif ( + email_type == "rejection" + and submission.status != ApplicationSubmission.ACCEPTED + ): + template = rejection_template + else: + continue + + data = { + "reason": submission.reason, + "name": submission.user.first_name or "", + "committee": submission.committee.name if submission.committee else "", + } + + html_content = template.render(data) + text_content = html_to_text(html_content) + + contact_email = app.club.email + + msg = EmailMultiAlternatives( + subject, + text_content, + settings.FROM_EMAIL, + [submission.user.email], + reply_to=[contact_email], + ) + msg.attach_alternative(html_content, "text/html") + mass_emails.append(msg) + + if not dry_run: + submission.notified = True + n += 1 + + if not dry_run: + with mail.get_connection() as conn: + conn.send_messages(mass_emails) + ApplicationSubmission.objects.bulk_update(submissions, ["notified"]) + + dry_run_msg = "Would have sent" if dry_run else "Sent" + return Response( + { + "detail": f"{dry_run_msg} emails to {n} people, " + f"skipping {skip} due to one of (already notified, no reason, no email)" + } + ) + + @action(detail=False, methods=["get"]) + def current(self, *args, **kwargs): + """ + Return the ongoing application(s) for this club + --- + responses: + "200": + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/ClubApplication" + --- + """ + qs = self.get_queryset().prefetch_related("extensions") + now = timezone.now() + user = self.request.user + q = Q(application_end_time__gte=now) + if user.is_authenticated: + q |= Q(extensions__end_time__gte=now, extensions__user=user) + + return Response(ClubApplicationSerializer(qs.filter(q), many=True).data) + @action(detail=True, methods=["post"]) def duplicate(self, *args, **kwargs): """ @@ -5061,6 +5308,7 @@ def duplicate(self, *args, **kwargs): now = timezone.now() clone.application_start_time = now + datetime.timedelta(days=1) clone.application_end_time = now + datetime.timedelta(days=30) + clone.result_release_time = now + datetime.timedelta(days=40) clone.external_url = ( f"https://pennclubs.com/club/{clone.club.code}/" f"application/{clone.pk}" ) @@ -5069,16 +5317,284 @@ def duplicate(self, *args, **kwargs): def get_serializer_class(self): if self.action in {"create", "update", "partial_update"}: + if "club_code" in self.kwargs: + club = ( + Club.objects.filter(code=self.kwargs["club_code"]) + .prefetch_related("badges") + .first() + ) + if club and club.is_wharton: + return ManagedClubApplicationSerializer return WritableClubApplicationSerializer return ClubApplicationSerializer def get_queryset(self): - return ClubApplication.objects.filter(club__code=self.kwargs["club_code"]) + return ( + ClubApplication.objects.filter(club__code=self.kwargs["club_code"],) + .select_related("application_cycle", "club") + .prefetch_related( + "questions__multiple_choice", "questions__committees", "committees", + ) + ) + + +class WhartonCyclesView(viewsets.ModelViewSet): + """ + get: Return information about all Wharton Council application cycles + patch: Update application cycle and WC applications with cycle + clubs: list clubs with cycle + add_clubs: add clubs to cycle + remove_clubs_from_all: remove clubs from all cycles + """ + + permission_classes = [WhartonApplicationPermission | IsSuperuser] + # Designed to support partial updates, but ModelForm sends all fields here + http_method_names = ["get", "post", "patch", "delete"] + serializer_class = ApplicationCycleSerializer + + def get_queryset(self): + return ApplicationCycle.objects.all().order_by("end_date") + + def update(self, *args, **kwargs): + """ + Updates times for all applications with cycle + """ + applications = ClubApplication.objects.filter( + application_cycle=self.get_object() + ) + str_start_date = self.request.data.get("start_date").replace("T", " ") + str_end_date = self.request.data.get("end_date").replace("T", " ") + str_release_date = self.request.data.get("release_date").replace("T", " ") + time_format = "%Y-%m-%d %H:%M:%S%z" + start = ( + datetime.datetime.strptime(str_start_date, time_format) + if str_start_date + else self.get_object().start_date + ) + end = ( + datetime.datetime.strptime(str_end_date, time_format) + if str_end_date + else self.get_object().end_date + ) + release = ( + datetime.datetime.strptime(str_release_date, time_format) + if str_release_date + else self.get_object().release_date + ) + for app in applications: + app.application_start_time = start + if app.application_end_time_exception: + continue + app.application_end_time = end + app.result_release_time = release + f = ["application_start_time", "application_end_time", "result_release_time"] + ClubApplication.objects.bulk_update(applications, f) + return super().update(*args, **kwargs) + + @action(detail=True, methods=["GET"]) + def get_clubs(self, *args, **kwargs): + """ + Retrieve clubs associated with given cycle + --- + requestBody: + content: {} + responses: + "200": + content: {} + --- + """ + cycle = self.get_object() + + return Response( + ClubApplication.objects.filter(application_cycle=cycle) + .select_related("club") + .values("club__name", "club__code") + ) + + @action(detail=True, methods=["PATCH"]) + def edit_clubs(self, *args, **kwargs): + """ + Edit clubs associated with given cycle + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + clubs: + type: array + items: + type: string + responses: + "200": + content: {} + --- + + """ + cycle = self.get_object() + club_codes = self.request.data.get("clubs") + start = cycle.start_date + end = cycle.end_date + release = cycle.release_date + + # Some apps get deleted + ClubApplication.objects.filter(application_cycle=cycle).exclude( + club__code__in=club_codes + ).delete() + + # Some apps need to be created - use the default Wharton Template + prompt_one = ( + "Tell us about a time you took " "initiative or demonstrated leadership" + ) + prompt_two = "Tell us about a time you faced a challenge and how you solved it" + prompt_three = "Tell us about a time you collaborated well in a team" + created_apps_clubs = ( + ClubApplication.objects.filter( + application_cycle=cycle, club__code__in=club_codes + ) + .select_related("club") + .values_list("club__code", flat=True) + ) + creation_pending_clubs = Club.objects.filter( + code__in=set(club_codes) - set(created_apps_clubs) + ) + + for club in creation_pending_clubs: + name = f"{club.name} Application" + most_recent = ( + ClubApplication.objects.filter(club=club) + .order_by("-created_at") + .first() + ) + + if most_recent: + # If an application for this club exists, clone it + application = most_recent.make_clone() + application.application_start_time = start + application.application_end_time = end + application.result_release_time = release + application.application_cycle = cycle + application.is_wharton_council = True + application.external_url = ( + f"https://pennclubs.com/club/{club.code}/" + f"application/{application.pk}" + ) + application.save() + else: + # Otherwise, start afresh + application = ClubApplication.objects.create( + name=name, + club=club, + application_start_time=start, + application_end_time=end, + result_release_time=release, + application_cycle=cycle, + is_wharton_council=True, + ) + external_url = ( + f"https://pennclubs.com/club/{club.code}/" + f"application/{application.pk}" + ) + application.external_url = external_url + application.save() + prompt = ( + "Choose one of the following prompts for your personal statement" + ) + prompt_question = ApplicationQuestion.objects.create( + question_type=ApplicationQuestion.MULTIPLE_CHOICE, + application=application, + prompt=prompt, + ) + ApplicationMultipleChoice.objects.create( + value=prompt_one, question=prompt_question + ) + ApplicationMultipleChoice.objects.create( + value=prompt_two, question=prompt_question + ) + ApplicationMultipleChoice.objects.create( + value=prompt_three, question=prompt_question + ) + ApplicationQuestion.objects.create( + question_type=ApplicationQuestion.FREE_RESPONSE, + prompt="Answer the prompt you selected", + word_limit=150, + application=application, + ) + return Response([]) -class WhartonApplicationAPIView(generics.ListAPIView): + @action(detail=False, methods=["post"]) + def add_clubs_to_exception(self, *args, **kwargs): + """ + Exempt selected clubs from application cycle deadline + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + clubs: + type: array + items: + type: object + properties: + id: + type: integer + application_end_time: + type: string + responses: + "200": + content: {} + --- + """ + clubs = self.request.data.get("clubs") + apps = [] + for club in clubs: + app = ClubApplication.objects.get(pk=club["id"]) + apps.append(app) + app.application_end_time = club["end_date"] + app.application_end_time_exception = True + ClubApplication.objects.bulk_update( + apps, ["application_end_time", "application_end_time_exception"], + ) + return Response([]) + + @action(detail=False, methods=["post"]) + def remove_clubs_from_exception(self, *args, **kwargs): + """ + Remove selected clubs from application cycle deadline exemption + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + clubs: + type: array + items: + type: string + responses: + "200": + content: {} + --- + """ + club_ids = self.request.data.get("clubs", []) + apps = ClubApplication.objects.filter(pk__in=club_ids) + for app in apps: + app.application_end_time_exception = False + app.application_end_time = app.application_cycle.end_date + ClubApplication.objects.bulk_update( + apps, ["application_end_time", "application_end_time_exception"], + ) + return Response([]) + + +class WhartonApplicationAPIView(viewsets.ModelViewSet): """ - get: Return information about all Wharton Council club applications which are + list: Return information about all Wharton Council club applications which are currently on going """ @@ -5086,31 +5602,38 @@ class WhartonApplicationAPIView(generics.ListAPIView): serializer_class = ClubApplicationSerializer def get_operation_id(self, **kwargs): - return "List Wharton applications and details" + return f"{kwargs['operId']} Wharton Application" def get_queryset(self): now = timezone.now() - qs = ClubApplication.objects.filter( - is_wharton_council=True, application_end_time__gte=now + qs = ( + ClubApplication.objects.filter( + is_wharton_council=True, + application_start_time__lte=now, + application_end_time__gte=now, + ) + .select_related("club") + .prefetch_related( + "committees", "questions__multiple_choice", "questions__committees" + ) ) - # randomly order Wharton Council applications by user - + # Order applications randomly for viewing (consistent and unique per user). key = str(self.request.user.id) - - cached_qs = cache.get(key) - - if cached_qs and qs.count() == cached_qs.count(): - return cached_qs - qs = qs.annotate( - random_id=SHA1(Concat("club", Value(key)), output_field=TextField()) - ).order_by("random_id") - cache.set(key, qs, 60) - + random=SHA1(Concat("name", Value(key), output_field=TextField())) + ).order_by("random") return qs + @method_decorator(cache_page(60 * 20)) + @method_decorator(vary_on_cookie) + def list(self, *args, **kwargs): + """ + Cache responses for 20 minutes. Vary cache by user. + """ + return super().list(*args, **kwargs) + class WhartonApplicationStatusAPIView(generics.ListAPIView): """ @@ -5125,23 +5648,7 @@ def get_operation_id(self, **kwargs): def get_queryset(self): return ( - ApplicationSubmission.objects.filter( - application__is_wharton_council=True, - created_at__in=RawSQL( - """SELECT recent_time - FROM - (SELECT user_id, - committee_id, - application_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE NOT archived - GROUP BY user_id, - committee_id, application_id) recent_subs""", - (), - ), - archived=False, - ) + ApplicationSubmission.objects.filter(application__is_wharton_council=True) .annotate( annotated_name=F("application__name"), annotated_committee=F("committee__name"), @@ -5158,7 +5665,17 @@ def get_queryset(self): ) -class ApplicationSubmissionViewSet(XLSXFormatterMixin, viewsets.ModelViewSet): +class ApplicationExtensionViewSet(viewsets.ModelViewSet): + permission_classes = [ClubSensitiveItemPermission | IsSuperuser] + serializer_class = ApplicationExtensionSerializer + + def get_queryset(self): + return ApplicationExtension.objects.filter( + application__pk=self.kwargs["application_pk"] + ) + + +class ApplicationSubmissionViewSet(viewsets.ModelViewSet): """ list: List submissions for a given club application. @@ -5171,35 +5688,50 @@ class ApplicationSubmissionViewSet(XLSXFormatterMixin, viewsets.ModelViewSet): http_method_names = ["get", "post"] def get_queryset(self): - # Use a raw SQL query to obtain the most recent (user, committee) pairs - # of application submissions for a specific application. - # Done by grouping by (user_id, commitee_id) and returning the most - # recent instance in each group, then selecting those instances + app_id = self.kwargs["application_pk"] + submissions = ( + ApplicationSubmission.objects.filter(application=app_id) + .select_related("user__profile", "committee", "application__club") + .prefetch_related( + Prefetch( + "responses", + queryset=ApplicationQuestionResponse.objects.select_related( + "multiple_choice", "question" + ), + ), + "responses__question__committees", + "responses__question__multiple_choice", + ) + ) + return submissions + + def list(self, *args, **kwargs): + """ + Manually cache responses (to support invalidation) + Responses are invalidated on status / reason updates and email sending + """ app_id = self.kwargs["application_pk"] - query = f""" - SELECT * - FROM clubs_applicationsubmission - WHERE application_id = {app_id} - AND NOT archived - AND created_at in - (SELECT recent_time - FROM - (SELECT user_id, - committee_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE application_id = {app_id} - AND NOT archived - GROUP BY user_id, - committee_id) recent_subs) - """ - return ApplicationSubmission.objects.raw(query) + key = f"applicationsubmissions:{app_id}" + cached = cache.get(key) + if cached is not None: + return Response(cached) + else: + serializer = self.get_serializer_class() + qs = self.get_queryset() + data = serializer(qs, many=True).data + cache.set(key, data, 60 * 60) + + return Response(data) + + @method_decorator(cache_page(60 * 60 * 2)) @action(detail=False, methods=["get"]) def export(self, *args, **kwargs): """ - Given some application submissions, export them + Given some application submissions, export them to CSV. + + Cached for 2 hours. --- requestBody: content: @@ -5213,6 +5745,43 @@ def export(self, *args, **kwargs): type: integer status: type: integer + responses: + "200": + content: + text/csv: + schema: + type: string + --- + """ + app_id = int(self.kwargs["application_pk"]) + data = ( + ApplicationSubmission.objects.filter(application=app_id) + .select_related("user__profile", "committee", "application__club") + .prefetch_related( + Prefetch( + "responses", + queryset=ApplicationQuestionResponse.objects.select_related( + "multiple_choice", "question" + ), + ), + "responses__question__committees", + "responses__question__multiple_choice", + ) + ) + df = pd.DataFrame(ApplicationSubmissionCSVSerializer(data, many=True).data) + resp = HttpResponse( + content_type="text/csv", + headers={"Content-Disposition": "attachment;filename=submissions.csv"}, + ) + df.to_csv(index=True, path_or_buf=resp) + return resp + + @action(detail=False, methods=["get"]) + def exportall(self, *args, **kwargs): + """ + Export all application submissions for a particular cycle + --- + requestBody: {} responses: "200": content: @@ -5222,27 +5791,17 @@ def export(self, *args, **kwargs): properties: output: type: string - --- """ + + app_id = int(self.kwargs["application_pk"]) + cycle = ClubApplication.objects.get(id=app_id).application_cycle data = ( ApplicationSubmission.objects.filter( application__is_wharton_council=True, - created_at__in=RawSQL( - """SELECT recent_time - FROM - (SELECT user_id, - committee_id, - application_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE NOT archived - GROUP BY user_id, - committee_id, application_id) recent_subs""", - (), - ), - archived=False, + application__application_cycle=cycle, ) + .select_related("application", "application__application_cycle") .annotate( annotated_name=F("application__name"), annotated_committee=F("committee__name"), @@ -5284,7 +5843,7 @@ def status(self, *args, **kwargs): schema: type: object properties: - output: + detail: type: string --- @@ -5295,10 +5854,83 @@ def status(self, *args, **kwargs): status in map(lambda x: x[0], ApplicationSubmission.STATUS_TYPES) and len(submission_pks) > 0 ): - ApplicationSubmission.objects.filter(pk__in=submission_pks).update( - status=status + # Invalidate submission viewset cache + submissions = ApplicationSubmission.objects.filter(pk__in=submission_pks) + app_id = submissions.first().application.id if submissions.first() else None + if not app_id: + return Response({"detail": "No submissions found"}) + key = f"applicationsubmissions:{app_id}" + cache.delete(key) + + submissions.update(status=status) + + return Response( + { + "detail": f"Successfully updated submissions' {submission_pks}" + f"status {status}" + } ) - return Response([]) + else: + return Response({"detail": "Invalid request"}) + + @action(detail=False, methods=["post"]) + def reason(self, *args, **kwargs): + """ + Given some application submissions, update their acceptance/rejection + reasons + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + submissions: + type: array + items: + type: object + properties: + id: + type: integer + reason: + type: string + + responses: + "200": + content: + application/json: + schema: + type: object + properties: + detail: + type: string + + --- + """ + submissions = self.request.data.get("submissions", []) + pks = list(map(lambda x: x["id"], submissions)) + reasons = list(map(lambda x: x["reason"], submissions)) + + submission_objs = ApplicationSubmission.objects.filter(pk__in=pks) + + # Invalidate submission viewset cache + app_id = ( + submission_objs.first().application.id if submission_objs.first() else None + ) + if not app_id: + return Response({"detail": "No submissions found"}) + key = f"applicationsubmissions:{app_id}" + cache.delete(key) + + for idx, pk in enumerate(pks): + obj = submission_objs.filter(pk=pk).first() + if obj: + obj.reason = reasons[idx] + obj.save() + else: + return Response({"detail": "Object not found"}) + + return Response({"detail": "Successfully updated submissions' reasons"}) def get_serializer_class(self): if self.request and self.request.query_params.get("format") == "xlsx": @@ -5319,41 +5951,26 @@ class ApplicationSubmissionUserViewSet(viewsets.ModelViewSet): http_method_names = ["get", "delete"] def get_queryset(self): - distinct_submissions = {} - submissions = ApplicationSubmission.objects.filter( - user=self.request.user, archived=False + submissions = ( + ApplicationSubmission.objects.filter(user=self.request.user) + .select_related("user__profile", "committee", "application__club") + .prefetch_related( + Prefetch( + "responses", + queryset=ApplicationQuestionResponse.objects.select_related( + "multiple_choice", "question" + ), + ), + "responses__question__committees", + "responses__question__multiple_choice", + ) ) - - # only want to return the most recent (user, committee) unique submission pair - for submission in submissions: - key = (submission.application.__str__(), submission.committee.__str__()) - if key in distinct_submissions: - if distinct_submissions[key].created_at < submission.created_at: - distinct_submissions[key] = submission - else: - distinct_submissions[key] = submission - - queryset = ApplicationSubmission.objects.none() - for submission in distinct_submissions.values(): - queryset |= ApplicationSubmission.objects.filter(pk=submission.pk) - - return queryset - - def perform_destroy(self, instance): - """ - Set archived boolean to be True so that the submissions - appears to have been deleted - """ - - instance.archived = True - instance.archived_by = self.request.user - instance.archived_on = timezone.now() - instance.save() + return submissions class ApplicationQuestionViewSet(viewsets.ModelViewSet): """ - create: Create a questions for a club application. + create: Create a question for a club application. list: List questions in a given club application. """ @@ -5367,6 +5984,54 @@ def get_queryset(self): application__pk=self.kwargs["application_pk"] ).order_by("precedence") + def destroy(self, *args, **kwargs): + """ + Invalidate caches before destroying + """ + app_id = self.kwargs["application_pk"] + key1 = f"applicationquestion:{app_id}" + key2 = f"clubapplication:{app_id}" + cache.delete(key1) + cache.delete(key2) + return super().destroy(*args, **kwargs) + + def create(self, *args, **kwargs): + """ + Invalidate caches before creating + """ + app_id = self.kwargs["application_pk"] + key1 = f"applicationquestion:{app_id}" + key2 = f"clubapplication:{app_id}" + cache.delete(key1) + cache.delete(key2) + return super().create(*args, **kwargs) + + def update(self, *args, **kwargs): + """ + Invalidate caches before updating + """ + app_id = self.kwargs["application_pk"] + key1 = f"applicationquestion:{app_id}" + key2 = f"clubapplication:{app_id}" + cache.delete(key1) + cache.delete(key2) + return super().update(*args, **kwargs) + + def list(self, *args, **kwargs): + """ + Manually cache responses for one hour + """ + + app_id = self.kwargs["application_pk"] + key = f"applicationquestion:{app_id}" + cached = cache.get(key) + if cached: + return Response(cached) + + data = ApplicationQuestionSerializer(self.get_queryset(), many=True).data + cache.set(key, data, 60 * 60) + return Response(data) + @action(detail=False, methods=["post"]) def precedence(self, *args, **kwargs): """ diff --git a/backend/pennclubs/settings/base.py b/backend/pennclubs/settings/base.py index 96ae167b3..e5b379dc5 100644 --- a/backend/pennclubs/settings/base.py +++ b/backend/pennclubs/settings/base.py @@ -160,7 +160,7 @@ "DEFAULT_RENDERER_CLASSES": ( "rest_framework.renderers.JSONRenderer", "rest_framework.renderers.BrowsableAPIRenderer", - "drf_renderer_xlsx.renderers.XLSXRenderer", + "drf_excel.renderers.XLSXRenderer", ), "DEFAULT_SCHEMA_CLASS": "pennclubs.doc_settings.CustomAutoSchema", "DEFAULT_AUTHENTICATION_CLASSES": [ diff --git a/backend/pennclubs/settings/production.py b/backend/pennclubs/settings/production.py index 6ae09c1c0..dbd81fd24 100644 --- a/backend/pennclubs/settings/production.py +++ b/backend/pennclubs/settings/production.py @@ -61,7 +61,12 @@ "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": f"redis://{REDIS_HOST}:6379/1", - "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + "IGNORE_EXCEPTIONS": True, # ignore Redis connection errors + "SOCKET_CONNECT_TIMEOUT": 1, + "SOCKET_TIMEOUT": 1, + }, "KEY_PREFIX": "django", } } diff --git a/backend/templates/emails/application_extension.html b/backend/templates/emails/application_extension.html new file mode 100644 index 000000000..f3aeee637 --- /dev/null +++ b/backend/templates/emails/application_extension.html @@ -0,0 +1,22 @@ + +{% extends 'emails/base.html' %} + +{% block content %} +

Application Extension for {{ application_name }}

+

Hello {{ name }},

+

You have been granted an extension for {{ application_name }} by the officers of {{ club }} on Penn Clubs:

+

The updated deadline to submit your application is {{ end_time }}. You can apply using the button below.

+ Apply +{% endblock %} diff --git a/backend/templates/emails/renew.html b/backend/templates/emails/renew.html index b0dbd3c31..38d145f64 100644 --- a/backend/templates/emails/renew.html +++ b/backend/templates/emails/renew.html @@ -12,7 +12,7 @@

Renew {{ name }} on Penn Clubs & SAC Fair Signup

Dear {{ name }} officers,

- We have opened the student group registration process for the 2020-2021 academic year. To be + We have opened the student group registration process for the current academic year. To be considered an active student group on campus, the University requires all student organizations to be registered with the Office of Student Affairs. This year, OSA has migrated the Student Groups platform from the previous system, G.O. Penn, to Penn Labs' platform, Penn Clubs. diff --git a/backend/templates/emails/renewal_reminder.html b/backend/templates/emails/renewal_reminder.html index a701bc094..fe337b736 100644 --- a/backend/templates/emails/renewal_reminder.html +++ b/backend/templates/emails/renewal_reminder.html @@ -43,14 +43,14 @@

Reminder to Renew {{ name }} on Penn Clubs & SAC Fair Signup by 8/24!

If you are logged in and do not see any clubs, reply to this email with your PennKey and club name to gain - access to re-register your club for the 2020-2021 school year. + access to re-register your club for the upcoming school year.

Follow the 5 steps listed to update your club's information. This would be the process for your organization to gain official approval from the Office of Student Affairs. The final page of the renewal process will ask whether or - not your club is interested in participating in the Fall 2020 SAC Fair. To sign up, please simply check the box on + not your club is interested in participating in the semesterly SAC Fair. To sign up, please simply check the box on this page, and your information will be sent to the SAC Fair Coordinator.

@@ -61,4 +61,4 @@

Reminder to Renew {{ name }} on Penn Clubs & SAC Fair Signup by 8/24! If you have any questions or problems regarding re-registration, please reply to this email. If you have questions specifically regarding the SAC Fair, please send an email to sacfair@sacfunded.net.

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/backend/tests/clubs/test_views.py b/backend/tests/clubs/test_views.py index 765e8a714..7646cc6ec 100644 --- a/backend/tests/clubs/test_views.py +++ b/backend/tests/clubs/test_views.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core import mail +from django.core.cache import cache from django.core.management import call_command from django.test import Client, TestCase from django.urls import reverse @@ -1086,6 +1087,7 @@ def test_club_create_description_sanitize_good(self): }, content_type="application/json", ) + cache.clear() self.assertIn(resp.status_code, [200, 201], resp.content) resp = self.client.get(reverse("clubs-detail", args=("penn-labs",))) @@ -2529,71 +2531,3 @@ def test_event_add_meeting(self): self.event1.refresh_from_db() self.assertIn("url", resp.data, resp.content) self.assertTrue(self.event1.url, resp.content) - - def test_zoom_webhook(self): - """ - Test that the Zoom webhook can properly parse a request sent by the Zoom API. - """ - person = self.user1 - event = self.event1 - meeting_id = "4880003126" - participant_id = "jswPTE5-StSc-kbAX6n2Rw" - join_time = "2021-01-10T20:38:49Z" - leave_time = "2021-01-10T20:42:23Z" - - req = { - "event": "meeting.participant_joined", - "payload": { - "object": { - "participant": { - "user_id": participant_id, - "join_time": join_time, - "email": person.email, - }, - "id": meeting_id, - } - }, - } - - resp = self.client.post( - reverse("webhooks-meeting"), req, content_type="application/json" - ) - self.assertIn(resp.status_code, [200, 201], resp.content) - self.assertTrue( - ZoomMeetingVisit.objects.filter( - person=person, - event=event, - meeting_id=meeting_id, - participant_id=participant_id, - join_time=join_time, - ) - ) - - req = { - "event": "meeting.participant_left", - "payload": { - "object": { - "participant": { - "user_id": participant_id, - "leave_time": leave_time, - "email": person.email, - }, - "id": meeting_id, - } - }, - } - - resp = self.client.post( - reverse("webhooks-meeting"), req, content_type="application/json" - ) - self.assertIn(resp.status_code, [200, 201], resp.content) - self.assertTrue( - ZoomMeetingVisit.objects.filter( - person=person, - event=event, - meeting_id=meeting_id, - participant_id=participant_id, - join_time=join_time, - leave_time=leave_time, - ) - ) diff --git a/frontend/components/Applications.tsx b/frontend/components/Applications.tsx index a5099030b..0bc7a4b39 100644 --- a/frontend/components/Applications.tsx +++ b/frontend/components/Applications.tsx @@ -1,25 +1,84 @@ import { NextPageContext } from 'next' import Link from 'next/link' import React, { ReactElement } from 'react' +import LazyLoad from 'react-lazy-load' import styled from 'styled-components' import { Application } from 'types' import { doBulkLookup } from 'utils' -import { Card, Text } from '~/components/common' +import { Text } from '~/components/common' import { ClubName } from '~/components/EventPage/common' import DateInterval from '~/components/EventPage/DateInterval' -import { mediaMaxWidth, mediaMinWidth, PHONE, WHITE } from '~/constants' +import { + ALLBIRDS_GRAY, + ANIMATION_DURATION, + BORDER_RADIUS, + CLUBS_GREY_LIGHT, + HOVER_GRAY, + mediaMaxWidth, + SM, + WHITE, +} from '~/constants' -const ApplicationCardContainer = styled.div` - ${mediaMinWidth(PHONE)} { - max-width: 18em; - margin: 1rem; +const CardWrapper = styled.div` + ${mediaMaxWidth(SM)} { + padding-top: 0; + padding-bottom: 1rem; } - ${mediaMaxWidth(PHONE)} { - margin: 1rem 0; +` + +const DescriptionWrapper = styled.p` + margin-top: 0.2rem; + color: ${CLUBS_GREY_LIGHT}; + border-top: 1.5px solid rgba(0, 0, 0, 0.05); + width: 100%; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; + overflow: hidden; +` + +const Description = (text) => ( +
+ {text.length > 255 ? ( +
{`${text.substring(0, 255)} ...`}
+ ) : ( +

{text}

+ )} +
+) + +const MainInfo = styled.div` + display: flex; + flex-direction: row; +` +type CardProps = { + readonly hovering?: boolean + className?: string +} + +const Card = styled.div` + padding: 10px; + box-shadow: 0 0 0 transparent; + transition: all ${ANIMATION_DURATION}ms ease; + border-radius: ${BORDER_RADIUS}; + box-shadow: 0 0 0 ${WHITE}; + background-color: ${({ hovering }) => (hovering ? HOVER_GRAY : WHITE)}; + border: 1px solid ${ALLBIRDS_GRAY}; + justify-content: space-between; + height: auto; + cursor: pointer; + + &:hover, + &:active, + &:focus { + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2); + } + + ${mediaMaxWidth(SM)} { + width: calc(100%); + padding: 8px; } - float: left; - cursor: pointer; !important ` const Image = styled.img` @@ -29,35 +88,60 @@ const Image = styled.img` overflow: hidden; ` +const AppsContainer = styled.div` + display: flex; + flex-direction: row; + min-height: 60vh; +` + function ApplicationsPage({ whartonapplications }): ReactElement { if ('detail' in whartonapplications) { return {whartonapplications.detail} } return ( - <> - {whartonapplications != null && whartonapplications.length > 0 ? ( - whartonapplications.map((application) => ( - - - - {application.club_image_url != null && - application.club_image_url !== '' && ( - - )} - - {application.name} - - - - )) - ) : ( - No applications are currently available. - )} - + +
+ {whartonapplications != null && whartonapplications.length > 0 ? ( + whartonapplications.map((application) => ( + + + + + +
+ {application.name} + +
+
+ {application.club_image_url != null && + application.club_image_url !== '' && ( + + + + )} +
+
+ {application.description && application.description.length && ( + + )} +
+
+ +
+ )) + ) : ( + No applications are currently available. + )} +
+
) } diff --git a/frontend/components/ClubEditPage/ApplicationsCard.tsx b/frontend/components/ClubEditPage/ApplicationsCard.tsx index 587d733be..1702c39a5 100644 --- a/frontend/components/ClubEditPage/ApplicationsCard.tsx +++ b/frontend/components/ClubEditPage/ApplicationsCard.tsx @@ -11,6 +11,7 @@ import { } from '../../utils/branding' import { Icon, Modal, Text } from '../common' import { + ApplicationUpdateTextField, CheckboxField, CreatableMultipleSelectField, DateTimeField, @@ -262,14 +263,23 @@ export default function ApplicationsCard({ club }: Props): ReactElement { ))} + {!club.is_wharton && ( + + TIP: To copy over your application from last semester, please + click duplicate on the application from the season that you + would like to copy over and refresh the page. You can then edit this + application as you please. + + )} + - TIP: To copy over your application from last semester, please - click duplicate on the application from the season that you - would like to copy over and refresh the page. You can then edit this - application as you please. + If your club is affiliated with the Wharton Council Centralised + Application, please note that editable applications will be provisioned + by the system administrator.{' '} { @@ -316,19 +326,25 @@ export default function ApplicationsCard({ club }: Props): ReactElement { name="application_start_time" as={DateTimeField} required={true} - helpText="The date when your application opens." + helpText={`The date when your application opens. ${ + club.is_wharton ? 'Read-only.' : '' + }`} /> +

Congratulations {{ name }}! You've been accepted to {{ committee }} because {{reason}}!

" + } + helpText={`Acceptance email for your ${OBJECT_NAME_SINGULAR}.`} + /> +

Sorry {{ name }}, You've been rejected because {{ reason }}!

" + } + helpText={`Rejection email for your ${OBJECT_NAME_SINGULAR}.`} + /> } confirmDeletion={true} tableFields={[ { name: 'name', label: 'Name' }, - { name: 'season', label: 'Season' }, + { name: 'cycle', label: 'Cycle' }, { name: 'id', label: 'Edit', @@ -365,14 +397,16 @@ export default function ApplicationsCard({ club }: Props): ReactElement { Questions ) : ( - + !club.is_wharton && ( + + ) )} + + {submitMessage !== null && ( +
+ {submitMessage} +
+ )} + + )} + + + ) +} + +const ReasonModal = (props: { + submissions: Array | null + club: string + application: Application | null + updateSubmissions: (s: { name: string }) => void +}): ReactElement => { + const { submissions, club, application, updateSubmissions } = props + const [submitMessage, setSubmitMessage] = useState< + string | ReactElement | null + >(null) + const initialValues = {} + return ( + + { + const data_: { id: number; reason: string }[] = [] + for (const [key, value] of Object.entries(data)) { + data_.push({ id: Number(key), reason: value }) + } + updateSubmissions(data) + doApiRequest( + `/clubs/${club}/applications/${application?.id}/submissions/reason/?format=json`, + { + method: 'POST', + body: { submissions: data_ }, + }, + ).then((response) => { + response.json().then((data) => { + setSubmitMessage(data.detail) + }) + }) + }} + > + {(props) => ( +
+ + Update reasons for selected{' '} + {submissions != null && submissions[0] != null + ? submissions[0].status + : null}{' '} + applicants + + {submissions != null + ? submissions.map((data) => { + return ( + data != null && ( +
+ +
+ ) + ) + }) + : null} + + {submitMessage !== null && ( +
+ {submitMessage} +
+ )} +
+ )} +
) } @@ -185,12 +401,17 @@ export default function ApplicationsPage({ [key: number]: Array }>([]) const [showModal, setShowModal] = useState(false) + const [showNotifModal, setShowNotifModal] = useState(false) + const [showReasonModal, setShowReasonModal] = useState(false) const [ currentSubmission, setCurrentSubmission, ] = useState(null) const [pageIndex, setPageIndex] = useState(0) const [statusToggle, setStatusToggle] = useState(false) + const [categoriesSelectAll, setCategoriesSelectAll] = useState>( + [], + ) useEffect(() => { doApiRequest(`/clubs/${club.code}/applications/?format=json`, { @@ -200,7 +421,10 @@ export default function ApplicationsPage({ .then((applications) => { if (applications.length !== 0) { setApplications(applications) - setCurrentApplication(applications[0]) + setCurrentApplication({ + ...applications[0], + name: format_app_name(applications[0]), + }) } }) }, []) @@ -241,6 +465,11 @@ export default function ApplicationsPage({ const [selectedSubmissions, setSelectedSubmissions] = useState>( [], ) + + const [submitMessage, setSubmitMessage] = useState< + string | ReactElement | null + >(null) + const [status, setStatus] = useState( ApplicationStatusType.Pending, ) @@ -266,10 +495,17 @@ export default function ApplicationsPage({ { label: 'First Name', name: 'first_name' }, { label: 'Last Name', name: 'last_name' }, { label: 'Email', name: 'email' }, - { label: 'Graduation Year', name: 'graduation_year' }, { label: 'Committee', name: 'committee' }, - { label: 'Submitted', name: 'created_at' }, { label: 'Status', name: 'status' }, + { label: 'Graduation Year', name: 'graduation_year' }, + ] + + const extensionTableFields = [ + { label: 'First Name', name: 'first_name' }, + { label: 'Last Name', name: 'last_name' }, + { label: 'Username', name: 'username' }, + { label: 'Graduation Year', name: 'graduation_year' }, + { label: 'Extension End Time', name: 'end_time' }, ] const columns = useMemo( @@ -281,6 +517,16 @@ export default function ApplicationsPage({ [responseTableFields], ) + const format_app_name: (application: Application) => any = (application) => ( + + {application.name} {' - '} + + {application.cycle || + getSemesterFromDate(application.application_end_time)} + + + ) + return ( <> Applications @@ -291,22 +537,31 @@ export default function ApplicationsPage({ setClubsSelectedMembership([...e])} + value={clubsSelectedMembership} + options={clubOptionsMembership} + isMulti + isClearable={false} + backspaceRemovesValue={false} + /> + + + + + + } + + )} + + + {extensionsCycle && extensionsCycle.name && ( + <> + + Individual Club Extensions for {extensionsCycle.name} + +
+ + + + + + + + + + + {clubsExtensions.map((club) => ( + + + + + + ))} + {clubsExtensions.length < 10 && ( +
+ )} +
+
ClubEnd DateException
+

{club.clubName}

+
+ { + club.endDate = date + club.changed = true + setClubsExtensions([...clubsExtensions]) + }} + /> + + { + club.exception = e.target.checked + club.changed = true + setClubsExtensions([...clubsExtensions]) + }} + checked={ + club.exception != null ? club.exception : false + } + /> +
+
+
+ + + )} +
+ + ) +} + +export default WhartonApplicationCycles diff --git a/frontend/components/Settings/WhartonApplicationStatus.tsx b/frontend/components/Settings/WhartonApplicationStatus.tsx index 08f468aef..39c141d18 100644 --- a/frontend/components/Settings/WhartonApplicationStatus.tsx +++ b/frontend/components/Settings/WhartonApplicationStatus.tsx @@ -170,6 +170,18 @@ const WhartonApplicationStatus = ({ ApplicationStatus[] | { detail: string } | null >(initialStatuses ?? null) + function downloadData(statuses) { + const dataStr = + 'data:text/json;charset=utf-8,' + + encodeURIComponent(JSON.stringify(statuses, null, 2)) + const downloadAnchorNode = document.createElement('a') + downloadAnchorNode.setAttribute('href', dataStr) + downloadAnchorNode.setAttribute('download', 'applicationStatuses.json') + document.body.appendChild(downloadAnchorNode) + downloadAnchorNode.click() + downloadAnchorNode.remove() + } + if (statuses == null) { return } @@ -187,6 +199,9 @@ const WhartonApplicationStatus = ({ registered {OBJECT_NAME_PLURAL} for an {FAIR_NAME} fair. Only users with the required permissions can view this page. +
({ diff --git a/frontend/components/Submissions.tsx b/frontend/components/Submissions.tsx index b78276f64..87b5ade21 100644 --- a/frontend/components/Submissions.tsx +++ b/frontend/components/Submissions.tsx @@ -163,12 +163,18 @@ function SubmissionsPage({ ) : ( - !club.is_wharton && ( - - ) + )} - - {submitMessage !== null && ( -
- {submitMessage} -
- )} - - )} - - - ) -} - -const ReasonModal = (props: { - submissions: Array | null - club: string - application: Application | null - updateSubmissions: (s: { name: string }) => void -}): ReactElement => { - const { submissions, club, application, updateSubmissions } = props - const [submitMessage, setSubmitMessage] = useState< - string | ReactElement | null - >(null) - const initialValues = {} - return ( - - { - const data_: { id: number; reason: string }[] = [] - for (const [key, value] of Object.entries(data)) { - data_.push({ id: Number(key), reason: value }) - } - updateSubmissions(data) - doApiRequest( - `/clubs/${club}/applications/${application?.id}/submissions/reason/?format=json`, - { - method: 'POST', - body: { submissions: data_ }, - }, - ).then((response) => { - response.json().then((data) => { - setSubmitMessage(data.detail) - }) - }) - }} - > - {(props) => ( -
- - Update reasons for selected{' '} - {submissions != null && submissions[0] != null - ? submissions[0].status - : null}{' '} - applicants - - {submissions != null - ? submissions.map((data) => { - return ( - data != null && ( -
- -
- ) - ) - }) - : null} - - {submitMessage !== null && ( -
- {submitMessage} -
- )} -
- )} -
) } @@ -401,17 +185,12 @@ export default function ApplicationsPage({ [key: number]: Array }>([]) const [showModal, setShowModal] = useState(false) - const [showNotifModal, setShowNotifModal] = useState(false) - const [showReasonModal, setShowReasonModal] = useState(false) const [ currentSubmission, setCurrentSubmission, ] = useState(null) const [pageIndex, setPageIndex] = useState(0) const [statusToggle, setStatusToggle] = useState(false) - const [categoriesSelectAll, setCategoriesSelectAll] = useState>( - [], - ) useEffect(() => { doApiRequest(`/clubs/${club.code}/applications/?format=json`, { @@ -421,10 +200,7 @@ export default function ApplicationsPage({ .then((applications) => { if (applications.length !== 0) { setApplications(applications) - setCurrentApplication({ - ...applications[0], - name: format_app_name(applications[0]), - }) + setCurrentApplication(applications[0]) } }) }, []) @@ -465,11 +241,6 @@ export default function ApplicationsPage({ const [selectedSubmissions, setSelectedSubmissions] = useState>( [], ) - - const [submitMessage, setSubmitMessage] = useState< - string | ReactElement | null - >(null) - const [status, setStatus] = useState( ApplicationStatusType.Pending, ) @@ -495,17 +266,10 @@ export default function ApplicationsPage({ { label: 'First Name', name: 'first_name' }, { label: 'Last Name', name: 'last_name' }, { label: 'Email', name: 'email' }, + { label: 'Graduation Year', name: 'graduation_year' }, { label: 'Committee', name: 'committee' }, + { label: 'Submitted', name: 'created_at' }, { label: 'Status', name: 'status' }, - { label: 'Graduation Year', name: 'graduation_year' }, - ] - - const extensionTableFields = [ - { label: 'First Name', name: 'first_name' }, - { label: 'Last Name', name: 'last_name' }, - { label: 'Username', name: 'username' }, - { label: 'Graduation Year', name: 'graduation_year' }, - { label: 'Extension End Time', name: 'end_time' }, ] const columns = useMemo( @@ -517,16 +281,6 @@ export default function ApplicationsPage({ [responseTableFields], ) - const format_app_name: (application: Application) => any = (application) => ( - - {application.name} {' - '} - - {application.cycle || - getSemesterFromDate(application.application_end_time)} - - - ) - return ( <> Applications @@ -537,31 +291,22 @@ export default function ApplicationsPage({ setClubsSelectedMembership([...e])} - value={clubsSelectedMembership} - options={clubOptionsMembership} - isMulti - isClearable={false} - backspaceRemovesValue={false} - /> - -
- - - - } - - )} - - - {extensionsCycle && extensionsCycle.name && ( - <> - - Individual Club Extensions for {extensionsCycle.name} - -
- - - - - - - - - - - {clubsExtensions.map((club) => ( - - - - - - ))} - {clubsExtensions.length < 10 && ( -
- )} -
-
ClubEnd DateException
-

{club.clubName}

-
- { - club.endDate = date - club.changed = true - setClubsExtensions([...clubsExtensions]) - }} - /> - - { - club.exception = e.target.checked - club.changed = true - setClubsExtensions([...clubsExtensions]) - }} - checked={ - club.exception != null ? club.exception : false - } - /> -
-
-
- - - )} -
- - ) -} - -export default WhartonApplicationCycles diff --git a/frontend/components/Settings/WhartonApplicationStatus.tsx b/frontend/components/Settings/WhartonApplicationStatus.tsx index 39c141d18..08f468aef 100644 --- a/frontend/components/Settings/WhartonApplicationStatus.tsx +++ b/frontend/components/Settings/WhartonApplicationStatus.tsx @@ -170,18 +170,6 @@ const WhartonApplicationStatus = ({ ApplicationStatus[] | { detail: string } | null >(initialStatuses ?? null) - function downloadData(statuses) { - const dataStr = - 'data:text/json;charset=utf-8,' + - encodeURIComponent(JSON.stringify(statuses, null, 2)) - const downloadAnchorNode = document.createElement('a') - downloadAnchorNode.setAttribute('href', dataStr) - downloadAnchorNode.setAttribute('download', 'applicationStatuses.json') - document.body.appendChild(downloadAnchorNode) - downloadAnchorNode.click() - downloadAnchorNode.remove() - } - if (statuses == null) { return } @@ -199,9 +187,6 @@ const WhartonApplicationStatus = ({ registered {OBJECT_NAME_PLURAL} for an {FAIR_NAME} fair. Only users with the required permissions can view this page. -
({ diff --git a/frontend/components/Submissions.tsx b/frontend/components/Submissions.tsx index 87b5ade21..b78276f64 100644 --- a/frontend/components/Submissions.tsx +++ b/frontend/components/Submissions.tsx @@ -163,18 +163,12 @@ function SubmissionsPage({ ) : ( - + !club.is_wharton && ( + + ) )} + + {submitMessage !== null && ( +
+ {submitMessage} +
+ )} + + )} + + + ) +} + +const ReasonModal = (props: { + submissions: Array | null + club: string + application: Application | null + updateSubmissions: (s: { name: string }) => void +}): ReactElement => { + const { submissions, club, application, updateSubmissions } = props + const [submitMessage, setSubmitMessage] = useState< + string | ReactElement | null + >(null) + const initialValues = {} + return ( + + { + const data_: { id: number; reason: string }[] = [] + for (const [key, value] of Object.entries(data)) { + data_.push({ id: Number(key), reason: value }) + } + updateSubmissions(data) + doApiRequest( + `/clubs/${club}/applications/${application?.id}/submissions/reason/?format=json`, + { + method: 'POST', + body: { submissions: data_ }, + }, + ).then((response) => { + response.json().then((data) => { + setSubmitMessage(data.detail) + }) + }) + }} + > + {(props) => ( +
+ + Update reasons for selected{' '} + {submissions != null && submissions[0] != null + ? submissions[0].status + : null}{' '} + applicants + + {submissions != null + ? submissions.map((data) => { + return ( + data != null && ( +
+ +
+ ) + ) + }) + : null} + + {submitMessage !== null && ( +
+ {submitMessage} +
+ )} +
+ )} +
) } @@ -185,12 +401,17 @@ export default function ApplicationsPage({ [key: number]: Array }>([]) const [showModal, setShowModal] = useState(false) + const [showNotifModal, setShowNotifModal] = useState(false) + const [showReasonModal, setShowReasonModal] = useState(false) const [ currentSubmission, setCurrentSubmission, ] = useState(null) const [pageIndex, setPageIndex] = useState(0) const [statusToggle, setStatusToggle] = useState(false) + const [categoriesSelectAll, setCategoriesSelectAll] = useState>( + [], + ) useEffect(() => { doApiRequest(`/clubs/${club.code}/applications/?format=json`, { @@ -200,7 +421,10 @@ export default function ApplicationsPage({ .then((applications) => { if (applications.length !== 0) { setApplications(applications) - setCurrentApplication(applications[0]) + setCurrentApplication({ + ...applications[0], + name: format_app_name(applications[0]), + }) } }) }, []) @@ -241,6 +465,11 @@ export default function ApplicationsPage({ const [selectedSubmissions, setSelectedSubmissions] = useState>( [], ) + + const [submitMessage, setSubmitMessage] = useState< + string | ReactElement | null + >(null) + const [status, setStatus] = useState( ApplicationStatusType.Pending, ) @@ -266,10 +495,17 @@ export default function ApplicationsPage({ { label: 'First Name', name: 'first_name' }, { label: 'Last Name', name: 'last_name' }, { label: 'Email', name: 'email' }, - { label: 'Graduation Year', name: 'graduation_year' }, { label: 'Committee', name: 'committee' }, - { label: 'Submitted', name: 'created_at' }, { label: 'Status', name: 'status' }, + { label: 'Graduation Year', name: 'graduation_year' }, + ] + + const extensionTableFields = [ + { label: 'First Name', name: 'first_name' }, + { label: 'Last Name', name: 'last_name' }, + { label: 'Username', name: 'username' }, + { label: 'Graduation Year', name: 'graduation_year' }, + { label: 'Extension End Time', name: 'end_time' }, ] const columns = useMemo( @@ -281,6 +517,16 @@ export default function ApplicationsPage({ [responseTableFields], ) + const format_app_name: (application: Application) => any = (application) => ( + + {application.name} {' - '} + + {application.cycle || + getSemesterFromDate(application.application_end_time)} + + + ) + return ( <> Applications @@ -291,22 +537,31 @@ export default function ApplicationsPage({ + + +
+ + ) +} + +type Ticket = { + name: string + count: string | null +} + +const TicketsModal = (props: { event: ClubEvent }): ReactElement => { + const { event } = props + const { large_image_url, image_url, club_name, name, id } = event + + const [submitting, setSubmitting] = useState(false) + + const [tickets, setTickets] = useState([ + { name: 'Regular Ticket', count: null }, + ]) + + const handleNameChange = (name, i) => { + const ticks = [...tickets] + ticks[i].name = name + setTickets(ticks) + } + + const handleCountChange = (count, i) => { + const ticks = [...tickets] + ticks[i].count = count + setTickets(ticks) + } + + const deleteTicket = (i) => { + const ticks = [...tickets] + ticks.splice(i, 1) + setTickets(ticks) + } + + const addNewTicket = () => { + const ticks = [...tickets] + ticks.push({ name: '', count: null }) + setTickets(ticks) + } + + const submit = () => { + if (typeof name === 'string' && tickets.length > 0) { + const quantities = tickets + .filter((ticket) => ticket.count != null) + .map((ticket) => { + return { type: ticket.name, count: parseInt(ticket.count || '') } + }) + doApiRequest(`/events/${id}/tickets/?format=json`, { + method: 'PUT', + body: { + quantities: quantities, + }, + }) + notify(<>Tickets Created!, 'success') + setSubmitting(false) + } + } + + const disableSubmit = tickets.some( + (ticket) => + typeof ticket.name !== 'string' || + typeof ticket.count === null || + !Number.isInteger(parseInt(ticket.count || '0')), + ) + + return ( + + {club_name != null ? club_name.toLocaleUpperCase() : 'Event'}

+ } + /> + + {name} + + Create new tickets for this event. To be filled with actual + instructions. Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut + enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi + ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. + + + +

Tickets

+
+ + {tickets.map((ticket, index) => ( + + ))} + + + + +
+ {submitting ? ( + <> +

+ Are you sure you want to create these tickets? Ticket classes + and quantities are final and you will not be able to change them + moving forward. +

+ + + + ) : ( + + )} +
+
+
+ ) +} + +export default TicketsModal diff --git a/frontend/components/ClubEditPage/TicketsViewCard.tsx b/frontend/components/ClubEditPage/TicketsViewCard.tsx new file mode 100644 index 000000000..d0a805bd5 --- /dev/null +++ b/frontend/components/ClubEditPage/TicketsViewCard.tsx @@ -0,0 +1,62 @@ +import Link from 'next/link' +import React, { ReactElement } from 'react' + +import { doApiRequest } from '~/utils' + +import Table from '../common/Table' +import BaseCard from './BaseCard' + +export default function TicketsViewCard({ club }): ReactElement { + const GetTicketsHolders = (id) => { + doApiRequest(`/events/${id}/tickets?format=json`, { + method: 'GET', + }) + .then((resp) => resp.json()) + .then((res) => { + // console.log(res) + }) + } + + const eventsTableFields = [ + { label: 'Event Name', name: 'name' }, + { + label: '', + name: 'view', + render: (id) => ( +
+ ), + }, + ] + + // console.log(club.events) + const ticketEvents = club.events.filter((event) => event.ticketed) + + return ( + + {ticketEvents.length > 0 ? ( + + item.id ? { ...item, id: item.id } : { ...item, id: index }, + )} + columns={eventsTableFields} + searchableColumns={['name']} + filterOptions={[]} + hideSearch={true} + focusable={true} + /> + ) : ( + <> + You don't have any ticketed events, to add create ticketed events or + add ticket offerings, to existing events, go to the events, click + create on the tickets section below the event details. + + )} + + ) +} diff --git a/frontend/components/ClubPage/Actions.tsx b/frontend/components/ClubPage/Actions.tsx index 563d89017..b578ec643 100644 --- a/frontend/components/ClubPage/Actions.tsx +++ b/frontend/components/ClubPage/Actions.tsx @@ -286,24 +286,24 @@ const Actions = ({ {SHOW_MEMBERSHIP_REQUEST && !inClub && club.members.length > 0 && - (isMembershipOpen - ? club.accepting_members && ( - - ) - : SHOW_APPLICATIONS && ( - - - Apply - - - ))} + isMembershipOpen && + club.accepting_members && ( + + )} + {SHOW_APPLICATIONS && ( + + + Apply + + + )} {canEdit && ( diff --git a/frontend/components/ClubPage/InfoBox.tsx b/frontend/components/ClubPage/InfoBox.tsx index e4e15eda7..a943235e6 100644 --- a/frontend/components/ClubPage/InfoBox.tsx +++ b/frontend/components/ClubPage/InfoBox.tsx @@ -40,9 +40,9 @@ const InfoBox = (props: InfoBoxProps): ReactElement | null => { field: 'size', icon: 'user', alt: 'members', - text: `${props.club.membership_count} Registered (${getSizeDisplay( - props.club.size, - )})`, + text: `${ + props.club.membership_count ?? props.club.members.length + } Registered (${getSizeDisplay(props.club.size)})`, }, { field: 'accepting_members', diff --git a/frontend/components/ClubPage/RenewalRequestDialog.tsx b/frontend/components/ClubPage/RenewalRequestDialog.tsx index fde71321f..fae2eeffb 100644 --- a/frontend/components/ClubPage/RenewalRequestDialog.tsx +++ b/frontend/components/ClubPage/RenewalRequestDialog.tsx @@ -29,8 +29,8 @@ const RenewalRequest = ({ club }: RenewalRequestProps): ReactElement => { clubs: { TITLE: ( <> - {club.name} needs to be re-registered for the 2020-2021 - academic year. + {club.name} needs to be re-registered for the current academic + year. ), PROCESS_ACTION: 'start the renewal process', diff --git a/frontend/components/CustomOption.tsx b/frontend/components/CustomOption.tsx new file mode 100644 index 000000000..d0f1da1f1 --- /dev/null +++ b/frontend/components/CustomOption.tsx @@ -0,0 +1,69 @@ +import { EditorState } from 'draft-js' +import { ReactElement, useState } from 'react' + +import { ModalContent } from './ClubPage/Actions' +import { Icon, Modal } from './common' + +type Props = { + editorState?: EditorState + onChange?: (state: EditorState) => void +} + +type Entity = { + type: string + mutability: 'MUTABLE' | 'IMMUTABLE' + data: any +} + +/** + * A toolbar widget for the editor to add custom variables to personalized outcome notifications. + */ +const CustomOption = (props: Props): ReactElement => { + const [showModal, setShowModal] = useState(false) + + const [variableSelected, setVariableSelected] = useState('name') + const [errorMessage, setErrorMessage] = useState< + ReactElement | string | null + >(null) + + const helperText = String.raw` + Hi {{ name }}, you have been invited to join our club's {{ committee }} committee. + + We were very impressed by your initiative and admitted you into the club because of it. + + {{ reason }} + + Congratulations! + ` + + return ( + <> +
setShowModal(true)} + > + Sample Email! +
+ setShowModal(false)} + marginBottom={false} + width="80%" + > + +

Sample Email

+
{helperText}
+

+ (note: don't use any variables other than the ones used here!) +

+ {errorMessage !== null && ( +

{errorMessage}

+ )} +
+
+ + ) +} + +export default CustomOption diff --git a/frontend/components/DisplayButtons.tsx b/frontend/components/DisplayButtons.tsx index 4b44e819f..9718b4c36 100644 --- a/frontend/components/DisplayButtons.tsx +++ b/frontend/components/DisplayButtons.tsx @@ -10,6 +10,7 @@ import { WHITE_ALPHA, } from '../constants/colors' import { BODY_FONT } from '../constants/styles' +import { isSummer } from '../utils' import { OBJECT_NAME_SINGULAR, OBJECT_NAME_TITLE_SINGULAR, @@ -69,16 +70,18 @@ const DisplayButtons = ({ > - - - - Add {OBJECT_NAME_TITLE_SINGULAR} - - + {!isSummer() && ( + + + + Add {OBJECT_NAME_TITLE_SINGULAR} + + + )} ) diff --git a/frontend/components/EventPage/EventModal.tsx b/frontend/components/EventPage/EventModal.tsx index c4d3615aa..6b92a1f82 100644 --- a/frontend/components/EventPage/EventModal.tsx +++ b/frontend/components/EventPage/EventModal.tsx @@ -7,12 +7,8 @@ import styled from 'styled-components' import { Icon } from '../../components/common' import { CLUB_ROUTE, ZOOM_BLUE } from '../../constants' import { MEDIUM_GRAY } from '../../constants/colors' -import { Club, ClubEvent } from '../../types' -import { - apiSetFavoriteStatus, - apiSetSubscribeStatus, - doApiRequest, -} from '../../utils' +import { ClubEvent } from '../../types' +import { doApiRequest } from '../../utils' import { OBJECT_NAME_SINGULAR, OBJECT_NAME_TITLE_SINGULAR, @@ -128,43 +124,32 @@ const LiveEventUpdater = ({ /** * Buttons that allow you to bookmark and subscribe to a club. */ -const ActionButtons = ({ club: code }): ReactElement | null => { - const [isBookmarked, setBookmarked] = useState(null) - const [isSubscribed, setSubscribed] = useState(null) - - useEffect(() => { - doApiRequest(`/clubs/${code}/?format=json`) - .then((resp) => resp.json()) - .then((data: Club) => { - setSubscribed(data.is_subscribe) - setBookmarked(data.is_favorite) - }) - }, [code]) - - if (isSubscribed == null || isBookmarked == null) { - return null - } - +const ActionButtons = ({ + club: code, + isTicketEvent, + setDisplayTicketModal, + ticketCount, + userHasTickets, +}): ReactElement | null => { return ( <> - - + {isTicketEvent && ( + <> + + + + + )} ) } @@ -222,11 +207,19 @@ const EventModal = (props: { club_name, start_time, end_time, + // ticketed, name, url, description, } = event + const ticketed = true const [userCount, setUserCount] = useState(null) + const [ticketCount, setTicketCount] = useState(null) + const [userHasTickets, setUserHasTickets] = useState(null) + const [displayTicketModal, setDisplayTicketModal] = useState(false) + const [availableTickets, setAvailableTickets] = useState | null>( + null, + ) const now = new Date() const startDate = new Date(start_time) @@ -242,11 +235,36 @@ const EventModal = (props: { setUserCount(resp) }) } + // TODO: CHANGE TO event.ticketed instead of true when that is added + if (ticketed) { + setTicketCount(0) // TODO: CHANGE BACK TO 0 + doApiRequest(`/events/${event.id}/tickets/`) + .then((resp) => resp.json()) + .then((resp) => { + if (resp.available) { + setAvailableTickets(resp.available) + for (let i = 0; i < resp.available.length; i++) { + setTicketCount(ticketCount + resp.available[i].count) + } + } + }) + setUserHasTickets(false) + doApiRequest(`/tickets/`) + .then((resp) => resp.json()) + .then((resp) => { + for (let i = 0; i < resp.length; i++) { + if (resp[i].event.id === event.id) { + setUserHasTickets(true) + break + } + } + }) + } } useEffect(refreshLiveData, []) - return ( + return !displayTicketModal ? (
- {club != null && } + {club != null && ( + + )} + ) : ( + // TODO: THIS IS NOTHING RN, SHOULD DISPLAY ALL AVAILABLE TICKETS + "" ) } diff --git a/frontend/components/FormComponents.tsx b/frontend/components/FormComponents.tsx index b31f7abd2..a6edf3c5f 100644 --- a/frontend/components/FormComponents.tsx +++ b/frontend/components/FormComponents.tsx @@ -16,12 +16,13 @@ import DatePicker from 'react-datepicker' import Select from 'react-select' import CreatableSelect from 'react-select/creatable' import styled from 'styled-components' -import uuid from 'uuid' +import { v4 as uuidv4 } from 'uuid' import { DynamicQuestion } from '../types' import { titleize } from '../utils' import AddressField from './ClubEditPage/AddressField' import { Icon } from './common' +import CustomOption from './CustomOption' import EmbedOption, { blockRendererFunction, entityToHtml, @@ -234,6 +235,91 @@ export const RichTextField = useFieldWrapper( }, ) +/** + * A rich text editor that accepts and outputs HTML as well as basic templating features. + */ +export const ApplicationUpdateTextField = useFieldWrapper( + (props: BasicFormField & AnyHack): ReactElement => { + const { setFieldValue } = useFormikContext() + const textValue = useRef(null) + + const [editorState, setEditorState] = useState(() => + EditorState.createEmpty(), + ) + + useEffect(() => { + if (props.value !== textValue.current) { + if (props.value && props.value.length) { + setEditorState( + EditorState.createWithContent( + ContentState.createFromBlockArray( + htmlToDraft(props.value, htmlToEntity).contentBlocks, + ), + ), + ) + } else { + setEditorState(EditorState.createEmpty()) + } + textValue.current = props.value + } + }, [props.value]) + + return ( +
+ + + + + {Editor != null && ( + { + setEditorState(state) + const newValue = draftToHtml( + convertToRaw(state.getCurrentContent()), + undefined, + undefined, + entityToHtml, + ) + textValue.current = newValue + setFieldValue(props.name, newValue) + }} + toolbar={{ + options: [ + 'inline', + 'fontSize', + 'fontFamily', + 'list', + 'textAlign', + 'colorPicker', + 'link', + 'image', + 'remove', + 'history', + ], + }} + editorStyle={{ + border: '1px solid #dbdbdb', + padding: '0 1em', + }} + toolbarCustomButtons={[]} + customBlockRenderFunc={blockRendererFunction} + /> + )} +
+ ) + }, +) + const DatePickerWrapper = styled.span` & .react-datepicker-wrapper { width: 100%; @@ -441,7 +527,7 @@ export const DynamicQuestionField = useFieldWrapper( const addField = (type: string): void => { values.push({ - name: uuid.v4(), + name: uuidv4(), label: '', type: type, choices: [], diff --git a/frontend/components/Header/Links.tsx b/frontend/components/Header/Links.tsx index 8737b706b..19701719c 100644 --- a/frontend/components/Header/Links.tsx +++ b/frontend/components/Header/Links.tsx @@ -18,7 +18,7 @@ import { MD, mediaMaxWidth, } from '../../constants/measurements' -import { SETTINGS_ROUTE } from '../../constants/routes' +import { CART_ROUTE, SETTINGS_ROUTE } from '../../constants/routes' import { UserInfo } from '../../types' import { LOGIN_URL } from '../../utils' import { logEvent } from '../../utils/analytics' @@ -100,6 +100,14 @@ const Links = ({ userInfo, authenticated, show }: Props): ReactElement => { logEvent('events', 'click')}> Events + logEvent('cfa redirect', 'click')} + target="_blank" + rel="noopener noreferrer" + > + Funding + logEvent('faq', 'click')}> FAQ @@ -118,6 +126,12 @@ const Links = ({ userInfo, authenticated, show }: Props): ReactElement => { {userInfo.name || userInfo.username} )} + {authenticated === true && ( + + + Cart + + )}
) diff --git a/frontend/components/ModelForm.tsx b/frontend/components/ModelForm.tsx index bbf84fd07..95af62886 100644 --- a/frontend/components/ModelForm.tsx +++ b/frontend/components/ModelForm.tsx @@ -1,4 +1,5 @@ import { Form, Formik } from 'formik' +import moment from 'moment' import { ReactElement, useEffect, useMemo, useState } from 'react' import styled from 'styled-components' @@ -73,6 +74,7 @@ type ModelFormProps = { empty?: ReactElement | string fields: any tableFields?: TableField[] + searchableColumns?: string[] filterOptions?: FilterOption[] currentTitle?: (object: ModelObject) => ReactElement | string noun?: string @@ -112,6 +114,7 @@ type ModelTableProps = { tableFields: TableField[] filterOptions?: FilterOption[] objects: ModelObject[] + searchableColumns?: string[] allowEditing?: boolean allowDeletion?: boolean confirmDeletion?: boolean @@ -131,6 +134,7 @@ export const ModelTable = ({ tableFields, filterOptions, objects, + searchableColumns, allowEditing = false, allowDeletion = false, confirmDeletion = false, @@ -174,17 +178,16 @@ export const ModelTable = ({ } return (
- {allowEditing && - (object.active === true || object.active === undefined) && ( - - )} + {allowEditing && ( + + )} {allowDeletion && (object.active === true || object.active === undefined) && ( + + + )} + /> + setEditMembership(false)}> + {membershipCycle && membershipCycle.name && ( + <> + + Club Membership for {membershipCycle.name} Cycle + + { + <> +
+ +
+ + + + + + + + + {clubsExtensions.map((club) => ( + + + + + + ))} + {clubsExtensions.length < 10 && ( +
+ )} +
+
ClubEnd DateException
+

{club.clubName}

+
+ { + club.endDate = date + club.changed = true + setClubsExtensions([...clubsExtensions]) + }} + /> + + { + club.exception = e.target.checked + club.changed = true + setClubsExtensions([...clubsExtensions]) + }} + checked={ + club.exception != null ? club.exception : false + } + /> +
+ + + + + )} + + + ) +} + +export default WhartonApplicationCycles diff --git a/frontend/components/TicketsPage/CartTickets.tsx b/frontend/components/TicketsPage/CartTickets.tsx new file mode 100644 index 000000000..874614286 --- /dev/null +++ b/frontend/components/TicketsPage/CartTickets.tsx @@ -0,0 +1,107 @@ +import { ReactElement, useMemo } from 'react' +import styled from 'styled-components' + +import { CARD_HEADING, CLUBS_GREY, H1_TEXT } from '~/constants' +import { EventTicket } from '~/types' + +import { Card } from '../common' + +export interface CountedEventTicket extends EventTicket { + count: number +} + +export interface CartTicketsProps { + tickets: EventTicket[] +} + +const TicketCard = styled(Card)` + display: flex; + flex-direction: row; +` + +const Body = styled.div` + margin-left: 10px; + display: flex; + flex-direction: column; + justify-content: space-between; +` + +const XButton = styled.button` + width: 14px; + height: 14px; +` + +const EventTitle = styled.strong` + font-size: 1.3rem; + line-height: 1.2; + color: ${H1_TEXT}; + margin-bottom: 0.5rem; + font-weight: ${CARD_HEADING}; +` + +const ClubName = styled.div` + color: ${CLUBS_GREY}; +` + +interface TicketImageProps { + url: string | null +} + +const THUMBNAIL_SIZE = '40px' + +const Thumbnail = styled.div` + width: ${THUMBNAIL_SIZE}; + height: ${THUMBNAIL_SIZE}; + margin: 8px; + + background-image: url(${({ url }) => url || '/static/img/tickets.png'}); + background-size: contain; + border-radius: 3px; +` + +// TODO: use ticket type as well +const ticketKey = (t1: EventTicket) => t1.event.id + +/** + * Combines an array of tickets into a list of unique ticket types with counts + * @param tickets - Original array of tickets + * @returns Array of tickets condensed into unique types + */ +const combineTickets = (tickets: EventTicket[]): CountedEventTicket[] => { + const tix = [...tickets] + const countedTickets: { [key: string]: CountedEventTicket } = {} + + while (tix.length > 0) { + const currentTicket = tix.pop() as EventTicket + const key = ticketKey(currentTicket) + if (countedTickets[key] === undefined) { + countedTickets[key] = { ...currentTicket, count: 1 } + } else { + countedTickets[key].count += 1 + } + } + + return Object.values(countedTickets) +} + +const CartTickets = ({ tickets }: CartTicketsProps): ReactElement => { + const condensedTickets = useMemo(() => combineTickets(tickets), [tickets]) + // console.log(condensedTickets) + return ( +
    + {condensedTickets.map(({ event, id }) => { + return ( + + + + {event.name} + {event.club_name} + + + ) + })} +
+ ) +} + +export default CartTickets diff --git a/frontend/components/common/Table.tsx b/frontend/components/common/Table.tsx index 141f608bd..ab8cf46bc 100644 --- a/frontend/components/common/Table.tsx +++ b/frontend/components/common/Table.tsx @@ -71,6 +71,7 @@ type tableProps = { searchableColumns: string[] filterOptions?: FilterOption[] focusable?: boolean + hideSearch?: boolean onClick?: (row: any, event: any) => void draggable?: boolean onDragEnd?: (result: any) => void | null | undefined @@ -122,6 +123,7 @@ const Table = ({ filterOptions, focusable, onClick, + hideSearch = false, draggable = false, onDragEnd, initialPage = 0, @@ -203,6 +205,7 @@ const Table = ({ { columns: memoColumns, data: tableData, + autoResetSortBy: false, filterTypes, initialState: { pageIndex: initialPage, pageSize: initialPageSize }, }, @@ -233,57 +236,61 @@ const Table = ({ } return ( - -
- - - -
-
-
- {filterOptions && - filterOptions.map((filterOption) => ( -
- + +
+ )} +
+
+ {filterOptions && + filterOptions.map((filterOption) => ( +
+ ) @@ -636,11 +618,11 @@ export const TextField = useFieldWrapper( return ( ) }, @@ -791,7 +773,6 @@ export const SelectField = useFieldWrapper( deserialize, valueDeserialize, isMulti, - creatable, formatOptionLabel, customHandleChange, }: BasicFormField & @@ -850,7 +831,6 @@ export const SelectField = useFieldWrapper( key={name} placeholder={placeholder} isMulti={isMulti} - creatable={creatable} value={(valueDeserialize ?? actualDeserialize)(value)} options={ actualDeserialize(choices) as { diff --git a/frontend/components/Header/Feedback.tsx b/frontend/components/Header/Feedback.tsx index 482fb9d88..05c3b77f8 100644 --- a/frontend/components/Header/Feedback.tsx +++ b/frontend/components/Header/Feedback.tsx @@ -13,19 +13,17 @@ const ICON_SIZE = '1.5rem' const OFFSET = 18 interface LinkProps { - offsetAddition: number + $offsetAddition: number } -export const ActionLink = styled.a.attrs((props: LinkProps) => ({ - offsetAddition: props.offsetAddition, -}))` +export const ActionLink = styled.a` display: inline-block; width: ${DIAMETER}; height: ${DIAMETER}; border-radius: 3rem; background-color: ${FEEDBACK_BG}; position: fixed; - bottom: ${(props) => props.offsetAddition + OFFSET}px; + bottom: ${(props) => props.$offsetAddition + OFFSET}px; right: ${OFFSET}px; text-align: center; box-shadow: 0 2px 8px rgba(25, 89, 130, 0.4); @@ -45,7 +43,7 @@ export const ActionLink = styled.a.attrs((props: LinkProps) => ({ const Feedback = (): ReactElement => ( { - - - - - - -