mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
API: Write methods for quotas (#657)
* MKBDIGI-183: Added quotas API write methods * MKBDIGI-183: Fixed code formatting * MKBDIGI-183: Added test for permission requirements * MKBDIGI-183: Documentation corrections * MKBDIGI-183: Removed redundant create/update locks * MKBDIGI-183: Added quota validation to check that items and variations corresponds to each other * MKBDIGI-183: Added quota validation to check that item belong to the same event as the endpoint * MKBDIGI-183: Added subevent validation to check that subevent belong to the same event as the endpoint * MKBDIGI-183: Added subevent validation to check that subevent is null for non-series events * MKBDIGI-183: Changed validation error text * MKBDIGI-183: Added logging for subevents * MKBDIGI-183: Fixed code formatting * MKBDIGI-183: Fixed validation error in API test * MKBDIGI-183: Fixed documentation errors * MKBDIGI-183: Fixed typos in validation messages * MKBDIGI-183: Refactored validation loop vars check * MKBDIGI-183: Updated error strings in test assersions * MKBDIGI-183: Fixed logging for API quota update to account changing subevents
This commit is contained in:
committed by
Raphael Michel
parent
445afcc50c
commit
e4167380b9
@@ -73,3 +73,16 @@ class QuotaSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
Quota.clean_variations(full_data.get('items'), full_data.get('variations'))
|
||||
Quota.clean_items(event, full_data.get('items'), full_data.get('variations'))
|
||||
Quota.clean_subevent(event, full_data.get('subevent'))
|
||||
|
||||
return data
|
||||
|
||||
@@ -11,6 +11,7 @@ from pretix.api.serializers.item import (
|
||||
QuotaSerializer,
|
||||
)
|
||||
from pretix.base.models import Item, ItemCategory, Question, Quota
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
|
||||
|
||||
class ItemFilter(FilterSet):
|
||||
@@ -77,7 +78,7 @@ class QuotaFilter(FilterSet):
|
||||
fields = ['subevent']
|
||||
|
||||
|
||||
class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class QuotaViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = QuotaSerializer
|
||||
queryset = Quota.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
@@ -85,10 +86,80 @@ class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ordering_fields = ('id', 'size')
|
||||
ordering = ('id',)
|
||||
permission = 'can_change_items'
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.quotas.all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.quota.added',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
if serializer.instance.subevent:
|
||||
serializer.instance.subevent.log_action(
|
||||
'pretix.subevent.quota.added',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
current_subevent = serializer.instance.subevent
|
||||
serializer.save(event=self.request.event)
|
||||
request_subevent = serializer.instance.subevent
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.quota.changed',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
if current_subevent == request_subevent:
|
||||
if current_subevent is not None:
|
||||
current_subevent.log_action(
|
||||
'pretix.subevent.quota.changed',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
else:
|
||||
if request_subevent is not None:
|
||||
request_subevent.log_action(
|
||||
'pretix.subevent.quota.added',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
data=self.request.data
|
||||
)
|
||||
if current_subevent is not None:
|
||||
current_subevent.log_action(
|
||||
'pretix.subevent.quota.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
serializer.instance.rebuild_cache()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.quota.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
if instance.subevent:
|
||||
instance.subevent.log_action(
|
||||
'pretix.subevent.quota.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
@detail_route(methods=['get'])
|
||||
def availability(self, request, *args, **kwargs):
|
||||
quota = self.get_object()
|
||||
|
||||
@@ -897,3 +897,30 @@ class Quota(LoggedModel):
|
||||
|
||||
class QuotaExceededException(Exception):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def clean_variations(items, variations):
|
||||
for variation in variations:
|
||||
if variation.item not in items:
|
||||
raise ValidationError(_('All variations must belong to an item contained in the items list.'))
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def clean_items(event, items, variations):
|
||||
for item in items:
|
||||
if event != item.event:
|
||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||
if item.has_variations:
|
||||
if not any(var.item == item for var in variations):
|
||||
raise ValidationError(_('One or more items has variations but none of these are in the variations list.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_subevent(event, subevent):
|
||||
if event.has_subevents:
|
||||
if not subevent:
|
||||
raise ValidationError(_('Subevent cannot be null for event series.'))
|
||||
if event != subevent.event:
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
else:
|
||||
if subevent:
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
|
||||
@@ -33,6 +33,28 @@ def event(organizer, meta_prop):
|
||||
return e
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event2(organizer, meta_prop):
|
||||
e = Event.objects.create(
|
||||
organizer=organizer, name='Dummy2', slug='dummy2',
|
||||
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC),
|
||||
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf'
|
||||
)
|
||||
e.meta_values.create(property=meta_prop, value="Conference")
|
||||
return e
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event3(organizer, meta_prop):
|
||||
e = Event.objects.create(
|
||||
organizer=organizer, name='Dummy3', slug='dummy3',
|
||||
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC),
|
||||
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf'
|
||||
)
|
||||
e.meta_values.create(property=meta_prop, value="Conference")
|
||||
return e
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def team(organizer):
|
||||
return Team.objects.create(
|
||||
@@ -65,8 +87,17 @@ def token_client(client, team):
|
||||
def subevent(event, meta_prop):
|
||||
event.has_subevents = True
|
||||
event.save()
|
||||
se = event.subevents.create(name="Foobar",
|
||||
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
|
||||
se = event.subevents.create(name="Foobar", date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
|
||||
|
||||
se.meta_values.create(property=meta_prop, value="Workshop")
|
||||
return se
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subevent2(event2, meta_prop):
|
||||
event2.has_subevents = True
|
||||
event2.save()
|
||||
se = event2.subevents.create(name="Foobar", date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
|
||||
|
||||
se.meta_values.create(property=meta_prop, value="Workshop")
|
||||
return se
|
||||
|
||||
@@ -2,6 +2,8 @@ from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from pretix.base.models import Quota
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def category(event):
|
||||
@@ -55,6 +57,16 @@ def item(event):
|
||||
return event.items.create(name="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def item2(event2):
|
||||
return event2.items.create(name="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def item3(event):
|
||||
return event.items.create(name="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
TEST_ITEM_RES = {
|
||||
"name": {"en": "Budget Ticket"},
|
||||
"default_price": "23.00",
|
||||
@@ -199,6 +211,22 @@ TEST_QUOTA_RES = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def variations(item):
|
||||
v = list()
|
||||
v.append(item.variations.create(value="ChildA1"))
|
||||
v.append(item.variations.create(value="ChildA2"))
|
||||
return v
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def variations2(item2):
|
||||
v = list()
|
||||
v.append(item2.variations.create(value="ChildB1"))
|
||||
v.append(item2.variations.create(value="ChildB2"))
|
||||
return v
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_list(token_client, organizer, event, quota, item, subevent):
|
||||
res = dict(TEST_QUOTA_RES)
|
||||
@@ -232,6 +260,171 @@ def test_quota_detail(token_client, organizer, event, quota, item):
|
||||
assert res == resp.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_create(token_client, organizer, event, event2, item):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [],
|
||||
"subevent": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
quota = Quota.objects.get(pk=resp.data['id'])
|
||||
assert quota.name == "Ticket Quota"
|
||||
assert quota.size == 200
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event2.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [],
|
||||
"subevent": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["One or more items do not belong to this event."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_create_with_variations(token_client, organizer, event, item, variations, variations2):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [variations[0].pk],
|
||||
"subevent": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [100],
|
||||
"subevent": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"variations":["Invalid pk \\"100\\" - object does not exist."]}'
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [variations[0].pk, variations2[0].pk],
|
||||
"subevent": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["All variations must belong to an item contained in the items list."]}'
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [],
|
||||
"subevent": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["One or more items has variations but none of these are in the variations list."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_create_with_subevent(token_client, organizer, event, event3, item, variations, subevent, subevent2):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [variations[0].pk],
|
||||
"subevent": subevent.pk
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [variations[0].pk],
|
||||
"subevent": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["Subevent cannot be null for event series."]}'
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [variations[0].pk],
|
||||
"subevent": subevent2.pk
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["The subevent does not belong to this event."]}'
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event3.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [],
|
||||
"variations": [],
|
||||
"subevent": subevent2.pk
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["The subevent does not belong to this event."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_update(token_client, organizer, event, quota, item):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/{}/'.format(organizer.slug, event.slug, quota.pk),
|
||||
{
|
||||
"name": "Ticket Quota Update",
|
||||
"size": 111,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
quota = Quota.objects.get(pk=resp.data['id'])
|
||||
assert quota.name == "Ticket Quota Update"
|
||||
assert quota.size == 111
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_availability(token_client, organizer, event, quota, item):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/quotas/{}/availability/'.format(
|
||||
|
||||
@@ -34,6 +34,10 @@ event_permission_urls = [
|
||||
('put', 'can_change_vouchers', 'vouchers/1/', 404),
|
||||
('patch', 'can_change_vouchers', 'vouchers/1/', 404),
|
||||
('delete', 'can_change_vouchers', 'vouchers/1/', 404),
|
||||
('post', 'can_change_items', 'quotas/', 400),
|
||||
('put', 'can_change_items', 'quotas/1/', 404),
|
||||
('patch', 'can_change_items', 'quotas/1/', 404),
|
||||
('delete', 'can_change_items', 'quotas/1/', 404),
|
||||
('post', 'can_change_orders', 'orders/ABC12/mark_paid/', 404),
|
||||
('post', 'can_change_orders', 'orders/ABC12/mark_pending/', 404),
|
||||
('post', 'can_change_orders', 'orders/ABC12/mark_expired/', 404),
|
||||
|
||||
Reference in New Issue
Block a user