Compare commits

...

10 Commits

Author SHA1 Message Date
Lukas Bockstaller
18b361422f add event to api response 2026-02-05 14:22:40 +01:00
Lukas Bockstaller
26b9d3c098 Apply suggestions from code review
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-02-03 15:55:50 +01:00
Lukas Bockstaller
9f8524a6bd revert no longer needed test changes 2026-01-28 17:46:56 +01:00
Lukas Bockstaller
8faf4e3f19 revert no longer needed test changes 2026-01-28 17:45:12 +01:00
Lukas Bockstaller
dbdf15e8fb tidying up 2026-01-28 17:38:41 +01:00
Lukas Bockstaller
cdea0cfffa revert import changes 2026-01-28 17:37:13 +01:00
Lukas Bockstaller
357c74bd66 remove left over empty lines 2026-01-28 17:35:53 +01:00
Lukas Bockstaller
d693cd1bdc split out organizer list endpoint 2026-01-28 17:31:46 +01:00
Lukas Bockstaller
00c2763748 handle permissions 2026-01-26 15:15:58 +01:00
Lukas Bockstaller
4a0884aaca initial implementation 2026-01-26 14:23:16 +01:00
5 changed files with 226 additions and 78 deletions

View File

@@ -1719,6 +1719,54 @@ List of all order positions
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/orderpositions/
Returns a list of all order positions within all events of a given organizer (with sufficient access permissions).
Supported query parameters and output format of this endpoint are identical to the list endpoint within an event,
with the exception that the ``pdf_data`` parameter is not supported here.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/orderpositions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
X-Page-Generated: 2017-12-01T10:00:00Z
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id:": 23442
"event": "sampleconf",
"order": "ABC12",
"positionid": 1,
"canceled": false,
"item": 1345,
...
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual positions
-----------------------------

View File

