mirror of
https://github.com/pretix/pretix.git
synced 2026-08-07 10:17:49 +00:00
API: CartPositions (#948)
This commit is contained in:
@@ -20,3 +20,4 @@ Resources and endpoints
|
||||
vouchers
|
||||
checkinlists
|
||||
waitinglist
|
||||
carts
|
||||
|
||||
@@ -490,6 +490,9 @@ Order endpoints
|
||||
``"n"`` for pending or ``"p"`` for paid. If you create a paid order, the ``order_paid`` signal will **not** be
|
||||
sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and then call
|
||||
the ``mark_paid`` API method.
|
||||
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
||||
order creation is successful. Any quotas that become free by this operation will be credited to your order
|
||||
creation.
|
||||
* ``email``
|
||||
* ``locale``
|
||||
* ``payment_provider`` – The identifier of the payment provider set for this order. This needs to be an existing
|
||||
@@ -580,11 +583,11 @@ Order endpoints
|
||||
{
|
||||
"positionid": 1,
|
||||
"item": 1,
|
||||
"variation": None,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_email": None,
|
||||
"addon_to": None,
|
||||
"attendee_email": null,
|
||||
"addon_to": null,
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
@@ -592,7 +595,7 @@ Order endpoints
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"subevent": None
|
||||
"subevent": null
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer,
|
||||
)
|
||||
from pretix.base.models import Quota
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||
'answers',)
|
||||
|
||||
|
||||
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
'subevent', 'expires', 'includes_tax', 'answers',)
|
||||
|
||||
def create(self, validated_data):
|
||||
answers_data = validated_data.pop('answers')
|
||||
if not validated_data.get('cart_id'):
|
||||
cid = "{}@api".format(get_random_string(48))
|
||||
while CartPosition.objects.filter(cart_id=cid).exists():
|
||||
cid = "{}@api".format(get_random_string(48))
|
||||
validated_data['cart_id'] = cid
|
||||
|
||||
if not validated_data.get('expires'):
|
||||
validated_data['expires'] = now() + timedelta(
|
||||
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
|
||||
)
|
||||
|
||||
with self.context['event'].lock():
|
||||
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
|
||||
if validated_data.get('variation')
|
||||
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||
for quota in new_quotas:
|
||||
avail = quota.availability()
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||
raise ValidationError(
|
||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
)
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options')
|
||||
answ = cp.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
return cp
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
if cid and not cid.endswith('@api'):
|
||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||
|
||||
def validate_item(self, item):
|
||||
if item.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified item does not belong to this event.'
|
||||
)
|
||||
if not item.active:
|
||||
raise ValidationError(
|
||||
'The specified item is not active.'
|
||||
)
|
||||
return item
|
||||
|
||||
def validate_subevent(self, subevent):
|
||||
if self.context['event'].has_subevents:
|
||||
if not subevent:
|
||||
raise ValidationError(
|
||||
'You need to set a subevent.'
|
||||
)
|
||||
if subevent.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified subevent does not belong to this event.'
|
||||
)
|
||||
elif subevent:
|
||||
raise ValidationError(
|
||||
'You cannot set a subevent for this event.'
|
||||
)
|
||||
return subevent
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('item'):
|
||||
if data.get('item').has_variations:
|
||||
if not data.get('variation'):
|
||||
raise ValidationError('You should specify a variation for this item.')
|
||||
else:
|
||||
if data.get('variation').item != data.get('item'):
|
||||
raise ValidationError(
|
||||
'The specified variation does not belong to the specified item.'
|
||||
)
|
||||
elif data.get('variation'):
|
||||
raise ValidationError(
|
||||
'You cannot specify a variation for this item.'
|
||||
)
|
||||
return data
|
||||
@@ -13,7 +13,7 @@ from pretix.base.models import (
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
|
||||
Question, QuestionAnswer, Quota,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.base.models.orders import CartPosition, OrderFee
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
|
||||
@@ -340,11 +340,12 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
comment = serializers.CharField(required=False, allow_blank=True)
|
||||
payment_provider = serializers.CharField(required=True)
|
||||
payment_info = CompatibleJSONField(required=False)
|
||||
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info')
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp not in self.context['event'].get_payment_providers():
|
||||
@@ -399,15 +400,27 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
ia = None
|
||||
|
||||
with self.context['event'].lock():
|
||||
with self.context['event'].lock() as now_dt:
|
||||
quotadiff = Counter()
|
||||
|
||||
consume_carts = validated_data.pop('consume_carts', [])
|
||||
delete_cps = []
|
||||
if consume_carts:
|
||||
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
|
||||
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
||||
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
||||
if cp.expires > now_dt:
|
||||
quotadiff.subtract(quotas)
|
||||
delete_cps.append(cp)
|
||||
|
||||
for pos_data in positions_data:
|
||||
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||
if pos_data.get('variation')
|
||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||
quotadiff.update(new_quotas)
|
||||
|
||||
for quota, diff in quotadiff.items():
|
||||
for quota, diff in quotadiff.items():
|
||||
if diff > 0:
|
||||
avail = quota.availability()
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < diff):
|
||||
raise ValidationError(
|
||||
@@ -445,6 +458,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
options = answ_data.pop('options')
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
|
||||
for cp in delete_cps:
|
||||
cp.delete()
|
||||
for fee_data in fees_data:
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order
|
||||
|
||||
@@ -4,6 +4,8 @@ from django.apps import apps
|
||||
from django.conf.urls import include, url
|
||||
from rest_framework import routers
|
||||
|
||||
from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, event, item, oauth, order, organizer, voucher, waitinglist,
|
||||
)
|
||||
@@ -28,6 +30,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
|
||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||
|
||||
checkinlist_router = routers.DefaultRouter()
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from django.db import transaction
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.serializers.cart import (
|
||||
CartPositionCreateSerializer, CartPositionSerializer,
|
||||
)
|
||||
from pretix.base.models import CartPosition
|
||||
|
||||
|
||||
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = CartPositionSerializer
|
||||
queryset = CartPosition.objects.none()
|
||||
filter_backends = (OrderingFilter,)
|
||||
ordering = ('datetime',)
|
||||
ordering_fields = ('datetime', 'cart_id')
|
||||
lookup_field = 'id'
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
return CartPosition.objects.filter(
|
||||
event=self.request.event,
|
||||
cart_id__endswith="@api"
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = CartPositionCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
self.perform_create(serializer)
|
||||
cp = serializer.instance
|
||||
serializer = CartPositionSerializer(cp, context=serializer.context)
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
@@ -25,7 +25,8 @@ class AllTicketsPDF(BaseExporter):
|
||||
continue
|
||||
if not op.item.admission and not self.event.settings.ticket_download_nonadm:
|
||||
continue
|
||||
o._draw_page(p, op, op.order)
|
||||
layout = o.layout_map.get(op.item_id, o.default_layout)
|
||||
o._draw_page(layout, p, op, op.order)
|
||||
|
||||
p.save()
|
||||
outbuffer = o._render_with_background(buffer)
|
||||
|
||||
@@ -48,3 +48,4 @@ django-countries
|
||||
pyuca # for better sorting of country names in django-countries
|
||||
defusedcsv>=1.0.1
|
||||
vat_moss==0.11.0
|
||||
idna==2.6 # required by current requests
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
import copy
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from pytz import UTC
|
||||
|
||||
from pretix.base.models import Question
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
@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="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def taxrule(event):
|
||||
return event.tax_rules.create(rate=Decimal('19.00'))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def question(event, item):
|
||||
q = event.questions.create(question="T-Shirt size", type="S", identifier="ABC")
|
||||
q.items.add(item)
|
||||
q.options.create(answer="XL", identifier="LVETRWVU")
|
||||
return q
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def question2(event2, item2):
|
||||
q = event2.questions.create(question="T-Shirt size", type="S", identifier="ABC")
|
||||
q.items.add(item2)
|
||||
return q
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def quota(event, item):
|
||||
q = event.quotas.create(name="Budget Quota", size=200)
|
||||
q.items.add(item)
|
||||
return q
|
||||
|
||||
|
||||
TEST_CARTPOSITION_RES = {
|
||||
'id': 1,
|
||||
'cart_id': 'aaa@api',
|
||||
'item': 1,
|
||||
'variation': None,
|
||||
'price': '23.00',
|
||||
'attendee_name': None,
|
||||
'attendee_email': None,
|
||||
'voucher': None,
|
||||
'addon_to': None,
|
||||
'subevent': None,
|
||||
'datetime': '2018-06-11T10:00:00Z',
|
||||
'expires': '2018-06-11T10:00:00Z',
|
||||
'includes_tax': True,
|
||||
'answers': []
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cp_list(token_client, organizer, event, item, taxrule, question):
|
||||
testtime = datetime.datetime(2018, 6, 11, 10, 0, 0, 0, tzinfo=UTC)
|
||||
|
||||
with mock.patch('django.utils.timezone.now') as mock_now:
|
||||
mock_now.return_value = testtime
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="aaa", item=item,
|
||||
price=23,
|
||||
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
|
||||
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
|
||||
)
|
||||
res = dict(TEST_CARTPOSITION_RES)
|
||||
res["id"] = cr.pk
|
||||
res["item"] = item.pk
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/cartpositions/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
assert [] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cp_list_api(token_client, organizer, event, item, taxrule, question):
|
||||
testtime = datetime.datetime(2018, 6, 11, 10, 0, 0, 0, tzinfo=UTC)
|
||||
|
||||
with mock.patch('django.utils.timezone.now') as mock_now:
|
||||
mock_now.return_value = testtime
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="aaa@api", item=item,
|
||||
price=23,
|
||||
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
|
||||
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
|
||||
)
|
||||
res = dict(TEST_CARTPOSITION_RES)
|
||||
res["id"] = cr.pk
|
||||
res["item"] = item.pk
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/cartpositions/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cp_detail(token_client, organizer, event, item, taxrule, question):
|
||||
testtime = datetime.datetime(2018, 6, 11, 10, 0, 0, 0, tzinfo=UTC)
|
||||
|
||||
with mock.patch('django.utils.timezone.now') as mock_now:
|
||||
mock_now.return_value = testtime
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="aaa@api", item=item,
|
||||
price=23,
|
||||
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
|
||||
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
|
||||
)
|
||||
res = dict(TEST_CARTPOSITION_RES)
|
||||
res["id"] = cr.pk
|
||||
res["item"] = item.pk
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/cartpositions/{}/'.format(organizer.slug, event.slug,
|
||||
cr.pk))
|
||||
assert resp.status_code == 200
|
||||
assert res == resp.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cp_delete(token_client, organizer, event, item, taxrule, question):
|
||||
testtime = datetime.datetime(2018, 6, 11, 10, 0, 0, 0, tzinfo=UTC)
|
||||
|
||||
with mock.patch('django.utils.timezone.now') as mock_now:
|
||||
mock_now.return_value = testtime
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="aaa@api", item=item,
|
||||
price=23,
|
||||
datetime=datetime.datetime(2018, 6, 11, 10, 0, 0, 0),
|
||||
expires=datetime.datetime(2018, 6, 11, 10, 0, 0, 0)
|
||||
)
|
||||
res = dict(TEST_CARTPOSITION_RES)
|
||||
res["id"] = cr.pk
|
||||
res["item"] = item.pk
|
||||
resp = token_client.delete('/api/v1/organizers/{}/events/{}/cartpositions/{}/'.format(organizer.slug, event.slug,
|
||||
cr.pk))
|
||||
assert resp.status_code == 204
|
||||
|
||||
|
||||
CARTPOS_CREATE_PAYLOAD = {
|
||||
'cart_id': 'aaa@api',
|
||||
'item': 1,
|
||||
'variation': None,
|
||||
'price': '23.00',
|
||||
'attendee_name': None,
|
||||
'attendee_email': None,
|
||||
'addon_to': None,
|
||||
'subevent': None,
|
||||
'expires': '2018-06-11T10:00:00Z',
|
||||
'includes_tax': True,
|
||||
'answers': []
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
cp = CartPosition.objects.get(pk=resp.data['id'])
|
||||
assert cp.price == Decimal('23.00')
|
||||
assert cp.item == item
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_cart_id_noapi(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
res['cart_id'] = 'aaa'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_cart_id_optional(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
del res['cart_id']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
cp = CartPosition.objects.get(pk=resp.data['id'])
|
||||
assert cp.price == Decimal('23.00')
|
||||
assert cp.item == item
|
||||
assert len(cp.cart_id) > 48
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_subevent_validation(token_client, organizer, event, item, subevent, subevent2, quota, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'subevent': ['You need to set a subevent.']}
|
||||
|
||||
res['subevent'] = subevent2.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'subevent': ['The specified subevent does not belong to this event.']}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_item_validation(token_client, organizer, event, item, item2, quota, question):
|
||||
item.active = False
|
||||
item.save()
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'item': ['The specified item is not active.']}
|
||||
item.active = True
|
||||
item.save()
|
||||
|
||||
res['item'] = item2.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'item': ['The specified item does not belong to this event.']}
|
||||
|
||||
var2 = item2.variations.create(value="A")
|
||||
|
||||
res['item'] = item.pk
|
||||
res['variation'] = var2.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'non_field_errors': ['You cannot specify a variation for this item.']}
|
||||
|
||||
var1 = item.variations.create(value="A")
|
||||
res['item'] = item.pk
|
||||
res['variation'] = var1.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
res['variation'] = var2.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'non_field_errors': ['The specified variation does not belong to the specified item.']}
|
||||
|
||||
res['variation'] = None
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'non_field_errors': ['You should specify a variation for this item.']}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_expires_optional(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
del res['expires']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
cp = CartPosition.objects.get(pk=resp.data['id'])
|
||||
assert cp.price == Decimal('23.00')
|
||||
assert cp.item == item
|
||||
assert cp.expires - now() > datetime.timedelta(minutes=25)
|
||||
assert cp.expires - now() < datetime.timedelta(minutes=35)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_answer_validation(token_client, organizer, event, item, quota, question, question2):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['answers'] = [{
|
||||
"question": 1,
|
||||
"answer": "S",
|
||||
"options": []
|
||||
}]
|
||||
|
||||
res['item'] = item.pk
|
||||
res['answers'][0]['question'] = question2.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'answers': [{'question': ['The specified question does not belong to this event.']}]}
|
||||
|
||||
res['answers'][0]['question'] = question.pk
|
||||
res['answers'][0]['options'] = [question.options.first().pk]
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'answers': [{'non_field_errors': ['You should not specify options if the question is not of a choice type.']}]}
|
||||
|
||||
question.type = Question.TYPE_CHOICE
|
||||
question.save()
|
||||
res['answers'][0]['options'] = []
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'answers': [{'non_field_errors': ['You need to specify options if the question is of a choice type.']}]}
|
||||
|
||||
question.options.create(answer="L")
|
||||
res['answers'][0]['options'] = [
|
||||
question.options.first().pk,
|
||||
question.options.last().pk,
|
||||
]
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'answers': [{'non_field_errors': ['You can specify at most one option for this question.']}]}
|
||||
|
||||
question.type = Question.TYPE_FILE
|
||||
question.save()
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'answers': [{'non_field_errors': ['File uploads are currently not supported via the API.']}]}
|
||||
|
||||
question.type = Question.TYPE_CHOICE_MULTIPLE
|
||||
question.save()
|
||||
res['answers'][0]['options'] = [
|
||||
question.options.first().pk,
|
||||
question.options.last().pk,
|
||||
]
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
pos = CartPosition.objects.get(pk=resp.data['id'])
|
||||
answ = pos.answers.first()
|
||||
assert answ.question == question
|
||||
assert answ.answer == "XL, L"
|
||||
|
||||
question.type = Question.TYPE_NUMBER
|
||||
question.save()
|
||||
res['answers'][0]['options'] = []
|
||||
res['answers'][0]['answer'] = '3.45'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
pos = CartPosition.objects.get(pk=resp.data['id'])
|
||||
answ = pos.answers.first()
|
||||
assert answ.answer == "3.45"
|
||||
|
||||
question.type = Question.TYPE_NUMBER
|
||||
question.save()
|
||||
res['answers'][0]['options'] = []
|
||||
res['answers'][0]['answer'] = 'foo'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'answers': [{'non_field_errors': ['A valid number is required.']}]}
|
||||
|
||||
question.type = Question.TYPE_BOOLEAN
|
||||
question.save()
|
||||
res['answers'][0]['options'] = []
|
||||
res['answers'][0]['answer'] = 'True'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
pos = CartPosition.objects.get(pk=resp.data['id'])
|
||||
answ = pos.answers.first()
|
||||
assert answ.answer == "True"
|
||||
|
||||
question.type = Question.TYPE_BOOLEAN
|
||||
question.save()
|
||||
res['answers'][0]['answer'] = '0'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
pos = CartPosition.objects.get(pk=resp.data['id'])
|
||||
answ = pos.answers.first()
|
||||
assert answ.answer == "False"
|
||||
|
||||
question.type = Question.TYPE_BOOLEAN
|
||||
question.save()
|
||||
res['answers'][0]['answer'] = 'bla'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'answers': [{'non_field_errors': ['Please specify "true" or "false" for boolean questions.']}]}
|
||||
|
||||
question.type = Question.TYPE_DATE
|
||||
question.save()
|
||||
res['answers'][0]['answer'] = '2018-05-14'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
pos = CartPosition.objects.get(pk=resp.data['id'])
|
||||
answ = pos.answers.first()
|
||||
assert answ.answer == "2018-05-14"
|
||||
|
||||
question.type = Question.TYPE_DATE
|
||||
question.save()
|
||||
res['answers'][0]['answer'] = 'bla'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'answers': [{'non_field_errors': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].']}]}
|
||||
|
||||
question.type = Question.TYPE_DATETIME
|
||||
question.save()
|
||||
res['answers'][0]['answer'] = '2018-05-14T13:00:00Z'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
pos = CartPosition.objects.get(pk=resp.data['id'])
|
||||
answ = pos.answers.first()
|
||||
assert answ.answer == "2018-05-14 13:00:00+00:00"
|
||||
|
||||
question.type = Question.TYPE_DATETIME
|
||||
question.save()
|
||||
res['answers'][0]['answer'] = 'bla'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'answers': [{'non_field_errors': [
|
||||
'Datetime has wrong format. Use one of these formats instead: '
|
||||
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].']}]}
|
||||
|
||||
question.type = Question.TYPE_TIME
|
||||
question.save()
|
||||
res['answers'][0]['answer'] = '13:00:00'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
pos = CartPosition.objects.get(pk=resp.data['id'])
|
||||
answ = pos.answers.first()
|
||||
assert answ.answer == "13:00:00"
|
||||
|
||||
question.type = Question.TYPE_TIME
|
||||
question.save()
|
||||
res['answers'][0]['answer'] = 'bla'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'answers': [
|
||||
{'non_field_errors': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].']}]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_quota_validation(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
|
||||
quota.size = 0
|
||||
quota.save()
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['There is not enough quota available on quota "Budget Quota" to perform the operation.']
|
||||
@@ -11,7 +11,7 @@ from django_countries.fields import Country
|
||||
from pytz import UTC
|
||||
|
||||
from pretix.base.models import InvoiceAddress, Order, OrderPosition, Question
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.base.models.orders import CartPosition, OrderFee
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice,
|
||||
)
|
||||
@@ -1618,6 +1618,38 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
|
||||
assert resp.data == ['There is not enough quota available on quota "Budget Quota" to perform the operation.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_quota_consume_cart(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
|
||||
cr = CartPosition.objects.create(
|
||||
event=event, cart_id="uxLJBUMEcnxOLI2EuxLYN1hWJq9GKu4yWL9FEgs2m7M0vdFi@api", item=item,
|
||||
price=23,
|
||||
expires=now() + datetime.timedelta(hours=3)
|
||||
)
|
||||
|
||||
quota.size = 1
|
||||
quota.save()
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['There is not enough quota available on quota "Budget Quota" to perform the operation.']
|
||||
|
||||
res['consume_carts'] = [cr.cart_id]
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert not CartPosition.objects.filter(pk=cr.pk).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_free(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
|
||||
Reference in New Issue
Block a user