diff --git a/backend/clubs/migrations/0118_ticket_description.py b/backend/clubs/migrations/0118_ticket_description.py new file mode 100644 index 000000000..24692ccc0 --- /dev/null +++ b/backend/clubs/migrations/0118_ticket_description.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2025-01-02 00:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clubs", "0117_clubapprovalresponsetemplate"), + ] + + operations = [ + migrations.AddField( + model_name="ticket", + name="description", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 22105c7a0..174ba5d24 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1879,6 +1879,7 @@ class Ticket(models.Model): blank=True, null=True, ) + description = models.TextField(null=True, blank=True) holding_expiration = models.DateTimeField(null=True, blank=True, db_index=True) carts = models.ManyToManyField(Cart, related_name="tickets", blank=True) price = models.DecimalField(max_digits=5, decimal_places=2) diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index fc9f1a095..22431b31e 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -1794,7 +1794,7 @@ def get_owner_name(self, obj): class Meta: model = Ticket - fields = ("id", "event", "type", "owner", "attended", "price") + fields = ("id", "event", "description", "type", "owner", "attended", "price") class UserUUIDSerializer(serializers.ModelSerializer): diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 6d03c9607..b6594ba7f 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -2724,6 +2724,9 @@ def create_tickets(self, request, *args, **kwargs): type: integer price: type: number + description: + type: string + required: false group_size: type: number required: false @@ -2847,6 +2850,7 @@ def create_tickets(self, request, *args, **kwargs): event=event, type=item["type"], price=item.get("price", 0), + description=item.get("description", None), group_discount=item.get("group_discount", 0), group_size=item.get("group_size", None), transferable=item.get("transferable", True), @@ -5206,7 +5210,7 @@ def _calculate_cart_total(cart) -> float: def partial_update(self, request, *args, **kwargs): """ - Update a ticket's attendance (only accessible by club officers) + Update a ticket's description or attendance (only accessible by club officers) --- requestBody: content: @@ -5216,6 +5220,8 @@ def partial_update(self, request, *args, **kwargs): properties: attended: type: boolean + description: + type: string responses: "200": content: @@ -5232,16 +5238,38 @@ def partial_update(self, request, *args, **kwargs): type: string --- """ - attended = request.data.get("attended") - if attended is None or not isinstance(attended, bool): - return Response( - {"detail": "Missing boolean attribute 'attended'."}, - status=status.HTTP_400_BAD_REQUEST, - ) ticket = self.get_object() - ticket.attended = attended - ticket.save() - return Response(TicketSerializer(ticket).data) + + # Get request data + attended = request.data.get("attended") + description = request.data.get("description") + + # attendance update + if attended is not None: + if not isinstance(attended, bool): + return Response( + {"detail": "Missing boolean attribute 'attended'."}, + status=status.HTTP_400_BAD_REQUEST, + ) + ticket.attended = attended + ticket.save() + return Response(TicketSerializer(ticket).data) + + # description update + if description is not None: + if ticket.owner is not None: + return Response( + {"detail": "Cannot update description of a sold ticket."}, + status=status.HTTP_400_BAD_REQUEST, + ) + ticket.description = description + ticket.save() + return Response(TicketSerializer(ticket).data) + + return Response( + {"detail": "No valid update parameters provided."}, + status=status.HTTP_400_BAD_REQUEST, + ) @transaction.atomic @update_holds diff --git a/backend/tests/clubs/test_ticketing.py b/backend/tests/clubs/test_ticketing.py index 8f047b9d8..41734fdf6 100644 --- a/backend/tests/clubs/test_ticketing.py +++ b/backend/tests/clubs/test_ticketing.py @@ -1660,6 +1660,100 @@ def test_update_attendance_non_officer(self): self.assertEqual(resp.status_code, 404, resp.content) self.assertFalse(ticket.attended) + def test_create_ticket_with_description(self): + self.client.login(username=self.user1.username, password="test") + qts = { + "quantities": [ + { + "type": "_normal", + "count": 20, + "price": 10, + "description": "This is a normal ticket.", + }, + { + "type": "_premium", + "count": 10, + "price": 20, + "description": "This is a premium ticket.", + }, + ] + } + + resp = self.client.put( + reverse("club-events-tickets", args=(self.club1.code, self.event1.pk)), + qts, + format="json", + ) + + normal_tickets = Ticket.objects.filter(event=self.event1, type="_normal") + premium_tickets = Ticket.objects.filter(event=self.event1, type="_premium") + + self.assertEqual(resp.status_code, 200, resp.content) + self.assertEqual(normal_tickets.first().description, "This is a normal ticket.") + self.assertEqual( + premium_tickets.first().description, "This is a premium ticket." + ) + + def test_update_ticket_description(self): + self.client.login(username=self.user1.username, password="test") + Membership.objects.create( + person=self.user1, + club=self.club1, + title="Officer", + role=Membership.ROLE_OFFICER, + ) + ticket = self.tickets1[0] + ticket.save() + + resp = self.client.patch( + reverse("tickets-detail", args=(ticket.id,)), + {"description": "This is a test description."}, + format="json", + ) + + ticket.refresh_from_db() + self.assertEqual(resp.status_code, 200, resp.content) + self.assertEqual(ticket.description, "This is a test description.") + + def test_update_description_sold_ticket(self): + self.client.login(username=self.user1.username, password="test") + Membership.objects.create( + person=self.user1, + club=self.club1, + title="Officer", + role=Membership.ROLE_OFFICER, + ) + ticket = self.tickets1[0] + # ticket was sold to user2 + ticket.owner = self.user2 + ticket.save() + + resp = self.client.patch( + reverse("tickets-detail", args=(ticket.id,)), + {"description": "This is a test description."}, + format="json", + ) + + ticket.refresh_from_db() + self.assertEqual(resp.status_code, 400, resp.content) + self.assertIsNone(ticket.description) + + def test_update_ticket_description_non_officer(self): + # user1 is no longer an officer for the ticket's club + self.client.login(username=self.user1.username, password="test") + ticket = self.tickets1[0] + ticket.owner = self.user1 + ticket.save() + + resp = self.client.patch( + reverse("tickets-detail", args=(ticket.id,)), + {"description": "This is a test description."}, + format="json", + ) + ticket.refresh_from_db() + self.assertEqual(resp.status_code, 404, resp.content) + self.assertFalse(ticket.attended) + class TicketModelTestCase(TestCase): """