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 price integration #659

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 31 additions & 3 deletions backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
DurationField,
ExpressionWrapper,
F,
Max,
Prefetch,
Q,
Sum,
Expand Down Expand Up @@ -2542,6 +2543,8 @@
type: string
count:
type: integer
price:
type: number
available:
type: array
items:
Expand All @@ -2551,15 +2554,24 @@
type: string
count:
type: integer
price:
type: number
---
"""
event = self.get_object()
tickets = Ticket.objects.filter(event=event)

totals = tickets.values("type").annotate(count=Count("type")).order_by("type")
# Take price of first ticket of given type for now
totals = (

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2565

Added line #L2565 was not covered by tests
tickets.values("type")
.annotate(price=Max("price"))
.annotate(count=Count("type"))
.order_by("type")
)
available = (
tickets.filter(owner__isnull=True, holder__isnull=True)
.values("type")
.annotate(price=Max("price"))
.annotate(count=Count("type"))
.order_by("type")
)
Expand All @@ -2586,6 +2598,8 @@
type: string
count:
type: integer
price:
type: number
order_limit:
type: int
required: false
Expand All @@ -2598,17 +2612,31 @@
properties:
detail:
type: string
"400":
content:
application/json:
schema:
type: object
properties:
detail:
type: string
---
"""
event = self.get_object()

quantities = request.data.get("quantities")

# Atomicity ensures idempotency
# Ticket prices must be non-negative
if any(item.get("price", 0) < 0 for item in quantities):
return Response(

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

View check run for this annotation

Codecov / codecov/patch

backend/clubs/views.py#L2630-L2631

Added lines #L2630 - L2631 were not covered by tests
{"detail": "Ticket price cannot be negative."},
status=status.HTTP_400_BAD_REQUEST,
)

# Atomicity ensures idempotency
Ticket.objects.filter(event=event).delete() # Idempotency
tickets = [
Ticket(event=event, type=item["type"])
Ticket(event=event, type=item["type"], price=item["price"])
for item in quantities
for _ in range(item["count"])
]
Expand Down
68 changes: 50 additions & 18 deletions frontend/components/ClubEditPage/TicketsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,32 @@ const TicketItem = ({
ticket,
changeName,
changeCount,
changePrice,
deleteTicket,
deletable,
index,
}): ReactElement => {
const [name, setName] = useState(ticket.name)
const [count, setCount] = useState(ticket.count)
const [price, setPrice] = useState(ticket.price)

const handleNameChange = (e) => {
setName(e.target.value)
changeName(e.target.value, index)
}

const handleCountChange = (e) => {
setCount(e.target.value)
changeCount(e.target.value, index)
let rounded = Math.round(parseFloat(e.target.value))
rounded = rounded < 0 ? 0 : rounded
setCount(rounded.toString())
changeCount(rounded.toString(), index)
}

const handlePriceChange = (e) => {
let rounded = Math.round(parseFloat(e.target.value) * 100) / 100
rounded = rounded < 0 ? 0 : rounded
setPrice(rounded.toString())
changePrice(rounded.toString(), index)
}

return (
Expand All @@ -95,6 +106,13 @@ const TicketItem = ({
placeholder="Ticket Count"
onChange={handleCountChange}
/>
<Input
type="number"
className="input"
value={price}
placeholder="Ticket Price"
onChange={handlePriceChange}
/>
<button
className="button is-danger"
disabled={!deletable}
Expand All @@ -110,6 +128,7 @@ const TicketItem = ({
type Ticket = {
name: string
count: string | null
price: string | null // Free if null
}

const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
Expand All @@ -119,7 +138,7 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
const [submitting, setSubmitting] = useState(false)

const [tickets, setTickets] = useState<Ticket[]>([
{ name: 'Regular Ticket', count: null },
{ name: 'Regular Ticket', count: null, price: null },
])

const handleNameChange = (name, i) => {
Expand All @@ -134,6 +153,12 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
setTickets(ticks)
}

const handlePriceChange = (price, i) => {
const ticks = [...tickets]
ticks[i].price = price
setTickets(ticks)
}

const deleteTicket = (i) => {
const ticks = [...tickets]
ticks.splice(i, 1)
Expand All @@ -142,7 +167,7 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {

const addNewTicket = () => {
const ticks = [...tickets]
ticks.push({ name: '', count: null })
ticks.push({ name: '', count: null, price: null })
setTickets(ticks)
}

Expand All @@ -151,24 +176,38 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
const quantities = tickets
.filter((ticket) => ticket.count != null)
.map((ticket) => {
return { type: ticket.name, count: parseInt(ticket.count || '') }
return {
type: ticket.name,
count: parseInt(ticket.count || ''),
price: parseFloat(ticket.price || ''),
}
})
doApiRequest(`/events/${id}/tickets/?format=json`, {
method: 'PUT',
body: {
quantities: quantities,
quantities,
},
}).then((res) => {
if (res.ok) {
notify(<>Tickets Created!</>, 'success')
setSubmitting(false)
} else {
notify(<>Error creating tickets</>, 'error')
setSubmitting(false)
}
})
notify(<>Tickets Created!</>, 'success')
setSubmitting(false)
}
}

const disableSubmit = tickets.some(
(ticket) =>
typeof ticket.name !== 'string' ||
ticket.count === null ||
!Number.isInteger(parseInt(ticket.count || '0')),
!Number.isInteger(parseInt(ticket.count || '0')) ||
parseInt(ticket.count || '0') < 0 ||
ticket.price === null ||
!Number.isFinite(parseFloat(ticket.price || '0')) ||
parseFloat(ticket.price || '0') < 0,
)

return (
Expand All @@ -181,15 +220,7 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
/>
<ModalBody>
<Title>{name}</Title>
<Text>
Create new tickets for this event. To be filled with actual
instructions. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur.
</Text>
<Text>Create new tickets for this event.</Text>
<Line />
<SectionContainer>
<h1>Tickets</h1>
Expand All @@ -202,6 +233,7 @@ const TicketsModal = (props: { event: ClubEvent }): ReactElement => {
deletable={tickets.length !== 1}
changeName={handleNameChange}
changeCount={handleCountChange}
changePrice={handlePriceChange}
deleteTicket={deleteTicket}
/>
))}
Expand Down
Loading