Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ticket drop time UI support, unapproved club event visibility changes #750

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ class Club(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

# signifies the existence of a previous instance within history with approved=True
ghost = models.BooleanField(default=False)
history = HistoricalRecords(cascade_delete_history=True)

Expand Down
9 changes: 9 additions & 0 deletions backend/clubs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,10 +457,18 @@ def validate(self, data):
end_time = data.get(
"end_time", self.instance.end_time if self.instance is not None else None
)
ticket_drop_time = data.get(
"ticket_drop_time",
self.instance.ticket_drop_time if self.instance is not None else None,
)
if start_time is not None and end_time is not None and start_time > end_time:
raise serializers.ValidationError(
"Your event start time must be less than the end time!"
)
if ticket_drop_time is not None and ticket_drop_time > end_time:
raise serializers.ValidationError(
"Your ticket drop time must be before the event ends!"
)
return data

def update(self, instance, validated_data):
Expand Down Expand Up @@ -507,6 +515,7 @@ class Meta:
"location",
"name",
"start_time",
"ticket_drop_time",
"ticketed",
"type",
"url",
Expand Down
93 changes: 77 additions & 16 deletions backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1762,7 +1762,7 @@
"""
serializer = ClubMinimalSerializer(
Club.objects.all()
.exclude(Q(approved=False) | Q(archived=True))
.exclude((~Q(approved=True) & Q(ghost=False)) | Q(archived=True))
.order_by(Lower("name")),
many=True,
)
Expand Down Expand Up @@ -2464,6 +2464,24 @@
---
"""
event = self.get_object()
club = Club.objects.filter(code=event.club.code).first()
# As clubs cannot go from historically approved to unapproved, we can
# check here without checking further on in the checkout process
# (the only exception is archiving a club, which is checked)
if not club:
return Response(

Check warning on line 2472 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2472

Added line #L2472 was not covered by tests
{"detail": "Related club does not exist", "success": False},
status=status.HTTP_404_NOT_FOUND,
)
elif not club.approved and not club.ghost:
return Response(
{
"detail": """This club has not been approved
and cannot sell 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
Expand Down Expand Up @@ -2682,9 +2700,6 @@
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 @@ -3193,24 +3208,60 @@

return super().destroy(request, *args, **kwargs)

def partial_update(self, request, *args, **kwargs):
"""
Do not let club admins modify the ticket drop time
if tickets have already been sold.
"""
event = self.get_object()
if (
"ticket_drop_time" in request.data
and Ticket.objects.filter(event=event, owner__isnull=False).exists()
):
raise DRFValidationError(
detail="""Ticket drop times cannot be edited
after tickets have been sold."""
)
return super().partial_update(request, *args, **kwargs)

def get_queryset(self):
qs = Event.objects.all()
is_club_specific = self.kwargs.get("club_code") is not None
if is_club_specific:
qs = qs.filter(club__code=self.kwargs["club_code"])
qs = qs.filter(
Q(club__approved=True) | Q(type=Event.FAIR) | Q(club__ghost=True),
club__archived=False,
)
# Check if the user is an officer or admin
if not self.request.user.is_authenticated or (
not self.request.user.has_perm("clubs.manage_club")
and not Membership.objects.filter(
person=self.request.user,
club__code=self.kwargs["club_code"],
role__lte=Membership.ROLE_OFFICER,
).exists()
):
qs = qs.filter(
Q(club__approved=True) | Q(type=Event.FAIR) | Q(club__ghost=True),
club__archived=False,
)
else:
qs = qs.filter(
Q(club__approved=True)
| Q(type=Event.FAIR)
| Q(club__ghost=True)
| Q(club__isnull=True),
Q(club__isnull=True) | Q(club__archived=False),
)

if not (
self.request.user.is_authenticated
and self.request.user.has_perm("clubs.manage_club")
):
officer_clubs = (

Check warning on line 3250 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L3250

Added line #L3250 was not covered by tests
Membership.objects.filter(
person=self.request.user, role__lte=Membership.ROLE_OFFICER
).values_list("club", flat=True)
if self.request.user.is_authenticated
else []
)
qs = qs.filter(

Check warning on line 3257 in backend/clubs/views.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L3257

Added line #L3257 was not covered by tests
Q(club__approved=True)
| Q(club__id__in=list(officer_clubs))
| Q(type=Event.FAIR)
| Q(club__ghost=True)
| Q(club__isnull=True),
Q(club__isnull=True) | Q(club__archived=False),
)
return (
qs.select_related("club", "creator")
.prefetch_related(
Expand Down Expand Up @@ -5203,8 +5254,13 @@

tickets_to_replace = cart.tickets.filter(
Q(owner__isnull=False)
| Q(event__club__archived=True)
| Q(holder__isnull=False)
| Q(event__end_time__lt=now)
| (
Q(event__ticket_drop_time__gt=timezone.now())
& Q(event__ticket_drop_time__isnull=False)
)
).exclude(holder=self.request.user)

# In most cases, we won't need to replace, so exit early
Expand Down Expand Up @@ -5241,6 +5297,8 @@
continue

available_tickets = Ticket.objects.filter(
Q(event__ticket_drop_time__lt=timezone.now())
| Q(event__ticket_drop_time__isnull=True),
event=ticket_class["event"],
type=ticket_class["type"],
buyable=True, # should not be triggered as buyable is by ticket class
Expand Down Expand Up @@ -5335,6 +5393,9 @@
# are locked, we shouldn't block.
tickets = cart.tickets.select_for_update(skip_locked=True).filter(
Q(holder__isnull=True) | Q(holder=self.request.user),
Q(event__ticket_drop_time__lt=timezone.now())
| Q(event__ticket_drop_time__isnull=True),
event__club__archived=False,
owner__isnull=True,
buyable=True,
)
Expand Down
116 changes: 101 additions & 15 deletions backend/tests/clubs/test_ticketing.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ def commonSetUp(self):
email="[email protected]",
)

self.unapproved_club = Club.objects.create(
code="unapproved-club",
name="Unapproved Club",
approved=False,
ghost=False,
email="[email protected]",
)

self.event1 = Event.objects.create(
code="test-event",
club=self.club1,
Expand All @@ -59,6 +67,14 @@ def commonSetUp(self):
end_time=timezone.now() + timezone.timedelta(days=3),
)

self.unapproved_event = Event.objects.create(
code="unapproved-event",
club=self.unapproved_club,
name="Unapproved Event",
start_time=timezone.now() + timezone.timedelta(days=2),
end_time=timezone.now() + timezone.timedelta(days=3),
)

self.ticket_totals = [
{"type": "normal", "count": 20, "price": 15.0},
{"type": "premium", "count": 10, "price": 30.0},
Expand All @@ -73,6 +89,11 @@ def commonSetUp(self):
for _ in range(10)
]

self.unapproved_tickets = [
Ticket.objects.create(type="normal", event=self.unapproved_event, price=15.0)
for _ in range(20)
]


class TicketEventTestCase(TestCase):
"""
Expand All @@ -87,6 +108,30 @@ def setUp(self):

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

# Test invalid start_time, ticket_drop_time editing
resp = self.client.patch(
reverse("club-events-detail", args=(self.club1.code, self.event1.pk)),
{
"ticket_drop_time": (
self.event1.end_time + timezone.timedelta(days=20)
).strftime("%Y-%m-%dT%H:%M:%S%z")
},
format="json",
)
self.assertEqual(resp.status_code, 400, resp.content)

resp = self.client.patch(
reverse("club-events-detail", args=(self.club1.code, self.event1.pk)),
{
"start_time": (
self.event1.end_time + timezone.timedelta(days=20)
).strftime("%Y-%m-%dT%H:%M:%S%z")
},
format="json",
)
self.assertEqual(resp.status_code, 400, resp.content)

qts = {
"quantities": [
{"type": "_normal", "count": 20, "price": 10},
Expand Down Expand Up @@ -277,6 +322,18 @@ def test_create_ticket_offerings_already_owned_or_held(self):
)
self.assertEqual(resp.status_code, 403, resp.content)

# Changing ticket drop time should fail
resp = self.client.patch(
reverse("club-events-detail", args=(self.club1.code, self.event1.pk)),
{
"ticket_drop_time": (
timezone.now() + timezone.timedelta(hours=12)
).strftime("%Y-%m-%dT%H:%M:%S%z")
},
format="json",
)
self.assertEqual(resp.status_code, 400, resp.content)

def test_issue_tickets(self):
self.client.login(username=self.user1.username, password="test")
args = {
Expand Down Expand Up @@ -430,21 +487,6 @@ 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 @@ -605,6 +647,50 @@ def test_add_to_cart_before_ticket_drop(self):
# Tickets should not be added to cart before drop time
self.assertEqual(resp.status_code, 403, resp.content)

def test_add_to_cart_unapproved_club(self):
self.client.login(username=self.user1.username, password="test")
tickets_to_add = {
"quantities": [
{"type": "normal", "count": 2},
]
}
resp = self.client.post(
reverse(
"club-events-add-to-cart",
args=(self.unapproved_club.code, self.unapproved_event.pk),
),
tickets_to_add,
format="json",
)
self.assertEqual(resp.status_code, 403, resp.content)
self.client.login(username=self.user2.username, password="test")
resp = self.client.post(
reverse(
"club-events-add-to-cart",
args=(self.unapproved_club.code, self.unapproved_event.pk),
),
tickets_to_add,
format="json",
)
# Cannot see event
self.assertEqual(resp.status_code, 404, resp.content)

def test_add_to_cart_nonexistent_club(self):
tickets_to_add = {
"quantities": [
{"type": "normal", "count": 2},
]
}
resp = self.client.post(
reverse(
"club-events-add-to-cart",
args=("Random club name", self.unapproved_event.pk),
),
tickets_to_add,
format="json",
)
self.assertEqual(resp.status_code, 404, resp.content)

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

Expand Down
8 changes: 8 additions & 0 deletions backend/tests/clubs/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@ def setUp(self):
visibility=Advisor.ADVISOR_VISIBILITY_ALL,
)

def test_directory(self):
"""
Test retrieving the club directory.
"""
resp = self.client.get(reverse("clubs-directory"))
self.assertIn(resp.status_code, [200, 201], resp.content)
self.assertEqual(len(resp.data), 1)

def test_advisor_visibility(self):
"""
Tests each tier of advisor visibility.
Expand Down
Loading
Loading