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:
Ture Gjørup
2017-11-16 18:39:10 +01:00
committed by Raphael Michel
parent 445afcc50c
commit e4167380b9
8 changed files with 470 additions and 6 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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.'))

View File

@@ -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

View File

@@ -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(

View File

@@ -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),