@@ -636,6 +636,14 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
return entry
class OrganizerOrderPositionSerializer(OrderPositionSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
class Meta(OrderPositionSerializer.Meta):
fields = OrderPositionSerializer.Meta.fields + ('event',)
read_only_fields = OrderPositionSerializer.Meta.read_only_fields + ('event',)
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
return instance.require_checkin_attention

View File

@@ -67,6 +67,7 @@ orga_router.register(r'invoices', order.InvoiceViewSet)
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
@@ -83,7 +84,7 @@ event_router.register(r'discounts', discount.DiscountViewSet)
event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.EventOrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'orderpositions', order.EventOrderPositionViewSet)
event_router.register(r'transactions', order.TransactionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')

View File

@@ -57,9 +57,10 @@ from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer, TransactionSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer,
OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer,
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
TransactionSerializer,
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
@@ -1071,8 +1072,7 @@ with scopes_disabled():
}
class OrderPositionViewSet(viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
class OrderPositionViewSetMixin:
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('order__datetime', 'positionid')
@@ -1093,8 +1093,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['pdf_data'] = False
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
return ctx
@@ -1103,9 +1102,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
qs = OrderPosition.all
else:
qs = OrderPosition.objects
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
qs = qs.filter(order__event__organizer=self.request.organizer)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(self.request, 'event', None):
prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects(
[self.request.event],
@@ -1174,6 +1172,37 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
return prov
raise NotFound('Unknown output provider.')
class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerOrderPositionSerializer
def get_queryset(self):
qs = super().get_queryset()
perm = self.permission if self.request.method in SAFE_METHODS else self.write_permission
if isinstance(self.request.auth, (TeamAPIToken, Device)) or self.request.user.is_authenticated:
qs = qs.filter(
order__event__in=self.request.auth.get_events_with_permission(perm, request=self.request)
)
return qs
class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
return ctx
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(order__event=self.request.event)
return qs
@action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs):
"""

View File

@@ -34,7 +34,7 @@ from stripe import error
from tests.plugins.stripe.test_checkout import apple_domain_create
from tests.plugins.stripe.test_provider import MockedCharge
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models import InvoiceAddress, Order, OrderPosition, Team
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
@@ -180,6 +180,41 @@ def order2(event2, item2):
return o
@pytest.fixture
@scopes_disabled()
def team2(organizer, event2):
team2 = Team.objects.create(
organizer=organizer,
name="Test-Team 2",
can_change_teams=True,
can_manage_gift_cards=True,
can_change_items=True,
can_create_events=True,
can_change_event_settings=True,
can_change_vouchers=True,
can_view_vouchers=True,
can_change_orders=True,
can_manage_customers=True,
can_manage_reusable_media=True,
can_change_organizer_settings=True,
)
team2.limit_events.add(event2)
team2.save()
return team2
@pytest.fixture
@scopes_disabled()
def limited_token_client(client, team2):
team2.can_view_orders = True
team2.can_view_vouchers = True
team2.save()
t = team2.tokens.create(name='Foo')
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
return client
TEST_ORDERPOSITION_RES = {
"id": 1,
"order": "FOO",
@@ -987,8 +1022,64 @@ def test_refund_cancel(token_client, organizer, event, order):
assert resp.status_code == 400
@pytest.mark.parametrize(
"endpoint_template, response_code",
[('/api/v1/organizers/{}/events/{}/orderpositions/', 403), ('/api/v1/organizers/{}/orderpositions/', 200)]
)
@pytest.mark.django_db
def test_orderposition_list(token_client, organizer, device, event, order, item, subevent, subevent2, question, django_assert_num_queries):
def test_orderposition_list_limited_read(
endpoint_template, response_code, limited_token_client, organizer, device, event, order, item, subevent, subevent2, question
):
endpoint = endpoint_template.format(organizer.slug, event.slug)
i2 = copy.copy(item)
i2.pk = None
i2.save()
with scopes_disabled():
var = item.variations.create(value="Children")
res = copy.copy(TEST_ORDERPOSITION_RES)
op = order.positions.first()
op.variation = var
op.save()
res["id"] = op.pk
res["item"] = item.pk
res["variation"] = var.pk
res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
resp = limited_token_client.get(endpoint)
assert resp.status_code == response_code
if response_code == 200:
assert resp.json() == {'count': 0, 'next': None, 'previous': None, 'results': []}
else:
assert resp.json() == {'detail': 'You do not have permission to perform this action.'}
@pytest.mark.parametrize(
("endpoint_template", "endpoint_type"),
[
('/api/v1/organizers/{}/events/{}/orderpositions/', "event"),
('/api/v1/organizers/{}/orderpositions/', "organizer")
],
)
@pytest.mark.django_db
def test_orderposition_list(
endpoint_template,
endpoint_type,
token_client,
organizer,
device,
event,
order,
item,
subevent,
subevent2,
question,
django_assert_num_queries
):
endpoint = endpoint_template.format(organizer.slug, event.slug)
i2 = copy.copy(item)
i2.pk = None
i2.save()
@@ -1005,88 +1096,64 @@ def test_orderposition_list(token_client, organizer, device, event, order, item,
res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
if endpoint_type == "organizer":
res["event"] = event.slug
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint)
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=n'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order__status=n')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=p'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order__status=p')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, item.pk))
resp = token_client.get(endpoint + '?item={}'.format(item.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item__in={},{}'.format(
organizer.slug, event.slug, item.pk, i2.pk
))
resp = token_client.get(endpoint + '?item__in={},{}'.format(item.pk, i2.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, i2.pk))
resp = token_client.get(endpoint + '?item={}'.format(i2.pk))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var.pk))
resp = token_client.get(endpoint + '?variation={}'.format(var.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var2.pk))
resp = token_client.get(endpoint + '?variation={}'.format(var2.pk))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=Peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Mark'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=Mark')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w'.format(
organizer.slug, event.slug))
resp = token_client.get(endpoint + '?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=abc123'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?secret=abc123')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=ABCDEFGHKL'.format(
organizer.slug, event.slug))
resp = token_client.get(endpoint + '?pseudonymization_id=ABCDEFGHKL')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=FOO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?pseudonymization_id=FOO')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=FO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=FO')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=z3fsn8j'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=z3fsn8j')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=Peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=Peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=5f4h6w'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=5f4h6w')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=FOO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order=FOO')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=BAR'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order=BAR')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=false'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?has_checkin=false')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?has_checkin=true')
assert [] == resp.data['results']
with scopes_disabled():
@@ -1103,33 +1170,28 @@ def test_orderposition_list(token_client, organizer, device, event, order, item,
'gate': None,
'type': 'entry'
}]
with django_assert_num_queries(16):
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug)
)
if '/events/' in endpoint:
with django_assert_num_queries(16):
resp = token_client.get(endpoint + '?has_checkin=true')
else:
with django_assert_num_queries(15):
resp = token_client.get(endpoint + '?has_checkin=true')
assert [res] == resp.data['results']
op.subevent = subevent
op.save()
res['subevent'] = subevent.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug, subevent.pk))
resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent__in={},{}'.format(organizer.slug, event.slug,
subevent.pk, subevent2.pk))
resp = token_client.get(endpoint + '?subevent__in={},{}'.format(subevent.pk, subevent2.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug,
subevent.pk + 1))
resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk + 1))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=false'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?include_canceled_positions=false')
assert len(resp.data['results']) == 1
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=true'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?include_canceled_positions=true')
assert len(resp.data['results']) == 2