From f41a07f4910abacfd1ac36b41cff0d473d3bd9c0 Mon Sep 17 00:00:00 2001 From: Avi Upadhyayula <69180850+aviupadhyayula@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:21:24 -0800 Subject: [PATCH 1/3] Add option to pass transaction fees onto buyers (#762) * Add TicketSettings model * Update views to use ticket settings * Update tests * Update tests for codecov * Minor tweaks to tests * Revert to default order limit --- ..._remove_event_ticket_drop_time_and_more.py | 41 ++++++ .../0119_alter_ticketsettings_order_limit.py | 18 +++ backend/clubs/models.py | 23 ++- backend/clubs/views.py | 136 +++++++++++------- backend/tests/clubs/test_ticketing.py | 48 ++++++- 5 files changed, 206 insertions(+), 60 deletions(-) create mode 100644 backend/clubs/migrations/0118_remove_event_ticket_drop_time_and_more.py create mode 100644 backend/clubs/migrations/0119_alter_ticketsettings_order_limit.py diff --git a/backend/clubs/migrations/0118_remove_event_ticket_drop_time_and_more.py b/backend/clubs/migrations/0118_remove_event_ticket_drop_time_and_more.py new file mode 100644 index 000000000..457f2973e --- /dev/null +++ b/backend/clubs/migrations/0118_remove_event_ticket_drop_time_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.4 on 2025-01-02 07:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0117_clubapprovalresponsetemplate"), + ] + + operations = [ + migrations.RemoveField(model_name="event", name="ticket_drop_time",), + migrations.RemoveField(model_name="event", name="ticket_order_limit",), + migrations.CreateModel( + name="TicketSettings", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order_limit", models.IntegerField(blank=True, null=True)), + ("drop_time", models.DateTimeField(blank=True, null=True)), + ("fee_charged_to_buyer", models.BooleanField(default=False)), + ( + "event", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="ticket_settings", + to="clubs.event", + ), + ), + ], + ), + ] diff --git a/backend/clubs/migrations/0119_alter_ticketsettings_order_limit.py b/backend/clubs/migrations/0119_alter_ticketsettings_order_limit.py new file mode 100644 index 000000000..8d2da40a6 --- /dev/null +++ b/backend/clubs/migrations/0119_alter_ticketsettings_order_limit.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2025-01-04 01:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0118_remove_event_ticket_drop_time_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="ticketsettings", + name="order_limit", + field=models.IntegerField(blank=True, default=10, null=True), + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 22105c7a0..dc4432a26 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -940,8 +940,6 @@ class Event(models.Model): parent_recurring_event = models.ForeignKey( RecurringEvent, on_delete=models.CASCADE, blank=True, null=True ) - ticket_order_limit = models.IntegerField(default=10) - ticket_drop_time = models.DateTimeField(null=True, blank=True) OTHER = 0 RECRUITMENT = 1 @@ -969,6 +967,10 @@ class Event(models.Model): def create_thumbnail(self, request=None): return create_thumbnail_helper(self, request, 400) + @property + def has_tickets(self): + return self.tickets.exists() + def __str__(self): return self.name @@ -1821,6 +1823,23 @@ class Cart(models.Model): checkout_context = models.CharField(max_length=8297, blank=True, null=True) +class TicketSettings(models.Model): + """ + Configuration settings for events that have tickets. + Only created when an event has associated tickets created. + """ + + event = models.OneToOneField( + Event, on_delete=models.CASCADE, related_name="ticket_settings" + ) + order_limit = models.IntegerField(null=True, blank=True, default=10) + drop_time = models.DateTimeField(null=True, blank=True) + fee_charged_to_buyer = models.BooleanField(default=False) + + def __str__(self): + return f"Ticket settings for {self.event.name}" + + class TicketQuerySet(models.query.QuerySet): def delete(self): if self.filter(transaction_record__isnull=False).exists(): diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 6d03c9607..7a1cde9ad 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -117,6 +117,7 @@ Tag, Testimonial, Ticket, + TicketSettings, TicketTransactionRecord, TicketTransferRecord, Year, @@ -2464,6 +2465,14 @@ def add_to_cart(self, request, *args, **kwargs): --- """ event = self.get_object() + + # Check if event has any tickets + if not event.has_tickets: + return Response( + {"detail": "This event does not have any tickets", "success": False}, + status=status.HTTP_403_FORBIDDEN, + ) + cart, _ = Cart.objects.get_or_create(owner=self.request.user) # Check if the event has already ended @@ -2474,7 +2483,10 @@ def add_to_cart(self, request, *args, **kwargs): ) # Cannot add tickets that haven't dropped yet - if event.ticket_drop_time and timezone.now() < event.ticket_drop_time: + if ( + event.ticket_settings.drop_time + and timezone.now() < event.ticket_settings.drop_time + ): return Response( {"detail": "Ticket drop time has not yet elapsed", "success": False}, status=status.HTTP_403_FORBIDDEN, @@ -2490,11 +2502,14 @@ def add_to_cart(self, request, *args, **kwargs): num_requested = sum(item["count"] for item in quantities) num_carted = cart.tickets.filter(event=event).count() - if num_requested + num_carted > event.ticket_order_limit: + if ( + event.ticket_settings.order_limit + and num_requested + num_carted > event.ticket_settings.order_limit + ): return Response( { "detail": f"Order exceeds the maximum ticket limit of " - f"{event.ticket_order_limit}.", + f"{event.ticket_settings.order_limit}.", "success": False, }, status=status.HTTP_403_FORBIDDEN, @@ -2680,20 +2695,22 @@ def tickets(self, request, *args, **kwargs): --- """ event = self.get_object() - tickets = Ticket.objects.filter(event=event) - if event.ticket_drop_time and timezone.now() < event.ticket_drop_time: + if not event.has_tickets or ( + event.ticket_settings.drop_time + and timezone.now() < event.ticket_settings.drop_time + ): return Response({"totals": [], "available": []}) # Take price of first ticket of given type for now totals = ( - tickets.values("type") + event.tickets.values("type") .annotate(price=Max("price")) .annotate(count=Count("type")) .order_by("type") ) available = ( - tickets.filter(owner__isnull=True, holder__isnull=True, buyable=True) + event.tickets.filter(owner__isnull=True, holder__isnull=True, buyable=True) .values("type") .annotate(price=Max("price")) .annotate(count=Count("type")) @@ -2705,7 +2722,11 @@ def tickets(self, request, *args, **kwargs): @transaction.atomic def create_tickets(self, request, *args, **kwargs): """ - Create ticket offerings for event + Create or update ticket offerings for an event. + + This endpoint allows configuring ticket types, quantities, prices, and settings. + Tickets cannot be modified after they have been dropped or sold. + --- requestBody: content: @@ -2717,6 +2738,11 @@ def create_tickets(self, request, *args, **kwargs): type: array items: type: object + required: + - type + - count + - price + - transferable properties: type: type: string @@ -2725,26 +2751,24 @@ def create_tickets(self, request, *args, **kwargs): price: type: number group_size: - type: number - required: false + type: integer group_discount: type: number format: float - required: false transferable: type: boolean buyable: type: boolean - required: false order_limit: - type: int - required: false + type: integer drop_time: type: string format: date-time - required: false + fee_charged_to_buyer: + type: boolean responses: "200": + description: Tickets created successfully content: application/json: schema: @@ -2753,6 +2777,7 @@ def create_tickets(self, request, *args, **kwargs): detail: type: string "400": + description: Invalid request parameters content: application/json: schema: @@ -2761,6 +2786,7 @@ def create_tickets(self, request, *args, **kwargs): detail: type: string "403": + description: Tickets cannot be modified content: application/json: schema: @@ -2772,27 +2798,59 @@ def create_tickets(self, request, *args, **kwargs): """ event = self.get_object() - # Tickets can't be edited after they've dropped - if event.ticket_drop_time and timezone.now() > event.ticket_drop_time: + # Tickets can't be edited after they've been sold or checked out + if event.tickets.filter( + Q(owner__isnull=False) | Q(holder__isnull=False) + ).exists(): return Response( - {"detail": "Tickets cannot be edited after they have dropped"}, + { + "detail": "Tickets cannot be edited after they have been " + "sold or checked out" + }, status=status.HTTP_403_FORBIDDEN, ) - # Tickets can't be edited after they've been sold or held + ticket_settings, _ = TicketSettings.objects.get_or_create(event=event) + + # Tickets can't be edited after they've dropped if ( - Ticket.objects.filter(event=event) - .filter(Q(owner__isnull=False) | Q(holder__isnull=False)) - .exists() + event.ticket_settings.drop_time + and timezone.now() > event.ticket_settings.drop_time ): return Response( - { - "detail": "Tickets cannot be edited after they have been " - "sold or checked out" - }, + {"detail": "Tickets cannot be edited after they have dropped"}, status=status.HTTP_403_FORBIDDEN, ) + order_limit = request.data.get("order_limit", None) + if order_limit is not None: + ticket_settings.order_limit = order_limit + ticket_settings.save() + + drop_time = request.data.get("drop_time", None) + if drop_time is not None: + try: + drop_time = datetime.datetime.strptime(drop_time, "%Y-%m-%dT%H:%M:%S%z") + except ValueError as e: + return Response( + {"detail": f"Invalid drop time: {str(e)}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if drop_time < timezone.now(): + return Response( + {"detail": "Specified drop time has already elapsed"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + ticket_settings.drop_time = drop_time + ticket_settings.save() + + fee_charged_to_buyer = request.data.get("fee_charged_to_buyer", None) + if fee_charged_to_buyer is not None: + ticket_settings.fee_charged_to_buyer = fee_charged_to_buyer + ticket_settings.save() + quantities = request.data.get("quantities", []) if not quantities: return Response( @@ -2855,35 +2913,11 @@ def create_tickets(self, request, *args, **kwargs): for item in quantities for _ in range(item["count"]) ] - Ticket.objects.bulk_create(tickets) - order_limit = request.data.get("order_limit", None) - if order_limit is not None: - event.ticket_order_limit = order_limit - event.save() - - drop_time = request.data.get("drop_time", None) - if drop_time is not None: - try: - drop_time = datetime.datetime.strptime(drop_time, "%Y-%m-%dT%H:%M:%S%z") - except ValueError as e: - return Response( - {"detail": f"Invalid drop time: {str(e)}"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if drop_time < timezone.now(): - return Response( - {"detail": "Specified drop time has already elapsed"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - event.ticket_drop_time = drop_time - event.save() - cache.delete(f"clubs:{event.club.id}-authed") cache.delete(f"clubs:{event.club.id}-anon") + return Response({"detail": "Successfully created tickets"}) @action(detail=True, methods=["post"]) diff --git a/backend/tests/clubs/test_ticketing.py b/backend/tests/clubs/test_ticketing.py index 8f047b9d8..ac6def249 100644 --- a/backend/tests/clubs/test_ticketing.py +++ b/backend/tests/clubs/test_ticketing.py @@ -20,6 +20,7 @@ Event, Membership, Ticket, + TicketSettings, TicketTransactionRecord, TicketTransferRecord, ) @@ -59,6 +60,10 @@ def commonSetUp(self): end_time=timezone.now() + timezone.timedelta(days=3), ) + self.ticket_settings = TicketSettings.objects.create( + event=self.event1, + ) + self.ticket_totals = [ {"type": "normal", "count": 20, "price": 15.0}, {"type": "premium", "count": 10, "price": 30.0}, @@ -212,14 +217,14 @@ def test_create_ticket_offerings_delay_drop(self): format="json", ) - self.event1.refresh_from_db() + self.ticket_settings.refresh_from_db() # Drop time should be set - self.assertIsNotNone(self.event1.ticket_drop_time) + self.assertIsNotNone(self.ticket_settings.drop_time) # Drop time should be 12 hours from initial ticket creation expected_drop_time = timezone.now() + timezone.timedelta(hours=12) - diff = abs(self.event1.ticket_drop_time - expected_drop_time) + diff = abs(self.ticket_settings.drop_time - expected_drop_time) self.assertTrue(diff < timezone.timedelta(minutes=5)) # Move Django's internal clock 13 hours forward @@ -277,6 +282,35 @@ def test_create_ticket_offerings_already_owned_or_held(self): ) self.assertEqual(resp.status_code, 403, resp.content) + def test_create_tickets_with_settings(self): + self.client.login(username=self.user1.username, password="test") + + drop_time = timezone.now() + timedelta(days=1) + args = { + "quantities": [ + {"type": "_normal", "count": 5, "price": 10}, + {"type": "_premium", "count": 3, "price": 20}, + ], + "order_limit": 2, + "drop_time": drop_time.strftime("%Y-%m-%dT%H:%M:%S%z"), + "fee_charged_to_buyer": True, + } + resp = self.client.put( + reverse("club-events-tickets", args=(self.club1.code, self.event1.pk)), + args, + format="json", + ) + self.assertIn(resp.status_code, [200, 201], resp.content) + + self.event1.refresh_from_db() + self.assertEqual(self.event1.ticket_settings.order_limit, 2) + self.assertAlmostEqual( + self.event1.ticket_settings.drop_time.timestamp(), + drop_time.timestamp(), + delta=1.0, # allow 1 second difference to avoid flaky tests + ) + self.assertTrue(self.event1.ticket_settings.fee_charged_to_buyer) + def test_issue_tickets(self): self.client.login(username=self.user1.username, password="test") args = { @@ -469,8 +503,8 @@ def test_get_tickets_information(self): ) def test_get_tickets_before_drop_time(self): - self.event1.ticket_drop_time = timezone.now() + timedelta(days=1) - self.event1.save() + self.ticket_settings.drop_time = timezone.now() + timedelta(days=1) + self.ticket_settings.save() self.client.login(username=self.user1.username, password="test") resp = self.client.get( @@ -626,8 +660,8 @@ def test_add_to_cart_before_ticket_drop(self): self.client.login(username=self.user1.username, password="test") # Set drop time - self.event1.ticket_drop_time = timezone.now() + timedelta(hours=12) - self.event1.save() + self.ticket_settings.drop_time = timezone.now() + timedelta(hours=12) + self.ticket_settings.save() tickets_to_add = { "quantities": [ From bf413bc8fd60ed28f9001f205dae6f8691c034f0 Mon Sep 17 00:00:00 2001 From: Avi Upadhyayula <69180850+aviupadhyayula@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:43:42 -0500 Subject: [PATCH 2/3] Add ability to open and close ticket sales (#766) * Add TicketSettings model * Update views to use ticket settings * Update tests * Update tests for codecov * Minor tweaks to tests * Add active attribute to ticket settings * Add route to toggle status of ticket sales * Add tests * Minor styling fixes * Merge migrations and keep default order limit --- .../migrations/0120_ticketsettings_active.py | 18 +++ backend/clubs/models.py | 3 +- backend/clubs/views.py | 102 ++++++++++++++++- backend/tests/clubs/test_ticketing.py | 106 ++++++++++++++++++ 4 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 backend/clubs/migrations/0120_ticketsettings_active.py diff --git a/backend/clubs/migrations/0120_ticketsettings_active.py b/backend/clubs/migrations/0120_ticketsettings_active.py new file mode 100644 index 000000000..a2e5e0ee3 --- /dev/null +++ b/backend/clubs/migrations/0120_ticketsettings_active.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2025-01-16 19:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0119_alter_ticketsettings_order_limit"), + ] + + operations = [ + migrations.AddField( + model_name="ticketsettings", + name="active", + field=models.BooleanField(default=True), + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index dc4432a26..3c21cd8e8 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1832,9 +1832,10 @@ class TicketSettings(models.Model): event = models.OneToOneField( Event, on_delete=models.CASCADE, related_name="ticket_settings" ) - order_limit = models.IntegerField(null=True, blank=True, default=10) + order_limit = models.IntegerField(default=10, null=True, blank=True) drop_time = models.DateTimeField(null=True, blank=True) fee_charged_to_buyer = models.BooleanField(default=False) + active = models.BooleanField(default=True) def __str__(self): return f"Ticket settings for {self.event.name}" diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 7a1cde9ad..6b2d89843 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2393,6 +2393,9 @@ class ClubEventViewSet(viewsets.ModelViewSet): buyers: Get information about the buyers of an event's ticket + toggle_sales: + Toggle whether ticket sales are active + remove_from_cart: Remove a ticket for this event from cart @@ -2492,6 +2495,13 @@ def add_to_cart(self, request, *args, **kwargs): status=status.HTTP_403_FORBIDDEN, ) + # Can't add tickets if ticket sales are not active + if not event.ticket_settings.active: + return Response( + {"detail": "Ticket sales are not active", "success": False}, + status=status.HTTP_403_FORBIDDEN, + ) + quantities = request.data.get("quantities") if not quantities: return Response( @@ -2499,6 +2509,13 @@ def add_to_cart(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST, ) + for item in quantities: + if item.get("count", 0) <= 0: + return Response( + {"detail": "Ticket quantities must be positive", "success": False}, + status=status.HTTP_400_BAD_REQUEST, + ) + num_requested = sum(item["count"] for item in quantities) num_carted = cart.tickets.filter(event=event).count() @@ -2696,9 +2713,13 @@ def tickets(self, request, *args, **kwargs): """ event = self.get_object() - if not event.has_tickets or ( - event.ticket_settings.drop_time - and timezone.now() < event.ticket_settings.drop_time + if ( + not event.has_tickets + or ( + event.ticket_settings.drop_time + and timezone.now() < event.ticket_settings.drop_time + ) + or not event.ticket_settings.active ): return Response({"totals": [], "available": []}) @@ -3160,6 +3181,81 @@ def email_blast(self, request, *args, **kwargs): } ) + @action(detail=True, methods=["post"]) + def toggle_sales(self, request, *args, **kwargs): + """ + Toggle whether ticket sales are active for this event. + --- + requestBody: + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: Whether ticket sales should be active + responses: + "200": + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + detail: + type: string + active: + type: boolean + "400": + content: + application/json: + schema: + type: object + properties: + detail: + type: string + --- + """ + event = self.get_object() + + if not event.has_tickets: + return Response( + {"detail": "This event does not have any tickets", "success": False}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + event.ticket_settings.drop_time + and timezone.now() < event.ticket_settings.drop_time + ): + return Response( + { + "detail": "Cannot toggle ticket sales before tickets have dropped", + "success": False, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + active = request.data.get("active", None) + if active is None: + return Response( + {"detail": "Active attribute must be specified"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + event.ticket_settings.active = active + event.ticket_settings.save() + + return Response( + { + "success": True, + "detail": f"Ticket sales are now {'active' if active else 'inactive'}", + "active": active, + } + ) + @action(detail=True, methods=["post"]) def upload(self, request, *args, **kwargs): """ diff --git a/backend/tests/clubs/test_ticketing.py b/backend/tests/clubs/test_ticketing.py index ac6def249..bdecdfa93 100644 --- a/backend/tests/clubs/test_ticketing.py +++ b/backend/tests/clubs/test_ticketing.py @@ -615,6 +615,9 @@ def test_add_to_cart_twice_accumulates(self): self.assertEqual(cart.tickets.filter(type="premium").count(), 2, cart.tickets) def test_add_to_cart_order_limit_exceeded(self): + self.ticket_settings.order_limit = 10 + self.ticket_settings.save() + self.client.login(username=self.user1.username, password="test") tickets_to_add = { @@ -783,6 +786,109 @@ def test_delete_event_with_claimed_tickets(self): ) self.assertEqual(resp_bought.status_code, 400, resp_bought.content) + def test_toggle_ticket_sales(self): + """Test toggling ticket sales on and off""" + self.client.login(username=self.user1.username, password="test") + + # Test activating sales + resp = self.client.post( + reverse("club-events-toggle-sales", args=(self.club1.code, self.event1.pk)), + {"active": True}, + format="json", + ) + self.assertEqual(resp.status_code, 200) + self.assertTrue(resp.data["success"]) + self.assertTrue(resp.data["active"]) + + # Verify ticket settings were updated + self.event1.refresh_from_db() + self.assertTrue(self.event1.ticket_settings.active) + + # Test deactivating sales + resp = self.client.post( + reverse("club-events-toggle-sales", args=(self.club1.code, self.event1.pk)), + {"active": False}, + format="json", + ) + self.assertEqual(resp.status_code, 200) + self.assertTrue(resp.data["success"]) + self.assertFalse(resp.data["active"]) + + # Verify ticket settings were updated + self.event1.refresh_from_db() + self.assertFalse(self.event1.ticket_settings.active) + + def test_toggle_ticket_sales_no_tickets(self): + """Test toggling ticket sales for event without tickets""" + self.client.login(username=self.user1.username, password="test") + + # Delete all tickets + Ticket.objects.filter(event=self.event1).delete() + + resp = self.client.post( + reverse("club-events-toggle-sales", args=(self.club1.code, self.event1.pk)), + {"active": True}, + format="json", + ) + self.assertEqual(resp.status_code, 400) + self.assertFalse(resp.data["success"]) + self.assertIn("does not have any tickets", resp.data["detail"]) + + def test_toggle_ticket_sales_before_drop(self): + """Test toggling ticket sales before drop time""" + self.client.login(username=self.user1.username, password="test") + + # Set future drop time + self.event1.ticket_settings.drop_time = timezone.now() + timezone.timedelta( + days=1 + ) + self.event1.ticket_settings.save() + + resp = self.client.post( + reverse("club-events-toggle-sales", args=(self.club1.code, self.event1.pk)), + {"active": True}, + format="json", + ) + self.assertEqual(resp.status_code, 400) + self.assertFalse(resp.data["success"]) + + def test_add_to_cart_inactive_sales(self): + """Test adding tickets to cart when sales are inactive""" + self.client.login(username=self.user1.username, password="test") + + # Deactivate ticket sales + self.event1.ticket_settings.active = False + self.event1.ticket_settings.save() + + tickets_to_add = { + "quantities": [ + {"type": "normal", "count": 1}, + ] + } + resp = self.client.post( + reverse("club-events-add-to-cart", args=(self.club1.code, self.event1.pk)), + tickets_to_add, + format="json", + ) + self.assertEqual(resp.status_code, 403) + self.assertFalse(resp.data["success"]) + self.assertIn("Ticket sales are not active", resp.data["detail"]) + + def test_get_tickets_inactive_sales(self): + """Test getting tickets when sales are inactive""" + self.client.login(username=self.user1.username, password="test") + + # Deactivate ticket sales + self.event1.ticket_settings.active = False + self.event1.ticket_settings.save() + + resp = self.client.get( + reverse("club-events-tickets", args=(self.club1.code, self.event1.pk)) + ) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data["totals"], []) + self.assertEqual(resp.data["available"], []) + @dataclass class MockPaymentResponse: From 80bb21d8ba0e8472e600a2785bfa69ec2fb6f936 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Fri, 17 Jan 2025 17:17:11 -0500 Subject: [PATCH 3/3] Add permissioning for `toggle_sales` route --- backend/clubs/permissions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/clubs/permissions.py b/backend/clubs/permissions.py index ce634fc9a..3511ecd24 100644 --- a/backend/clubs/permissions.py +++ b/backend/clubs/permissions.py @@ -230,6 +230,7 @@ def has_object_permission(self, request, view, obj): "create_tickets", "issue_tickets", "email_blast", + "toggle_sales", ]: if not request.user.is_authenticated: return False