Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
  • Loading branch information
owlester12 committed Oct 18, 2024
2 parents a1b1b2e + 715ba15 commit 0026954
Show file tree
Hide file tree
Showing 22 changed files with 556 additions and 32 deletions.
6 changes: 6 additions & 0 deletions backend/clubs/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Cart,
Club,
ClubApplication,
ClubApprovalResponseTemplate,
ClubFair,
ClubFairBooth,
ClubFairRegistration,
Expand Down Expand Up @@ -415,6 +416,10 @@ class ApplicationSubmissionAdmin(admin.ModelAdmin):
list_display = ("user", "id", "created_at", "status")


class ClubApprovalResponseTemplateAdmin(admin.ModelAdmin):
search_fields = ("title", "content")


admin.site.register(Asset)
admin.site.register(ApplicationCommittee)
admin.site.register(ApplicationExtension)
Expand Down Expand Up @@ -460,3 +465,4 @@ class ApplicationSubmissionAdmin(admin.ModelAdmin):
admin.site.register(TicketTransferRecord)
admin.site.register(Cart)
admin.site.register(ApplicationCycle)
admin.site.register(ClubApprovalResponseTemplate, ClubApprovalResponseTemplateAdmin)
42 changes: 42 additions & 0 deletions backend/clubs/migrations/0117_clubapprovalresponsetemplate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 5.0.4 on 2024-10-16 02:18

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("clubs", "0116_alter_club_approved_on_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="ClubApprovalResponseTemplate",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=255, unique=True)),
("content", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"author",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="templates",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
18 changes: 18 additions & 0 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1998,6 +1998,24 @@ def send_confirmation_email(self):
)


class ClubApprovalResponseTemplate(models.Model):
"""
Represents a (rejection) template for site administrators to use
during the club approval process.
"""

author = models.ForeignKey(
get_user_model(), on_delete=models.SET_NULL, null=True, related_name="templates"
)
title = models.CharField(max_length=255, unique=True)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return self.title


@receiver(models.signals.pre_delete, sender=Asset)
def asset_delete_cleanup(sender, instance, **kwargs):
if instance.file:
Expand Down
37 changes: 28 additions & 9 deletions backend/clubs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
Badge,
Club,
ClubApplication,
ClubApprovalResponseTemplate,
ClubFair,
ClubFairBooth,
ClubVisit,
Expand Down Expand Up @@ -2974,8 +2975,7 @@ class Meta:
)


class AdminNoteSerializer(serializers.ModelSerializer):
club = serializers.SlugRelatedField(queryset=Club.objects.all(), slug_field="code")
class AdminNoteSerializer(ClubRouteMixin, serializers.ModelSerializer):
creator = serializers.SerializerMethodField("get_creator")
title = serializers.CharField(max_length=255, default="Note")
content = serializers.CharField(required=False)
Expand All @@ -2984,20 +2984,39 @@ def get_creator(self, obj):
return obj.creator.get_full_name()

def create(self, validated_data):
return AdminNote.objects.create(
creator=self.context["request"].user,
club=validated_data["club"],
title=validated_data["title"],
content=validated_data["content"],
)
validated_data["creator"] = self.context["request"].user
return super().create(validated_data)

def update(self, instance, validated_data):
validated_data.pop("creator", "")
return super().update(instance, validated_data)

class Meta:
model = AdminNote
fields = ("id", "creator", "club", "title", "content", "created_at")
fields = ("id", "creator", "title", "content", "created_at")


class WritableClubFairSerializer(ClubFairSerializer):
time = serializers.CharField(required=False, allow_blank=True)

class Meta(ClubFairSerializer.Meta):
pass


class ClubApprovalResponseTemplateSerializer(serializers.ModelSerializer):
author = serializers.SerializerMethodField("get_author")

def get_author(self, obj):
return obj.author.get_full_name()

def create(self, validated_data):
validated_data["author"] = self.context["request"].user
return super().create(validated_data)

def update(self, instance, validated_data):
validated_data.pop("author", "")
return super().update(instance, validated_data)

class Meta:
model = ClubApprovalResponseTemplate
fields = ("id", "author", "title", "content", "created_at", "updated_at")
2 changes: 2 additions & 0 deletions backend/clubs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BadgeClubViewSet,
BadgeViewSet,
ClubApplicationViewSet,
ClubApprovalResponseTemplateViewSet,
ClubBoothsViewSet,
ClubEventViewSet,
ClubFairViewSet,
Expand Down Expand Up @@ -92,6 +93,7 @@
basename="wharton",
)
router.register(r"submissions", ApplicationSubmissionUserViewSet, basename="submission")
router.register(r"templates", ClubApprovalResponseTemplateViewSet, basename="templates")

clubs_router = routers.NestedSimpleRouter(router, r"clubs", lookup="club")
clubs_router.register(r"members", MemberViewSet, basename="club-members")
Expand Down
53 changes: 47 additions & 6 deletions backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
Cart,
Club,
ClubApplication,
ClubApprovalResponseTemplate,
ClubFair,
ClubFairBooth,
ClubFairRegistration,
Expand Down Expand Up @@ -158,6 +159,7 @@
AuthenticatedMembershipSerializer,
BadgeSerializer,
ClubApplicationSerializer,
ClubApprovalResponseTemplateSerializer,
ClubBoothSerializer,
ClubConstitutionSerializer,
ClubFairSerializer,
Expand Down Expand Up @@ -2418,10 +2420,17 @@ def add_to_cart(self, request, *args, **kwargs):
event = self.get_object()
cart, _ = Cart.objects.get_or_create(owner=self.request.user)

