Skip to content

Commit 8196022

Browse files
authored
Add support for extended voucher endpoint (#8)
1 parent c3c8180 commit 8196022

File tree

8 files changed

+200
-19
lines changed

8 files changed

+200
-19
lines changed

pretix_extended_api/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
from .views.orders import OrdersViewSet
44
from .views.tickets import TicketsViewSet
5+
from .views.vouchers import VouchersViewSet
56

67
event_router.register("tickets", TicketsViewSet, basename="tickets")
78
event_router.register("extended-orders", OrdersViewSet, basename="extended-orders")
9+
event_router.register(
10+
"extended-vouchers", VouchersViewSet, basename="extended-vouchers"
11+
)

pretix_extended_api/views/orders.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
from pretix.api.serializers.order import OrderSerializer
22
from pretix.api.views.order import OrderViewSet
3-
from pretix.base.models import TeamAPIToken
4-
from rest_framework import exceptions, viewsets
3+
from rest_framework import viewsets
54
from rest_framework.response import Response
65

6+
from .permissions import check_permission
7+
78

89
class OrdersViewSet(viewsets.ViewSet):
910
def retrieve(self, request, pk: str, **kwargs):
10-
# Only allow Team API tokens to call this API.
11-
perm_holder = request.auth if isinstance(request.auth, TeamAPIToken) else None
12-
if not perm_holder or not perm_holder.has_event_permission(
13-
request.event.organizer, request.event, "can_view_orders"
14-
):
15-
raise exceptions.PermissionDenied()
11+
check_permission(request, "can_view_orders")
1612

1713
codes = pk.split(",")
1814
qs = OrderViewSet(request=request).get_queryset()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from pretix.base.models import TeamAPIToken
2+
from rest_framework import exceptions
3+
4+
5+
def check_permission(request, permission):
6+
# Only allow Team API tokens to call this API.
7+
perm_holder = request.auth if isinstance(request.auth, TeamAPIToken) else None
8+
if not perm_holder or not perm_holder.has_event_permission(
9+
request.event.organizer, request.event, permission
10+
):
11+
raise exceptions.PermissionDenied()

pretix_extended_api/views/serializers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pretix.api.serializers.i18n import I18nAwareModelSerializer
22
from pretix.api.serializers.item import ItemSerializer, QuestionSerializer
3+
from pretix.api.serializers.voucher import VoucherSerializer
34
from pretix.base.models import OrderPosition, QuestionAnswer
45
from rest_framework import serializers
56

@@ -61,3 +62,16 @@ def __init__(self, *args, **kwargs):
6162
super().__init__(*args, **kwargs)
6263

6364
self.fields["item"] = ItemSerializer(read_only=True, context=self.context)
65+
66+
67+
class ExtendedVoucherSerializer(VoucherSerializer):
68+
quota_items = serializers.SerializerMethodField()
69+
70+
def get_quota_items(self, instance):
71+
if not instance.quota_id:
72+
return None
73+
74+
return list(instance.quota.items.values_list("id", flat=True))
75+
76+
class Meta(VoucherSerializer.Meta):
77+
fields = VoucherSerializer.Meta.fields + ("quota_items",)

pretix_extended_api/views/tickets.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
from django.db.models import Q
22
from django_scopes import scopes_disabled
3-
from pretix.base.models import Order, OrderPosition, TeamAPIToken
4-
from rest_framework import exceptions, viewsets
3+
from pretix.base.models import Order, OrderPosition
4+
from rest_framework import viewsets
55
from rest_framework.decorators import action
66
from rest_framework.response import Response
77

8+
from .permissions import check_permission
89
from .serializers import (
910
AttendeeHasTicketBodySerializer,
1011
AttendeeTicketBodySerializer,
1112
OrderPositionSerializer,
1213
)
1314

1415

15-
def check_permission(request, permission):
16-
# Only allow Team API tokens to call this API.
17-
perm_holder = request.auth if isinstance(request.auth, TeamAPIToken) else None
18-
if not perm_holder or not perm_holder.has_event_permission(
19-
request.event.organizer, request.event, permission
20-
):
21-
raise exceptions.PermissionDenied()
22-
23-
2416
class TicketsViewSet(viewsets.ViewSet):
2517
# Disable scoping because we want to allow cross-organization checking
2618
# This allows us to do stuff like, you can attend PyCon Italia 2022
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from django.db.models import F, Q
2+
from django.utils.timezone import now
3+
from rest_framework import viewsets
4+
from rest_framework.response import Response
5+
6+
from .permissions import check_permission
7+
from .serializers import ExtendedVoucherSerializer
8+
9+
10+
class VouchersViewSet(viewsets.ViewSet):
11+
def retrieve(self, request, pk: str, **kwargs):
12+
check_permission(request, "can_view_vouchers")
13+
voucher = (
14+
self.request.event.vouchers.select_related("seat")
15+
.filter(
16+
Q(valid_until__isnull=True) | Q(valid_until__gt=now()),
17+
code=pk,
18+
redeemed__lt=F("max_usages"),
19+
)
20+
.first()
21+
)
22+
23+
if not voucher:
24+
return Response(status=404)
25+
26+
return Response(ExtendedVoucherSerializer(voucher).data)

tests/conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,35 @@ def user_client(client, team, user):
321321
team.members.add(user)
322322
client.force_authenticate(user=user)
323323
return client
324+
325+
326+
@pytest.fixture
327+
@scopes_disabled()
328+
def voucher_for_item(event, admission_item):
329+
return event.vouchers.create(
330+
item=admission_item, price_mode="set", value=12, tag="Foo"
331+
)
332+
333+
334+
@pytest.fixture
335+
@scopes_disabled()
336+
def voucher_for_quota(event, quota):
337+
return event.vouchers.create(
338+
item=None, quota=quota, price_mode="set", value=12, tag="Foo"
339+
)
340+
341+
342+
@pytest.fixture
343+
@scopes_disabled()
344+
def voucher_for_all_items(event):
345+
return event.vouchers.create(
346+
item=None, quota=None, price_mode="set", value=12, tag="Foo"
347+
)
348+
349+
350+
@pytest.fixture
351+
@scopes_disabled()
352+
def quota(event, admission_item):
353+
q = event.quotas.create(name="Budget Quota", size=200)
354+
q.items.add(admission_item)
355+
return q

tests/views/test_vouchers.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import pytest
2+
from datetime import timedelta
3+
from django.utils import timezone
4+
5+
pytestmark = pytest.mark.django_db
6+
7+
8+
def test_voucher_info_for_item(token_client, admission_item, voucher_for_item):
9+
resp = token_client.get(
10+
f"/api/v1/organizers/dummy/events/dummy/extended-vouchers/{voucher_for_item.code}/",
11+
format="json",
12+
)
13+
14+
assert resp.status_code == 200
15+
assert resp.data["id"] == voucher_for_item.id
16+
assert resp.data["item"] == admission_item.id
17+
assert resp.data["quota"] is None
18+
assert resp.data["quota_items"] is None
19+
20+
21+
def test_quota_voucher_info(token_client, admission_item, quota, voucher_for_quota):
22+
resp = token_client.get(
23+
f"/api/v1/organizers/dummy/events/dummy/extended-vouchers/{voucher_for_quota.code}/",
24+
format="json",
25+
)
26+
27+
assert resp.status_code == 200
28+
assert resp.data["id"] == voucher_for_quota.id
29+
assert resp.data["item"] is None
30+
assert resp.data["quota"] == quota.id
31+
assert resp.data["quota_items"] == [admission_item.id]
32+
33+
34+
def test_voucher_for_all_items(token_client, voucher_for_all_items):
35+
resp = token_client.get(
36+
f"/api/v1/organizers/dummy/events/dummy/extended-vouchers/{voucher_for_all_items.code}/",
37+
format="json",
38+
)
39+
40+
assert resp.status_code == 200
41+
assert resp.data["id"] == voucher_for_all_items.id
42+
assert resp.data["item"] is None
43+
assert resp.data["quota"] is None
44+
assert resp.data["quota_items"] is None
45+
46+
47+
def test_invalid_code(token_client, voucher_for_all_items):
48+
resp = token_client.get(
49+
"/api/v1/organizers/dummy/events/dummy/extended-vouchers/ABCABCABC/",
50+
format="json",
51+
)
52+
53+
assert resp.status_code == 404
54+
assert resp.data is None
55+
56+
57+
def test_requires_authentication(client, voucher_for_all_items):
58+
resp = client.get(
59+
f"/api/v1/organizers/dummy/events/dummy/extended-vouchers/{voucher_for_all_items.code}/",
60+
format="json",
61+
)
62+
63+
assert resp.status_code == 401
64+
65+
66+
def test_requires_authentication_of_team(user_client, voucher_for_all_items):
67+
resp = user_client.get(
68+
f"/api/v1/organizers/dummy/events/dummy/extended-vouchers/{voucher_for_all_items.code}/",
69+
format="json",
70+
)
71+
72+
assert resp.status_code == 403
73+
74+
75+
def test_requires_permissions(no_permissions_token_client, voucher_for_all_items):
76+
resp = no_permissions_token_client.get(
77+
f"/api/v1/organizers/dummy/events/dummy/extended-vouchers/{voucher_for_all_items.code}/",
78+
format="json",
79+
)
80+
81+
assert resp.status_code == 403
82+
83+
84+
def test_expired_voucher_is_not_returned(token_client, voucher_for_item):
85+
voucher_for_item.valid_until = timezone.now() - timedelta(days=50)
86+
voucher_for_item.save()
87+
88+
resp = token_client.get(
89+
f"/api/v1/organizers/dummy/events/dummy/extended-vouchers/{voucher_for_item.code}/",
90+
format="json",
91+
)
92+
93+
assert resp.status_code == 404
94+
95+
96+
def test_used_voucher_is_not_returned(token_client, voucher_for_item):
97+
voucher_for_item.redeemed = 1
98+
voucher_for_item.max_usages = 1
99+
voucher_for_item.save()
100+
101+
resp = token_client.get(
102+
f"/api/v1/organizers/dummy/events/dummy/extended-vouchers/{voucher_for_item.code}/",
103+
format="json",
104+
)
105+
106+
assert resp.status_code == 404

0 commit comments

Comments
 (0)