Skip to content

Commit

Permalink
Merge branch 'ticketing' into transfer-tickets
Browse files Browse the repository at this point in the history
  • Loading branch information
rm03 committed Apr 23, 2024
2 parents d819fbc + 66cceb0 commit 95d3740
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 10 deletions.
1 change: 1 addition & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ numpy = "*"
inflection = "*"
cybersource-rest-client-python = "*"
pyjwt = "*"
freezegun = "*"

[requires]
python_version = "3.11"
11 changes: 10 additions & 1 deletion backend/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions backend/clubs/migrations/0105_event_ticket_drop_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-04-21 04:35

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("clubs", "0104_cart_checkout_context"),
]

operations = [
migrations.AddField(
model_name="event",
name="ticket_drop_time",
field=models.DateTimeField(blank=True, null=True),
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class Migration(migrations.Migration):
dependencies = [
("clubs", "0104_cart_checkout_context"),
("clubs", "0105_event_ticket_drop_time"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

Expand Down
1 change: 1 addition & 0 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ class Event(models.Model):
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
Expand Down
65 changes: 64 additions & 1 deletion backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2392,6 +2392,13 @@ def add_to_cart(self, request, *args, **kwargs):
event = self.get_object()
cart, _ = Cart.objects.get_or_create(owner=self.request.user)

# Cannot add tickets that haven't dropped yet
if event.ticket_drop_time and timezone.now() < event.ticket_drop_time:
return Response(
{"detail": "Ticket drop time has not yet elapsed"},
status=status.HTTP_403_FORBIDDEN,
)

quantities = request.data.get("quantities")
if not quantities:
return Response(
Expand Down Expand Up @@ -2567,6 +2574,9 @@ 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:
return Response({"totals": [], "available": []})

# Take price of first ticket of given type for now
totals = (
tickets.values("type")
Expand Down Expand Up @@ -2617,6 +2627,10 @@ def create_tickets(self, request, *args, **kwargs):
order_limit:
type: int
required: false
drop_time:
type: string
format: date-time
required: false
responses:
"200":
content:
Expand All @@ -2634,10 +2648,39 @@ def create_tickets(self, request, *args, **kwargs):
properties:
detail:
type: string
"403":
content:
application/json:
schema:
type: object
properties:
detail:
type: string
---
"""
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:
return Response(
{"detail": "Tickets cannot be edited after they have dropped"},
status=status.HTTP_403_FORBIDDEN,
)

# Tickets can't be edited after they've been sold
if (
Ticket.objects.filter(event=event)
.filter(Q(owner__isnull=False) | Q(holder__isnull=False))
.exists()
):
return Response(
{
"detail": "Tickets cannot be edited after they have been "
"sold or checked out"
},
status=status.HTTP_403_FORBIDDEN,
)

quantities = request.data.get("quantities", [])
if not quantities:
return Response(
Expand Down Expand Up @@ -2707,6 +2750,25 @@ def create_tickets(self, request, *args, **kwargs):
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()

return Response({"detail": "Successfully created tickets"})

@action(detail=True, methods=["post"])
Expand Down Expand Up @@ -2880,7 +2942,8 @@ def get_queryset(self):
if "fair" in self.request.query_params
else Badge.objects.filter(visible=True)
),
)
),
"tickets",
)
.order_by("start_time")
)
Expand Down
132 changes: 125 additions & 7 deletions backend/tests/clubs/test_ticketing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
from datetime import timedelta
from unittest.mock import patch

import freezegun
from django.contrib.auth import get_user_model
from django.db.models import (
Count,
)
from django.db.models import Count
from django.db.models.deletion import ProtectedError
from django.test import TestCase
from django.urls import reverse
Expand Down Expand Up @@ -163,6 +162,89 @@ def test_create_ticket_offerings_bad_data(self):
self.assertIn(resp.status_code, [400], resp.content)
self.assertEqual(Ticket.objects.filter(type__contains="_").count(), 0, data)

def test_create_ticket_offerings_delay_drop(self):
self.client.login(username=self.user1.username, password="test")

args = {
"quantities": [
{"type": "_normal", "count": 20, "price": 10},
{"type": "_premium", "count": 10, "price": 20},
],
"drop_time": (timezone.now() + timezone.timedelta(hours=12)).strftime(
"%Y-%m-%dT%H:%M:%S%z"
),
}
_ = self.client.put(
reverse("club-events-tickets", args=(self.club1.code, self.event1.pk)),
args,
format="json",
)

self.event1.refresh_from_db()

# Drop time should be set
self.assertIsNotNone(self.event1.ticket_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)
self.assertTrue(diff < timezone.timedelta(minutes=5))

# Move Django's internal clock 13 hours forward
with freezegun.freeze_time(timezone.now() + timezone.timedelta(hours=13)):
resp = self.client.put(
reverse("club-events-tickets", args=(self.club1.code, self.event1.pk)),
args,
format="json",
)

# Tickets shouldn't be editable after drop time has elapsed
self.assertEqual(resp.status_code, 403, resp.content)

def test_create_ticket_offerings_already_owned_or_held(self):
self.client.login(username=self.user1.username, password="test")

# Create ticket offerings
args = {
"quantities": [
{"type": "_normal", "count": 5, "price": 10},
{"type": "_premium", "count": 3, "price": 20},
],
}
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)

# Simulate checkout by applying holds
for ticket in Ticket.objects.filter(type="_normal"):
ticket.holder = self.user1
ticket.save()

# Recreating tickets should fail
resp = self.client.put(
reverse("club-events-tickets", args=(self.club1.code, self.event1.pk)),
args,
format="json",
)
self.assertEqual(resp.status_code, 403, resp.content)

# Simulate purchase by transferring ownership
for ticket in Ticket.objects.filter(type="_normal", holder=self.user1):
ticket.owner = self.user1
ticket.holder = None
ticket.save()

# Recreating tickets should fail
resp = self.client.put(
reverse("club-events-tickets", args=(self.club1.code, self.event1.pk)),
args,
format="json",
)
self.assertEqual(resp.status_code, 403, resp.content)

def test_get_tickets_information_no_tickets(self):
# Delete all the tickets
Ticket.objects.all().delete()
Expand Down Expand Up @@ -194,6 +276,21 @@ def test_get_tickets_information(self):
data["available"],
)

def test_get_tickets_before_drop_time(self):
self.event1.ticket_drop_time = timezone.now() + timedelta(days=1)
self.event1.save()

self.client.login(username=self.user1.username, password="test")
resp = self.client.get(
reverse("club-events-tickets", args=(self.club1.code, self.event1.pk)),
)
self.assertEqual(resp.status_code, 200, resp.content)
data = resp.json()

# Tickets shouldn't be available before the drop time
self.assertEqual(data["totals"], [])
self.assertEqual(data["available"], [])

def test_get_tickets_buyers(self):
self.client.login(username=self.user1.username, password="test")

Expand Down Expand Up @@ -312,6 +409,27 @@ def test_add_to_cart_tickets_unavailable(self):
"Not enough tickets of type normal left!", resp.data["detail"], resp.data
)

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()

tickets_to_add = {
"quantities": [
{"type": "normal", "count": 2},
]
}
resp = self.client.post(
reverse("club-events-add-to-cart", args=(self.club1.code, self.event1.pk)),
tickets_to_add,
format="json",
)

# Tickets should not be added to cart before drop time
self.assertEqual(resp.status_code, 403, resp.content)

def test_remove_from_cart(self):
self.client.login(username=self.user1.username, password="test")

Expand Down Expand Up @@ -798,7 +916,7 @@ def test_complete_checkout(self):
self.assertIn(resp.status_code, [200, 201], resp.content)
self.assertIn("Payment successful", resp.data["detail"], resp.data)

# Ownership transfered
# Ownership transferred
owned_tickets = Ticket.objects.filter(owner=self.user1)
self.assertEqual(owned_tickets.count(), 2, owned_tickets)

Expand Down Expand Up @@ -846,7 +964,7 @@ def test_complete_checkout_stale_cart(self):
self.assertEqual(resp.status_code, 403, resp.content)
self.assertIn("Cart is stale", resp.data["detail"], resp.content)

# Ownership not transfered
# Ownership not transferred
owned_tickets = Ticket.objects.filter(owner=self.user1)
self.assertEqual(owned_tickets.count(), 0, owned_tickets)

Expand Down Expand Up @@ -880,7 +998,7 @@ def test_complete_checkout_validate_token_fails(self):
self.assertEqual(resp.status_code, 500, resp.content)
self.assertIn("Validation failed", resp.data["detail"], resp.content)

# Ownership not transfered
# Ownership not transferred
owned_tickets = Ticket.objects.filter(owner=self.user1)
self.assertEqual(owned_tickets.count(), 0, owned_tickets)

Expand Down Expand Up @@ -923,7 +1041,7 @@ def test_complete_checkout_cybersource_fails(self):
self.assertIn("Transaction failed", resp.data["detail"], resp.content)
self.assertIn("HTTP status 400", resp.data["detail"], resp.content)

# Ownership not transfered
# Ownership not transferred
owned_tickets = Ticket.objects.filter(owner=self.user1)
self.assertEqual(owned_tickets.count(), 0, owned_tickets)

Expand Down

0 comments on commit 95d3740

Please sign in to comment.