Refs #654 -- API: Writable item endpoints (#676)

* MKBDIGI-184: Basic create added for API items endpoint

* MKBDIGI-184: Starting endpoint for GET /api/v1/organizers/(organizer)/events/(event)/items/(id)/variations/

* MKBDIGI-184: endpoint for GET /api/v1/organizers/(organizer)/events/(event)/items/(id)/variations/

* MKBDIGI-184: Completed endpoint for variations

* MKBDIGI-184: Added endpoint for addons

* MKBDIGI-184: Added Item validation

* MKBDIGI-184: Added check for order/cart positions on item variation destroy.

* MKBDIGI-184: Fixed check for order/cart positions on item variation destroy.

* MKBDIGI-184: Updated tests, validation for addons

* MKBDIGI-184: Documentation feedback corrections

* MKBDIGI-184: Added documentation for item add-ons

* MKBDIGI-184: Code formatting fixes

* MKBDIGI-184: Feedback fixes

* MKBDIGI-184: Updated tests for delete item

* MKBDIGI-184: Cleaned up tests

* MKBDIGI-184: Added additional test URLs

* MKBDIGI-184: Documentation fixes

* MKBDIGI-184: Fixed read-only fields/Documentation

* MKBDIGI-184: Documentation fixes

* MKBDIGI-184: Added helper for dict merge for 3.4 compatibility

* MKBDIGI-184: Validation updates

* MKBDIGI-184: Fixed permissions test error. Changed to HTTP 404 for POST to addons endpoint

* MKBDIGI-184: Implemented nested variations and add-ons for POST on the item endpoint.
This commit is contained in:
Ture Gjørup
2018-02-01 15:43:51 +01:00
committed by Raphael Michel
parent f5dba45fa0
commit 8eaada992f
13 changed files with 1854 additions and 28 deletions

View File

@@ -1,5 +1,8 @@
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -16,11 +19,44 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
'position', 'default_price', 'price')
class ItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price')
class InlineItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
fields = ('addon_category', 'min_count', 'max_count',
'position')
'position', 'price_included')
class ItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
fields = ('id', 'addon_category', 'min_count', 'max_count',
'position', 'price_included')
def validate(self, data):
data = super().validate(data)
ItemAddOn.clean_max_min_count(data.get('max_count'), data.get('min_count'))
return data
def validate_min_count(self, value):
ItemAddOn.clean_min_count(value)
return value
def validate_max_count(self, value):
ItemAddOn.clean_max_count(value)
return value
def validate_addon_category(self, value):
ItemAddOn.clean_categories(self.context['event'], self.context['item'], self.instance, value)
return value
class ItemTaxRateField(serializers.Field):
@@ -32,8 +68,8 @@ class ItemTaxRateField(serializers.Field):
class ItemSerializer(I18nAwareModelSerializer):
addons = InlineItemAddOnSerializer(many=True)
variations = InlineItemVariationSerializer(many=True)
addons = InlineItemAddOnSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
class Meta:
@@ -44,6 +80,55 @@ class ItemSerializer(I18nAwareModelSerializer):
'require_voucher', 'hide_without_voucher', 'allow_cancel',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
'variations', 'addons')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):
return {"has_variations": self.kwargs['has_variations']}
def validate(self, data):
data = super().validate(data)
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
return data
def validate_category(self, value):
Item.clean_category(value, self.context['event'])
return value
def validate_tax_rule(self, value):
Item.clean_tax_rule(value, self.context['event'])
return value
def validate_variations(self, value):
if self.instance is not None:
raise ValidationError(_('Updating variations via PATCH/PUT is not supported. Please use the dedicated'
' nested endpoint.'))
return value
def validate_addons(self, value):
if self.instance is not None:
raise ValidationError(_('Updating add-ons via PATCH/PUT is not supported. Please use the dedicated'
' nested endpoint.'))
else:
for addon_data in value:
ItemAddOn.clean_categories(self.context['event'], None, self.instance, addon_data['addon_category'])
ItemAddOn.clean_min_count(addon_data['min_count'])
ItemAddOn.clean_max_count(addon_data['max_count'])
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
return value
@transaction.atomic
def create(self, validated_data):
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
item = Item.objects.create(**validated_data)
for variation_data in variations_data:
ItemVariation.objects.create(item=item, **variation_data)
for addon_data in addons_data:
ItemAddOn.objects.create(base_item=item, **addon_data)
return item
class ItemCategorySerializer(I18nAwareModelSerializer):

View File

@@ -29,6 +29,10 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
item_router = routers.DefaultRouter()
item_router.register(r'variations', item.ItemVariationViewSet)
item_router.register(r'addons', item.ItemAddOnViewSet)
# Force import of all plugins to give them a chance to register URLs with the router
for app in apps.get_app_configs():
if hasattr(app, 'PretixPluginMeta'):
@@ -39,6 +43,7 @@ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
include(checkinlist_router.urls)),
]

View File

@@ -1,17 +1,22 @@
import django_filters
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.serializers.item import (
ItemCategorySerializer, ItemSerializer, QuestionSerializer,
QuotaSerializer,
ItemAddOnSerializer, ItemCategorySerializer, ItemSerializer,
ItemVariationSerializer, QuestionSerializer, QuotaSerializer,
)
from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, Quota,
)
from pretix.base.models import Item, ItemCategory, Question, Quota
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.dicts import merge_dicts
class ItemFilter(FilterSet):
@@ -28,7 +33,7 @@ class ItemFilter(FilterSet):
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
class ItemViewSet(viewsets.ReadOnlyModelViewSet):
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -36,10 +41,159 @@ class ItemViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('position', 'id')
filter_class = ItemFilter
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons').all()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.item.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
ctx['has_variations'] = self.request.data.get('has_variations')
return ctx
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.item.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This item cannot be deleted because it has already been ordered '
'by a user or currently is in a users\'s cart. Please set the item as '
'"inactive" instead.')
instance.log_action(
'pretix.event.item.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
class ItemVariationViewSet(viewsets.ModelViewSet):
serializer_class = ItemVariationSerializer
queryset = ItemVariation.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
return item.variations.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['item'] = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
return ctx
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
if not item.has_variations:
raise PermissionDenied('This variation cannot be created because the item does not have variations. '
'Changing a product without variations to a product with variations is not allowed.')
serializer.save(item=item)
item.log_action(
'pretix.event.item.variation.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
{'value': serializer.instance.value})
)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.item.log_action(
'pretix.event.item.variation.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
{'value': serializer.instance.value})
)
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This variation cannot be deleted because it has already been ordered '
'by a user or currently is in a users\'s cart. Please set the variation as '
'\'inactive\' instead.')
if instance.is_only_variation():
raise PermissionDenied('This variation cannot be deleted because it is the only variation. Changing a '
'product with variations to a product without variations is not allowed.')
super().perform_destroy(instance)
instance.item.log_action(
'pretix.event.item.variation.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data={
'value': instance.value,
'id': self.kwargs['pk']
}
)
class ItemAddOnViewSet(viewsets.ModelViewSet):
serializer_class = ItemAddOnSerializer
queryset = ItemAddOn.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
return item.addons.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['item'] = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
return ctx
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
serializer.save(base_item=item, addon_category=category)
item.log_action(
'pretix.event.item.addons.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.base_item.log_action(
'pretix.event.item.addons.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.base_item.log_action(
'pretix.event.item.addons.removed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data={'category': instance.addon_category.pk}
)
class ItemCategoryFilter(FilterSet):
class Meta: