forked from CGM_Public/pretix_original
- [x] Write operations for subevents - [x] Tests - [x] Documentation
This commit is contained in:
committed by
Raphael Michel
parent
516fab52da
commit
ca7d55082b
@@ -379,7 +379,7 @@ Endpoints
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event to update
|
:param organizer: The ``slug`` field of the organizer of the event to update
|
||||||
:param event: The ``slug`` field of the event to update
|
:param event: The ``slug`` field of the event to update
|
||||||
:statuscode 201: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The event could not be created due to invalid submitted data.
|
:statuscode 400: The event could not be created due to invalid submitted data.
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ meta_data dict Values set for
|
|||||||
|
|
||||||
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
|
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.6
|
||||||
|
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -103,11 +105,83 @@ Endpoints
|
|||||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the main event
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||||
|
|
||||||
|
Creates a new subevent.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": {"en": "First Sample Conference"},
|
||||||
|
"active": false,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": 2,
|
||||||
|
"price": "12.00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": {"en": "First Sample Conference"},
|
||||||
|
"active": false,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": 2,
|
||||||
|
"price": "12.00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the main event
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The sub-event could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||||
|
|
||||||
Returns information on one sub-event, identified by its ID.
|
Returns information on one sub-event, identified by its ID.
|
||||||
@@ -149,13 +223,106 @@ Endpoints
|
|||||||
"meta_data": {}
|
"meta_data": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the main event
|
||||||
:param id: The ``slug`` field of the sub-event to fetch
|
:param id: The ``id`` field of the sub-event to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||||
|
|
||||||
|
Updates a sub-event, identified by its ID. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to
|
||||||
|
provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide
|
||||||
|
the fields that you want to change.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": {"en": "New Subevent Name"},
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": 2,
|
||||||
|
"price": "23.42"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": {"en": "New Subevent Name"},
|
||||||
|
"event": "sampleconf",
|
||||||
|
"active": false,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": 2,
|
||||||
|
"price": "23.42"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the main event
|
||||||
|
:param id: The ``id`` field of the sub-event to update
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The sub-event could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/sub-event does not exist **or** you have no permission to update this resource.
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||||
|
|
||||||
|
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the main event
|
||||||
|
:param id: The ``id`` field of the sub-event to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/sub-event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/subevents/
|
.. http:get:: /api/v1/organizers/(organizer)/subevents/
|
||||||
|
|
||||||
Returns a list of all sub-events of any event series you have access to within an organizer account.
|
Returns a list of all sub-events of any event series you have access to within an organizer account.
|
||||||
|
|||||||
@@ -191,8 +191,8 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class SubEventSerializer(I18nAwareModelSerializer):
|
class SubEventSerializer(I18nAwareModelSerializer):
|
||||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
|
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
||||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
|
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
||||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
meta_data = MetaDataField(source='*')
|
meta_data = MetaDataField(source='*')
|
||||||
|
|
||||||
@@ -202,6 +202,103 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
'presale_start', 'presale_end', 'location', 'event',
|
'presale_start', 'presale_end', 'location', 'event',
|
||||||
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
event = self.context['request'].event
|
||||||
|
|
||||||
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
|
full_data.update(data)
|
||||||
|
|
||||||
|
Event.clean_dates(data.get('date_from'), data.get('date_to'))
|
||||||
|
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
|
||||||
|
|
||||||
|
SubEvent.clean_items(event, [item['item'] for item in full_data.get('subeventitem_set')])
|
||||||
|
SubEvent.clean_variations(event, [item['variation'] for item in full_data.get('subeventitemvariation_set')])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def validate_item_price_overrides(self, data):
|
||||||
|
return list(filter(lambda i: 'item' in i, data))
|
||||||
|
|
||||||
|
def validate_variation_price_overrides(self, data):
|
||||||
|
return list(filter(lambda i: 'variation' in i, data))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def meta_properties(self):
|
||||||
|
return {
|
||||||
|
p.name: p for p in self.context['request'].organizer.meta_properties.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_meta_data(self, value):
|
||||||
|
for key in value['meta_data'].keys():
|
||||||
|
if key not in self.meta_properties:
|
||||||
|
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||||
|
return value
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||||
|
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||||
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
subevent = super().create(validated_data)
|
||||||
|
|
||||||
|
for item_price_override_data in item_price_overrides_data:
|
||||||
|
SubEventItem.objects.create(subevent=subevent, **item_price_override_data)
|
||||||
|
for variation_price_override_data in variation_price_overrides_data:
|
||||||
|
SubEventItemVariation.objects.create(subevent=subevent, **variation_price_override_data)
|
||||||
|
|
||||||
|
# Meta data
|
||||||
|
if meta_data is not None:
|
||||||
|
for key, value in meta_data.items():
|
||||||
|
subevent.meta_values.create(
|
||||||
|
property=self.meta_properties.get(key),
|
||||||
|
value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
return subevent
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||||
|
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||||
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
subevent = super().update(instance, validated_data)
|
||||||
|
|
||||||
|
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
|
||||||
|
|
||||||
|
for item_price_override_data in item_price_overrides_data:
|
||||||
|
id = existing_item_overrides.pop(item_price_override_data['item'], None)
|
||||||
|
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
|
||||||
|
|
||||||
|
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
|
||||||
|
|
||||||
|
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
|
||||||
|
|
||||||
|
for variation_price_override_data in variation_price_overrides_data:
|
||||||
|
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
|
||||||
|
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
|
||||||
|
|
||||||
|
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
|
||||||
|
|
||||||
|
# Meta data
|
||||||
|
if meta_data is not None:
|
||||||
|
current = {mv.property: mv for mv in subevent.meta_values.select_related('property')}
|
||||||
|
for key, value in meta_data.items():
|
||||||
|
prop = self.meta_properties.get(key)
|
||||||
|
if prop in current:
|
||||||
|
current[prop].value = value
|
||||||
|
current[prop].save()
|
||||||
|
else:
|
||||||
|
subevent.meta_values.create(
|
||||||
|
property=self.meta_properties.get(key),
|
||||||
|
value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
for prop, current_object in current.items():
|
||||||
|
if prop.name not in meta_data:
|
||||||
|
current_object.delete()
|
||||||
|
|
||||||
|
return subevent
|
||||||
|
|
||||||
|
|
||||||
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -217,9 +217,10 @@ class SubEventFilter(FilterSet):
|
|||||||
return queryset.exclude(expr)
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
|
|
||||||
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
|
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = SubEventSerializer
|
serializer_class = SubEventSerializer
|
||||||
queryset = ItemCategory.objects.none()
|
queryset = ItemCategory.objects.none()
|
||||||
|
write_permission = 'can_change_event_settings'
|
||||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
filterset_class = SubEventFilter
|
filterset_class = SubEventFilter
|
||||||
|
|
||||||
@@ -240,6 +241,42 @@ class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
|
|||||||
'subeventitem_set', 'subeventitemvariation_set'
|
'subeventitem_set', 'subeventitemvariation_set'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
super().perform_update(serializer)
|
||||||
|
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.subevent.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(event=self.request.event)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.subevent.added',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
if not instance.allow_delete():
|
||||||
|
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
|
||||||
|
' \'active\' to false instead to hide it from users.')
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
instance.log_action(
|
||||||
|
'pretix.subevent.deleted',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
super().perform_destroy(instance)
|
||||||
|
except ProtectedError:
|
||||||
|
raise PermissionDenied('The sub-event could not be deleted as some constraints (e.g. data created by '
|
||||||
|
'plug-ins) do not allow it.')
|
||||||
|
|
||||||
|
|
||||||
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = TaxRuleSerializer
|
serializer_class = TaxRuleSerializer
|
||||||
|
|||||||
@@ -936,6 +936,18 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
if self.event:
|
if self.event:
|
||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clean_items(event, items):
|
||||||
|
for item in items:
|
||||||
|
if event != item.event:
|
||||||
|
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clean_variations(event, variations):
|
||||||
|
for variation in variations:
|
||||||
|
if event != variation.item.event:
|
||||||
|
raise ValidationError(_('One or more variations do not belong to this event.'))
|
||||||
|
|
||||||
|
|
||||||
def generate_invite_token():
|
def generate_invite_token():
|
||||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||||
|
|||||||
@@ -1,4 +1,66 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django_countries.fields import Country
|
||||||
|
from pytz import UTC
|
||||||
|
|
||||||
|
from pretix.base.models import InvoiceAddress, Order, OrderPosition
|
||||||
|
from pretix.base.models.orders import OrderFee
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def variations(item):
|
||||||
|
v = list()
|
||||||
|
v.append(item.variations.create(value="ChildA1", default_price='12.00'))
|
||||||
|
v.append(item.variations.create(value="ChildA2", default_price='13.00'))
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def variations2(item2):
|
||||||
|
v = list()
|
||||||
|
v.append(item2.variations.create(value="ChildB1", default_price='12.00'))
|
||||||
|
v.append(item2.variations.create(value="ChildB2", default_price='13.00'))
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def order(event, item, taxrule):
|
||||||
|
testtime = datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC)
|
||||||
|
|
||||||
|
with mock.patch('django.utils.timezone.now') as mock_now:
|
||||||
|
mock_now.return_value = testtime
|
||||||
|
o = Order.objects.create(
|
||||||
|
code='FOO', event=event, email='dummy@dummy.test',
|
||||||
|
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
|
||||||
|
datetime=datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC),
|
||||||
|
expires=datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC),
|
||||||
|
total=23, locale='en'
|
||||||
|
)
|
||||||
|
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
|
||||||
|
tax_value=Decimal('0.05'), tax_rule=taxrule)
|
||||||
|
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def order_position(item, order, subevent, taxrule, variations):
|
||||||
|
op = OrderPosition.objects.create(
|
||||||
|
order=order,
|
||||||
|
item=item,
|
||||||
|
subevent=subevent,
|
||||||
|
variation=variations[0],
|
||||||
|
tax_rule=taxrule,
|
||||||
|
tax_rate=taxrule.rate,
|
||||||
|
tax_value=Decimal("3"),
|
||||||
|
price=Decimal("23"),
|
||||||
|
attendee_name_parts={'full_name': "Peter"},
|
||||||
|
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w"
|
||||||
|
)
|
||||||
|
return op
|
||||||
|
|
||||||
|
|
||||||
TEST_SUBEVENT_RES = {
|
TEST_SUBEVENT_RES = {
|
||||||
'active': False,
|
'active': False,
|
||||||
@@ -17,6 +79,16 @@ TEST_SUBEVENT_RES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def item(event):
|
||||||
|
return event.items.create(name="Budget Ticket", default_price=23)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def item2(event2):
|
||||||
|
return event2.items.create(name="Another Ticket", default_price=23)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_subevent_list(token_client, organizer, event, subevent):
|
def test_subevent_list(token_client, organizer, event, subevent):
|
||||||
res = dict(TEST_SUBEVENT_RES)
|
res = dict(TEST_SUBEVENT_RES)
|
||||||
@@ -50,6 +122,412 @@ def test_subevent_list(token_client, organizer, event, subevent):
|
|||||||
assert [] == resp.data['results']
|
assert [] == resp.data['results']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_subevent_get(token_client, organizer, event, subevent):
|
||||||
|
res = dict(TEST_SUBEVENT_RES)
|
||||||
|
res["id"] = subevent.pk
|
||||||
|
resp = token_client.get('/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug,
|
||||||
|
subevent.pk))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert res == resp.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_subevent_create(token_client, organizer, event, subevent, meta_prop, item):
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"de": "Demo Subevent 2020 Test",
|
||||||
|
"en": "Demo Subevent 2020 Test"
|
||||||
|
},
|
||||||
|
"active": False,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": "2017-12-28T10:00:00Z",
|
||||||
|
"date_admission": None,
|
||||||
|
"presale_start": None,
|
||||||
|
"presale_end": None,
|
||||||
|
"location": None,
|
||||||
|
"item_price_overrides": [],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {
|
||||||
|
"type": "Workshop"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert not subevent.active
|
||||||
|
assert subevent.meta_values.filter(
|
||||||
|
property__name=meta_prop.name, value="Workshop"
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"de": "Demo Subevent 2020 Test",
|
||||||
|
"en": "Demo Subevent 2020 Test"
|
||||||
|
},
|
||||||
|
"active": False,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": "2017-12-28T10:00:00Z",
|
||||||
|
"date_admission": None,
|
||||||
|
"presale_start": None,
|
||||||
|
"presale_end": None,
|
||||||
|
"location": None,
|
||||||
|
"item_price_overrides": [],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {
|
||||||
|
"foo": "bar"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"meta_data":["Meta data property \'foo\' does not exist."]}'
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"de": "Demo Subevent 2020 Test",
|
||||||
|
"en": "Demo Subevent 2020 Test"
|
||||||
|
},
|
||||||
|
"active": False,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": "2017-12-28T10:00:00Z",
|
||||||
|
"date_admission": None,
|
||||||
|
"presale_start": None,
|
||||||
|
"presale_end": None,
|
||||||
|
"location": None,
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": item.pk,
|
||||||
|
"price": "23.42"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {
|
||||||
|
"type": "Workshop"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert item.default_price == Decimal('23.00')
|
||||||
|
assert event.subevents.get(id=resp.data['id']).item_price_overrides[item.pk] == Decimal('23.42')
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"de": "Demo Subevent 2020 Test",
|
||||||
|
"en": "Demo Subevent 2020 Test"
|
||||||
|
},
|
||||||
|
"active": False,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": "2017-12-28T10:00:00Z",
|
||||||
|
"date_admission": None,
|
||||||
|
"presale_start": None,
|
||||||
|
"presale_end": None,
|
||||||
|
"location": None,
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": 555,
|
||||||
|
"price": "23.42"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {
|
||||||
|
"type": "Workshop"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"item_price_overrides":[{"item":["Invalid pk \\"555\\" - object does not exist."]}]}'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_subevent_update(token_client, organizer, event, subevent, item, item2, meta_prop, variations, variations2):
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"date_from": "2018-12-27T10:00:00Z",
|
||||||
|
"date_to": "2018-12-28T10:00:00Z",
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
subevent = event.subevents.get(id=subevent.id)
|
||||||
|
assert subevent.date_from == datetime(2018, 12, 27, 10, 0, tzinfo=UTC)
|
||||||
|
assert subevent.date_to == datetime(2018, 12, 28, 10, 0, tzinfo=UTC)
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": "2017-12-26T10:00:00Z"
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"non_field_errors":["The event cannot end before it starts."]}'
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"presale_start": "2017-12-27T10:00:00Z",
|
||||||
|
"presale_end": "2017-12-26T10:00:00Z"
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"non_field_errors":["The event\'s presale cannot end before it starts."]}'
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"meta_data": {
|
||||||
|
meta_prop.name: "Conference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert organizer.events.get(slug=event.slug).subevents.get(id=resp.data['id']).meta_values.filter(
|
||||||
|
property__name=meta_prop.name, value="Conference"
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"meta_data": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert not subevent.meta_values.filter(
|
||||||
|
property__name=meta_prop.name
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"meta_data": {
|
||||||
|
"test": "test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"meta_data":["Meta data property \'test\' does not exist."]}'
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": item.pk,
|
||||||
|
"price": "99.99"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert subevent.items.get(id=item.pk).default_price == Decimal('23.00')
|
||||||
|
assert subevent.item_price_overrides[item.pk] == Decimal('99.99')
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": item.pk,
|
||||||
|
"price": "88.88"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert event.subevents.get(id=subevent.id).item_price_overrides[item.pk] == Decimal('88.88')
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": item.pk,
|
||||||
|
"price": None
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert item.pk not in event.subevents.get(id=subevent.id).item_price_overrides
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": item.pk,
|
||||||
|
"price": "12.34"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert event.subevents.get(id=subevent.id).item_price_overrides[item.pk] == Decimal('12.34')
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"item_price_overrides": [],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert item.pk not in event.subevents.get(id=subevent.id).item_price_overrides
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": 123,
|
||||||
|
"price": "99.99"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"item_price_overrides":[{"item":["Invalid pk \\"123\\" - object does not exist."]}]}'
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": item2.id,
|
||||||
|
"price": "99.99"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"non_field_errors":["One or more items do not belong to this event."]}'
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"variation_price_overrides": [
|
||||||
|
{
|
||||||
|
"variation": variations[0].pk,
|
||||||
|
"price": "99.99"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert subevent.variations.get(id=variations[0].pk).default_price == Decimal('12.00')
|
||||||
|
assert subevent.var_price_overrides[variations[0].pk] == Decimal('99.99')
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"variation_price_overrides": [
|
||||||
|
{
|
||||||
|
"variation": variations[0].pk,
|
||||||
|
"price": "88.88"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert event.subevents.get(id=subevent.id).var_price_overrides[variations[0].pk] == Decimal('88.88')
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"variation_price_overrides": [
|
||||||
|
{
|
||||||
|
"variation": variations[0].pk,
|
||||||
|
"price": None
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert variations[0].pk not in event.subevents.get(id=subevent.id).var_price_overrides
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"variation_price_overrides": [
|
||||||
|
{
|
||||||
|
"variation": variations[0].pk,
|
||||||
|
"price": "12.34"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert event.subevents.get(id=subevent.id).var_price_overrides[variations[0].pk] == Decimal('12.34')
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert variations[0].pk not in event.subevents.get(id=subevent.id).var_price_overrides
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"variation_price_overrides": [
|
||||||
|
{
|
||||||
|
"variation": 123,
|
||||||
|
"price": "99.99"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"variation_price_overrides":[{"variation":["Invalid pk \\"123\\" - object does not exist."]}]}'
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||||
|
{
|
||||||
|
"variation_price_overrides": [
|
||||||
|
{
|
||||||
|
"variation": variations2[0].pk,
|
||||||
|
"price": "99.99"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.content.decode() == '{"non_field_errors":["One or more variations do not belong to this event."]}'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_subevent_detail(token_client, organizer, event, subevent):
|
def test_subevent_detail(token_client, organizer, event, subevent):
|
||||||
res = dict(TEST_SUBEVENT_RES)
|
res = dict(TEST_SUBEVENT_RES)
|
||||||
@@ -58,3 +536,21 @@ def test_subevent_detail(token_client, organizer, event, subevent):
|
|||||||
subevent.pk))
|
subevent.pk))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert res == resp.data
|
assert res == resp.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_subevent_delete(token_client, organizer, event, subevent):
|
||||||
|
resp = token_client.delete('/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug,
|
||||||
|
subevent.pk))
|
||||||
|
assert resp.status_code == 204
|
||||||
|
assert not event.subevents.filter(pk=subevent.id).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_subevent_with_order_position_not_delete(token_client, organizer, event, subevent, item, order_position):
|
||||||
|
resp = token_client.delete('/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug,
|
||||||
|
subevent.pk))
|
||||||
|
assert resp.status_code == 403
|
||||||
|
assert resp.content.decode() == '{"detail":"The sub-event can not be deleted as it has already been used in ' \
|
||||||
|
'orders. Please set \'active\' to false instead to hide it from users."}'
|
||||||
|
assert event.subevents.filter(pk=subevent.id).exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user