# Check if the event has already ended
if event.end_time < timezone.now():
return Response(
{"detail": "This event has already ended", "success": False},
status=status.HTTP_403_FORBIDDEN,
)

# 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"},
{"detail": "Ticket drop time has not yet elapsed", "success": False},
status=status.HTTP_403_FORBIDDEN,
)

Expand Down Expand Up @@ -2465,7 +2474,10 @@ def add_to_cart(self, request, *args, **kwargs):

if tickets.count() < count:
return Response(
{"detail": f"Not enough tickets of type {type} left!"},
{
"detail": f"Not enough tickets of type {type} left!",
"success": False,
},
status=status.HTTP_403_FORBIDDEN,
)
cart.tickets.add(*tickets[:count])
Expand Down Expand Up @@ -5141,8 +5153,12 @@ def cart(self, request, *args, **kwargs):
owner=self.request.user
)

now = timezone.now()

tickets_to_replace = cart.tickets.filter(
Q(owner__isnull=False) | Q(holder__isnull=False)
Q(owner__isnull=False)
| Q(holder__isnull=False)
| Q(event__end_time__lt=now)
).exclude(holder=self.request.user)

# In most cases, we won't need to replace, so exit early
Expand All @@ -5154,16 +5170,30 @@ def cart(self, request, *args, **kwargs):
},
)

# Attempt to replace all tickets that have gone stale
# Attempt to replace all tickets that have gone stale or are for elapsed events
replacement_tickets, sold_out_tickets = [], []

tickets_in_cart = cart.tickets.values_list("id", flat=True)
tickets_to_replace = tickets_to_replace.select_related("event")

for ticket_class in tickets_to_replace.values(
"type", "event", "event__name"
"type", "event", "event__name", "event__end_time"
).annotate(count=Count("id")):
# we don't need to lock, since we aren't updating holder/owner
if ticket_class["event__end_time"] < now:
# Event has elapsed, mark all tickets as sold out
sold_out_tickets.append(
{
"type": ticket_class["type"],
"event": {
"id": ticket_class["event"],
"name": ticket_class["event__name"],
},
"count": ticket_class["count"],
}
)
continue

available_tickets = Ticket.objects.filter(
event=ticket_class["event"],
type=ticket_class["type"],
Expand Down Expand Up @@ -7382,7 +7412,18 @@ class AdminNoteViewSet(viewsets.ModelViewSet):
http_method_names = ["get", "post", "put", "patch", "delete"]

def get_queryset(self):
return AdminNote.objects.filter(club__code=self.kwargs.get("club_code"))
return AdminNote.objects.filter(
club__code=self.kwargs.get("club_code")
).order_by("-created_at")


class ClubApprovalResponseTemplateViewSet(viewsets.ModelViewSet):
serializer_class = ClubApprovalResponseTemplateSerializer
permission_classes = [IsSuperuser]
lookup_field = "id"

def get_queryset(self):
return ClubApprovalResponseTemplate.objects.all().order_by("-created_at")


class ScriptExecutionView(APIView):
Expand Down
55 changes: 55 additions & 0 deletions backend/tests/clubs/test_ticketing.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,27 @@ def test_add_to_cart(self):
self.assertEqual(cart.tickets.filter(type="normal").count(), 2, cart.tickets)
self.assertEqual(cart.tickets.filter(type="premium").count(), 1, cart.tickets)

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

# Set the event end time to the past
self.event1.end_time = timezone.now() - timezone.timedelta(days=1)
self.event1.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, resp.content)
self.assertIn("This event has already ended", resp.data["detail"], resp.data)

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

Expand Down Expand Up @@ -948,6 +969,40 @@ def test_get_cart_replacement_required_sold_out(self):
to_add = set(map(lambda t: str(t.id), tickets_to_add))
self.assertEqual(len(in_cart & to_add), 0, in_cart | to_add)

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

# Add a few tickets
cart, _ = Cart.objects.get_or_create(owner=self.user1)
tickets_to_add = self.tickets1[:5]
for ticket in tickets_to_add:
cart.tickets.add(ticket)
cart.save()

# Set the event end time to the past
self.event1.end_time = timezone.now() - timezone.timedelta(days=1)
self.event1.save()

resp = self.client.get(reverse("tickets-cart"), format="json")
data = resp.json()

# The cart should now be empty
self.assertEqual(len(data["tickets"]), 0, data)

# All tickets should be in the sold out array
self.assertEqual(len(data["sold_out"]), 1, data)

expected_sold_out = {
"type": self.tickets1[0].type,
"event": {
"id": self.event1.id,
"name": self.event1.name,
},
"count": 5,
}
for key, val in expected_sold_out.items():
self.assertEqual(data["sold_out"][0][key], val, data)

def test_place_hold_on_tickets(self):
from clubs.views import TicketViewSet

Expand Down
Loading

0 comments on commit 0026954

Please sign in to comment.