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

Ticketing #501

Merged
merged 66 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
074fda5
Ticket model
cphalen Dec 13, 2021
4171a61
some basic untested functionality
rohangpta Jan 16, 2022
ef872be
tested routes, added view ticket count route
rohangpta Jan 16, 2022
c55fd75
lint
rohangpta Jan 16, 2022
2d0db0d
email confirmation, qrcode, retrieve ticket
rohangpta Nov 12, 2022
7240a11
remove commented stuff
rohangpta Jan 17, 2022
ae99446
created cart model and add to cart, validate cart, and checkout from …
rohangpta Nov 12, 2022
0a975fb
added holding expiration functionality, holding is initiated at check…
Dfeng6789 Feb 20, 2022
7fe194d
lint and tests
Dfeng6789 Feb 20, 2022
ac0975a
added cart creation for a user upon adding to a cart if a cart does n…
Dfeng6789 Feb 20, 2022
c69254a
slightly changed holding updates, added update to holding status when…
Dfeng6789 Feb 27, 2022
5382f48
atomic transactions, cleanup, and some views
rohangpta Nov 12, 2022
6508a97
holding, validation improvements, perms
rohangpta Jul 6, 2022
9610747
some docstring stuff
rohangpta Oct 16, 2022
c9fb115
rebase + pipfile lock
rohangpta Oct 16, 2022
1b0bda8
use master pipfile.lock
rohangpta Oct 16, 2022
094ad42
lint and pre-commit fixes
rohangpta Oct 16, 2022
d67a3a6
minor bugs
rohangpta Oct 16, 2022
a352b01
add to populate script
rohangpta Nov 12, 2022
7367db1
lint
rohangpta Nov 12, 2022
7f650dc
rebase, fix some documentation
rohangpta Nov 12, 2022
03214af
Update ticketing (backend) branch (#612)
aviupadhyayula Feb 13, 2024
50b9c0b
Revert "Update ticketing (backend) branch (#612)"
julianweng Feb 16, 2024
80e8201
Merge ticketing branches (#615)
julianweng Mar 4, 2024
22968db
Merge master into ticketing
julianweng Mar 4, 2024
91b1df9
Fix TS errors
julianweng Mar 4, 2024
580be09
Update Links to new Next.js behavior
anli5005 Mar 20, 2024
6151c4f
Merge branch 'master' into ticketing
esinx Apr 13, 2024
a40e469
missed merge conflict
esinx Apr 13, 2024
20d250d
migrations
esinx Apr 13, 2024
428dd7e
Ticketing integrate cybersource -> ticketing (#652)
rohangpta Apr 14, 2024
b911872
Set and enforce order limit on ticket purchases (#654)
aviupadhyayula Apr 14, 2024
4af8765
Check ticket holds before completing checkout (#657)
aviupadhyayula Apr 15, 2024
5880d7c
Ticketing price integration (#659)
julianweng Apr 16, 2024
125905a
feat(events): new events page
esinx Apr 16, 2024
a8059d3
todo remove
esinx Apr 16, 2024
c32bbba
Merge branch 'feat/ticketing/cart' into ticketing
esinx Apr 16, 2024
3a8829d
Add support for group discounts (#661)
aviupadhyayula Apr 16, 2024
27ac7c6
Owned Tickets Tab in Settings (#663)
joyliu-q Apr 16, 2024
40fb7f7
Ticketing backend tests (#666)
rohangpta Apr 18, 2024
6f75d48
Rm console.log on frontend
rohangpta Apr 18, 2024
20f68ab
Add Group Discount to Create Ticket Flow and Auto Scroll Down (#669)
joyliu-q Apr 21, 2024
78f02fd
Use capture context in cart checkout (#671)
aviupadhyayula Apr 21, 2024
212720b
fix N+1 query due to attribute
rm03 Apr 22, 2024
66cceb0
Add support for ticket drop times (#672)
aviupadhyayula Apr 23, 2024
d4de8d4
Add ability to transfer ownership of tickets (#653)
rm03 Apr 23, 2024
8847a3b
Ticketing Cart Pre-Checkout (#670)
joyliu-q Apr 23, 2024
3888a5c
Fix breaking changes introduced in #670 (#680)
aviupadhyayula Apr 23, 2024
ad28028
Add guards on ticketed event deletion (#667)
aviupadhyayula Apr 23, 2024
fb4e4cf
Default billing phone number to null
aviupadhyayula Apr 23, 2024
71e1fbc
Basic mobile responsiveness for TicketCard
julianweng Apr 24, 2024
da6abe0
Add route for admins to issue tickets to users (#679)
aviupadhyayula Apr 25, 2024
b57d797
Add route for attendance tracking (#684)
rm03 Apr 25, 2024
14ae31e
Fix ticket interface for sellers (#665)
julianweng Apr 26, 2024
ffea49a
✨ feat(checkout): cybersource integration
esinx Apr 23, 2024
a614997
:art: It's toast time
joyliu-q Apr 26, 2024
5f4716a
🛠️ fix(close): handle modal close with confirm
esinx Apr 26, 2024
4076a4f
✨ feat(success): checkout success message
esinx Apr 26, 2024
21eceff
❌ feat(handle error)
esinx Apr 26, 2024
5d13f9b
🛠️ fix(message): more explicit message for modal close confirmation
esinx Apr 26, 2024
d032c7a
Fix lint, min/max add to cart quantities per class
julianweng Apr 28, 2024
a1b694d
Add support for free tickets (#658)
Porcupine1 Apr 28, 2024
32dd60d
:arrow_down: Downgrade Eslint dependency
joyliu-q Apr 28, 2024
1c4d9ef
Add ability to make tickets not buyable + disable paid tickets for be…
julianweng Apr 28, 2024
ff74f68
Ticketing Beta branding and File Cleanup (#686)
julianweng Apr 29, 2024
e1fac0b
Merge master into ticketing (#687)
aviupadhyayula Apr 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ repos:
hooks:
- id: black
name: black
entry: black
entry: cd backend && pipenv run black
language: python
types: [python]
require_serial: true
Expand Down
4 changes: 4 additions & 0 deletions backend/clubs/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ApplicationSubmission,
Asset,
Badge,
Cart,
Club,
ClubApplication,
ClubFair,
Expand Down Expand Up @@ -48,6 +49,7 @@
TargetStudentType,
TargetYear,
Testimonial,
Ticket,
Year,
ZoomMeetingVisit,
)
Expand Down Expand Up @@ -443,3 +445,5 @@ class ZoomMeetingVisitAdmin(admin.ModelAdmin):
admin.site.register(Year, YearAdmin)
admin.site.register(ZoomMeetingVisit, ZoomMeetingVisitAdmin)
admin.site.register(AdminNote)
admin.site.register(Ticket)
admin.site.register(Cart)
29 changes: 29 additions & 0 deletions backend/clubs/management/commands/populate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ApplicationQuestion,
ApplicationSubmission,
Badge,
Cart,
Club,
ClubApplication,
ClubFair,
Expand All @@ -28,6 +29,7 @@
StudentType,
Tag,
Testimonial,
Ticket,
Year,
)

Expand Down Expand Up @@ -746,4 +748,31 @@ def get_image(url):
first_mship.save()
count += 1

# Add tickets

hr = Club.objects.get(code="harvard-rejects")

hr_events = Event.objects.filter(club=hr)

for idx, e in enumerate(hr_events[:3]):
# Switch up person every so often
person = ben if idx < 2 else user_objs[1]

# Create some unowned tickets
Ticket.objects.bulk_create(
[Ticket(event=e, type="Regular") for _ in range(10)]
)

Ticket.objects.bulk_create(
[Ticket(event=e, type="Premium") for _ in range(5)]
)

# Create some owned tickets and tickets in cart
for i in range((idx + 1) * 10):
if i % 5:
Ticket.objects.create(event=e, owner=person, type="Regular")
else:
c, _ = Cart.objects.get_or_create(owner=person)
c.tickets.add(Ticket.objects.create(event=e, type="Premium"))

self.stdout.write("Finished populating database!")
90 changes: 90 additions & 0 deletions backend/clubs/migrations/0091_cart_ticket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Generated by Django 3.2.8 on 2022-11-12 20:05

import uuid

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("clubs", "0090_adminnote"),
]

operations = [
migrations.CreateModel(
name="Cart",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"owner",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="cart",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="Ticket",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("type", models.CharField(max_length=100)),
("holding_expiration", models.DateTimeField(blank=True, null=True)),
(
"carts",
models.ManyToManyField(
blank=True, related_name="tickets", to="clubs.Cart"
),
),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="tickets",
to="clubs.event",
),
),
(
"holder",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="held_tickets",
to=settings.AUTH_USER_MODEL,
),
),
(
"owner",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="owned_tickets",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
98 changes: 98 additions & 0 deletions backend/clubs/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import base64
import datetime
import os
import re
import uuid
import warnings
from io import BytesIO
from urllib.parse import urlparse

import pytz
import qrcode
import requests
import yaml
from django.conf import settings
Expand Down Expand Up @@ -941,6 +944,10 @@
def __str__(self):
return self.name

@property
def tickets_count(self):
return Ticket.objects.count(event=self)

Check warning on line 949 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L949

Added line #L949 was not covered by tests


class Favorite(models.Model):
"""
Expand Down Expand Up @@ -1695,6 +1702,97 @@
response = models.TextField(blank=True)


class Cart(models.Model):
"""
Represents an instance of a ticket cart for a user
"""

owner = models.OneToOneField(
get_user_model(), related_name="cart", on_delete=models.CASCADE
)


class TicketManager(models.Manager):

# Update holds for all tickets
def update_holds(self):
expired_tickets = self.select_for_update().filter(

Check warning on line 1719 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1719

Added line #L1719 was not covered by tests
holder__isnull=False, holding_expiration__lte=timezone.now()
)
with transaction.atomic():
for ticket in expired_tickets:
ticket.holder = None
self.bulk_update(expired_tickets, ["holder"])

Check warning on line 1725 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1722-L1725

Added lines #L1722 - L1725 were not covered by tests


class Ticket(models.Model):
"""
Represents an instance of a ticket for an event
"""

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
event = models.ForeignKey(
Event, related_name="tickets", on_delete=models.DO_NOTHING
)
type = models.CharField(max_length=100)
owner = models.ForeignKey(
get_user_model(),
related_name="owned_tickets",
on_delete=models.SET_NULL,
blank=True,
null=True,
)
holder = models.ForeignKey(
get_user_model(),
related_name="held_tickets",
on_delete=models.SET_NULL,
blank=True,
null=True,
)
holding_expiration = models.DateTimeField(null=True, blank=True)
carts = models.ManyToManyField(Cart, related_name="tickets", blank=True)
objects = TicketManager()

def get_qr(self):
"""
Return a QR code image linking to the ticket page
"""
if not self.owner:
return None

Check warning on line 1761 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1760-L1761

Added lines #L1760 - L1761 were not covered by tests

url = f"https://{settings.DOMAIN}/api/tickets/{self.id}"
qr_image = qrcode.make(url, box_size=20, border=0)
return qr_image

Check warning on line 1765 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1763-L1765

Added lines #L1763 - L1765 were not covered by tests

def send_confirmation_email(self):
"""
Send a confirmation email to the ticket owner after purchase
"""
owner = self.owner

Check warning on line 1771 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1771

Added line #L1771 was not covered by tests

output = BytesIO()
qr_image = self.get_qr()
qr_image.save(output, format="PNG")
decoded_image = base64.b64encode(output.getvalue()).decode("ascii")

Check warning on line 1776 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1773-L1776

Added lines #L1773 - L1776 were not covered by tests

context = {

Check warning on line 1778 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1778

Added line #L1778 was not covered by tests
"first_name": self.owner.first_name,
"name": self.event.name,
"type": self.type,
"start_time": self.event.start_time,
"end_time": self.event.end_time,
"qr": decoded_image,
}

if self.owner.email:
send_mail_helper(

Check warning on line 1788 in backend/clubs/models.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/models.py#L1787-L1788

Added lines #L1787 - L1788 were not covered by tests
name="ticket_confirmation",
subject=f"Ticket confirmation for {owner.get_full_name()}",
emails=[owner.email],
context=context,
)


@receiver(models.signals.pre_delete, sender=Asset)
def asset_delete_cleanup(sender, instance, **kwargs):
if instance.file:
Expand Down
10 changes: 8 additions & 2 deletions backend/clubs/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@

class EventPermission(permissions.BasePermission):
"""
Officers and above can create/update/delete events.
Officers and above can create/update/delete events and view ticket buyers.
Everyone else can view and list events.
"""

Expand Down Expand Up @@ -224,7 +224,13 @@

if not old_type == FAIR_TYPE and new_type == FAIR_TYPE:
return False

elif view.action in ["buyers", "create_tickets"]:
if not request.user.is_authenticated:
return False
membership = find_membership_helper(request.user, obj.club)
return membership is not None and membership.role <= Membership.ROLE_OFFICER

Check warning on line 231 in backend/clubs/permissions.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/permissions.py#L228-L231

Added lines #L228 - L231 were not covered by tests
elif view.action in ["add_to_cart", "remove_from_cart"]:
return request.user.is_authenticated

Check warning on line 233 in backend/clubs/permissions.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/permissions.py#L233

Added line #L233 was not covered by tests
return True


Expand Down
23 changes: 23 additions & 0 deletions backend/clubs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
TargetStudentType,
TargetYear,
Testimonial,
Ticket,
Year,
)
from clubs.utils import clean
Expand Down Expand Up @@ -339,8 +340,12 @@
image_url = serializers.SerializerMethodField("get_image_url")
large_image_url = serializers.SerializerMethodField("get_large_image_url")
url = serializers.SerializerMethodField("get_event_url")
ticketed = serializers.SerializerMethodField("get_ticketed")
creator = serializers.HiddenField(default=serializers.CurrentUserDefault())

def get_ticketed(self, obj) -> bool:
return obj.tickets.count() > 0

def get_event_url(self, obj):
# if no url, return that
if not obj.url:
Expand Down Expand Up @@ -478,6 +483,7 @@
"location",
"name",
"start_time",
"ticketed",
"type",
"url",
]
Expand Down Expand Up @@ -1721,6 +1727,23 @@
fields = ("club", "role", "title", "active", "public")


class TicketSerializer(serializers.ModelSerializer):

"""
Used to return a ticket object
"""

owner = serializers.SerializerMethodField("get_owner_name")
event = EventSerializer()

def get_owner_name(self, obj):
return obj.owner.get_full_name() if obj.owner else "None"

Check warning on line 1740 in backend/clubs/serializers.py

View check run for this annotation

Codecov / codecov/patch

backend/clubs/serializers.py#L1740

Added line #L1740 was not covered by tests

class Meta:
model = Ticket
fields = ("id", "event", "type", "owner")


class UserUUIDSerializer(serializers.ModelSerializer):
"""
Used to get the uuid of a user (for ICS Calendar export)
Expand Down
2 changes: 2 additions & 0 deletions backend/clubs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
SubscribeViewSet,
TagViewSet,
TestimonialViewSet,
TicketViewSet,
UserGroupAPIView,
UserPermissionAPIView,
UserUpdateAPIView,
Expand All @@ -67,6 +68,7 @@
router.register(r"searches", SearchQueryViewSet, basename="searches")
router.register(r"memberships", MembershipViewSet, basename="members")
router.register(r"requests", MembershipRequestViewSet, basename="requests")
router.register(r"tickets", TicketViewSet, basename="tickets")

router.register(r"schools", SchoolViewSet, basename="schools")
router.register(r"majors", MajorViewSet, basename="majors")
Expand Down
Loading
Loading