diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py
index 23b20d36c5..a5640ef573 100644
--- a/src/pretix/api/serializers/order.py
+++ b/src/pretix/api/serializers/order.py
@@ -675,6 +675,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(errs)
return data
+ def validate_testmode(self, testmode):
+ if 'sales_channel' in self.initial_data:
+ try:
+ sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
+
+ if testmode and not sales_channel.testmode_supported:
+ raise ValidationError('This sales channel does not provide support for testmode.')
+ except KeyError:
+ # We do not need to raise a ValidationError here, since there is another check to validate the
+ # sales_channel
+ pass
+
+ return testmode
+
def create(self, validated_data):
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
diff --git a/src/pretix/base/channels.py b/src/pretix/base/channels.py
index 8e4855587f..0a89cc2257 100644
--- a/src/pretix/base/channels.py
+++ b/src/pretix/base/channels.py
@@ -35,6 +35,13 @@ class SalesChannel:
"""
return "circle"
+ @property
+ def testmode_supported(self) -> bool:
+ """
+ Indication, if a saleschannels supports test mode orders
+ """
+ return True
+
def get_all_sales_channels():
global _ALL_CHANNELS
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index 76471e1464..11dff6e4c6 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -17,6 +17,7 @@ from django.utils.translation import ugettext as _
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
+from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_email_context
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import (
@@ -595,6 +596,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
meta_info: dict=None, sales_channel: str='web', gift_cards: list=None,
shown_total=None):
p = None
+ sales_channel = get_all_sales_channels()[sales_channel]
+
with transaction.atomic():
checked_gift_cards = []
if gift_cards:
@@ -622,10 +625,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
datetime=now_dt,
locale=locale,
total=total,
- testmode=event.testmode,
+ testmode=True if sales_channel.testmode_supported and event.testmode else False,
meta_info=json.dumps(meta_info or {}),
require_approval=any(p.item.require_approval for p in positions),
- sales_channel=sales_channel
+ sales_channel=sales_channel.identifier
)
order.set_expires(now_dt, event.subevents.filter(id__in=[p.subevent_id for p in positions]))
order.save()
diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py
index 00e34ab850..89c6af2851 100644
--- a/src/pretix/presale/checkoutflow.py
+++ b/src/pretix/presale/checkoutflow.py
@@ -247,7 +247,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
quota_cache=quota_cache,
item_cache=item_cache,
subevent=cartpos.subevent,
- sales_channel=self.request.sales_channel
+ sales_channel=self.request.sales_channel.identifier
)
}
@@ -306,7 +306,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
return self.do(self.request.event.id, data, get_or_create_cart_id(self.request),
invoice_address=self.invoice_address.pk, locale=get_language(),
- sales_channel=request.sales_channel)
+ sales_channel=request.sales_channel.identifier)
class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
@@ -711,7 +711,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
return self.do(self.request.event.id, self.payment_provider.identifier if self.payment_provider else None,
[p.id for p in self.positions], self.cart_session.get('email'),
translation.get_language(), self.invoice_address.pk, meta_info,
- request.sales_channel, self.cart_session.get('gift_cards'),
+ request.sales_channel.identifier, self.cart_session.get('gift_cards'),
self.cart_session.get('shown_total'))
def get_success_message(self, value):
diff --git a/src/pretix/presale/templates/pretixpresale/event/base.html b/src/pretix/presale/templates/pretixpresale/event/base.html
index d18dc273b8..336a5e285f 100644
--- a/src/pretix/presale/templates/pretixpresale/event/base.html
+++ b/src/pretix/presale/templates/pretixpresale/event/base.html
@@ -66,11 +66,19 @@
{% if request.event.testmode %}
-
-
- {% trans "This ticket shop is currently in test mode. Please do not perform any real purchases as your order might be deleted without notice." %}
-
-
+ {% if request.sales_channel.testmode_supported %}
+
+
+ {% trans "This ticket shop is currently in test mode. Please do not perform any real purchases as your order might be deleted without notice." %}
+
+
+ {% else %}
+
+
+ {% trans "Orders made through this sales channel cannot be deleted - even if the ticket shop is in test mode!" %}
+
+
+ {% endif %}
{% endif %}
{% if messages %}
{% for message in messages %}
@@ -82,11 +90,19 @@
{% block content %}
{% endblock %}
{% if request.event.testmode %}
-
-
- {% trans "This ticket shop is currently in test mode. Please do not perform any real purchases as your order might be deleted without notice." %}
-
-
+ {% if request.sales_channel.testmode_supported %}
+
+
+ {% trans "This ticket shop is currently in test mode. Please do not perform any real purchases as your order might be deleted without notice." %}
+
+
+ {% else %}
+
+
+ {% trans "Orders made through this sales channel cannot be deleted - even if the ticket shop is in test mode!" %}
+
+
+ {% endif %}
{% endif %}
{% endblock %}
{% block footer %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html
index 43b26a5940..c9bf860772 100644
--- a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html
+++ b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html
@@ -36,6 +36,14 @@
{{ p.provider.test_mode_message }}
+ {% if not request.event.sales_channel.testmode_supported %}
+
+ {% trans "This sales channel does not provide support for testmode." %}
+
+ {% trans "If you continue, you might pay an actual order with non-existing money!" %}
+
+
+ {% endif %}
{% else %}
{% trans "This payment provider does not provide support for testmode." %}
diff --git a/src/pretix/presale/utils.py b/src/pretix/presale/utils.py
index ad5216402a..190df1c00b 100644
--- a/src/pretix/presale/utils.py
+++ b/src/pretix/presale/utils.py
@@ -11,6 +11,7 @@ from django.urls import resolve
from django.utils.translation import ugettext_lazy as _
from django_scopes import scope
+from pretix.base.channels import WebshopSalesChannel
from pretix.base.middleware import LocaleMiddleware
from pretix.base.models import Event, Organizer
from pretix.multidomain.urlreverse import get_domain
@@ -103,7 +104,7 @@ def _detect_event(request, require_live=True, require_plugin=None):
if not hasattr(request, 'sales_channel'):
# The environ lookup is only relevant during unit testing
- request.sales_channel = request.environ.get('PRETIX_SALES_CHANNEL', 'web')
+ request.sales_channel = request.environ.get('PRETIX_SALES_CHANNEL', WebshopSalesChannel())
for receiver, response in process_request.send(request.event, request=request):
if response:
return response
diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py
index b019e154e7..330906f6de 100644
--- a/src/pretix/presale/views/cart.py
+++ b/src/pretix/presale/views/cart.py
@@ -401,7 +401,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
items = self._items_from_post_data()
if items:
return self.do(self.request.event.id, items, cart_id, translation.get_language(),
- self.invoice_address.pk, widget_data, self.request.sales_channel)
+ self.invoice_address.pk, widget_data, self.request.sales_channel.identifier)
else:
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
return JsonResponse({
@@ -424,7 +424,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
# Fetch all items
items, display_add_to_cart = get_grouped_items(self.request.event, self.subevent,
- voucher=self.voucher, channel=self.request.sales_channel)
+ voucher=self.voucher, channel=self.request.sales_channel.identifier)
# Calculate how many options the user still has. If there is only one option, we can
# check the box right away ;)
diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py
index 873d88ed42..c788e8f7db 100644
--- a/src/pretix/presale/views/event.py
+++ b/src/pretix/presale/views/event.py
@@ -294,7 +294,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
if not self.request.event.has_subevents or self.subevent:
# Fetch all items
items, display_add_to_cart = get_grouped_items(self.request.event, self.subevent,
- channel=self.request.sales_channel)
+ channel=self.request.sales_channel.identifier)
context['itemnum'] = len(items)
# Regroup those by category
@@ -335,7 +335,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
ebd = defaultdict(list)
add_subevents_for_days(
- filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel).using(settings.DATABASE_REPLICA), self.request),
+ filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel.identifier).using(settings.DATABASE_REPLICA), self.request),
before, after, ebd, set(), self.request.event,
kwargs.get('cart_namespace')
)
@@ -345,7 +345,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['years'] = range(now().year - 2, now().year + 3)
else:
context['subevent_list'] = self.request.event.subevents_sorted(
- filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel).using(settings.DATABASE_REPLICA), self.request)
+ filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel.identifier).using(settings.DATABASE_REPLICA), self.request)
)
context['show_cart'] = (
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index 0008dfe918..15c58399c5 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -393,7 +393,7 @@ class WidgetAPIProductList(EventListMixin, View):
else:
if hasattr(self.request, 'event'):
evs = self.request.event.subevents_sorted(
- filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel), self.request)
+ filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel.identifier), self.request)
)
tz = pytz.timezone(request.event.settings.timezone)
data['events'] = [
diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py
index 400e56e3a8..4bcab2ad6c 100644
--- a/src/tests/api/test_orders.py
+++ b/src/tests/api/test_orders.py
@@ -6,6 +6,7 @@ from unittest import mock
import pytest
from django.core import mail as djmail
+from django.dispatch import receiver
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scopes_disabled
@@ -13,6 +14,7 @@ from pytz import UTC
from stripe.error import APIConnectionError
from tests.plugins.stripe.test_provider import MockedCharge
+from pretix.base.channels import SalesChannel
from pretix.base.models import (
InvoiceAddress, Order, OrderPosition, Question, SeatingPlan,
)
@@ -22,6 +24,21 @@ from pretix.base.models.orders import (
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice,
)
+from pretix.base.signals import register_sales_channels
+
+
+class FoobarSalesChannel(SalesChannel):
+ identifier = "bar"
+ verbose_name = "Foobar"
+ icon = "home"
+ testmode_supported = False
+
+
+@receiver(register_sales_channels, dispatch_uid="test_orders_register_sales_channels")
+def base_sales_channels(sender, **kwargs):
+ return (
+ FoobarSalesChannel(),
+ )
@pytest.fixture
@@ -1536,6 +1553,22 @@ def test_order_create_in_test_mode(token_client, organizer, event, item, quota,
assert o.testmode
+@pytest.mark.django_db
+def test_order_create_in_test_mode_saleschannel_limited(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
+ res['testmode'] = True
+ res['sales_channel'] = 'bar'
+ 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 == {'testmode': ['This sales channel does not provide support for testmode.']}
+
+
@pytest.mark.django_db
def test_order_create_attendee_name_optional(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py
index 258b8701e5..412fa137b7 100644
--- a/src/tests/base/test_orders.py
+++ b/src/tests/base/test_orders.py
@@ -4,11 +4,13 @@ from decimal import Decimal
import pytest
import pytz
from django.core import mail as djmail
+from django.dispatch import receiver
from django.test import TestCase
from django.utils.timezone import make_aware, now
from django_countries.fields import Country
from django_scopes import scope
+from pretix.base.channels import SalesChannel
from pretix.base.decimal import round_decimal
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, Order, OrderPosition, Organizer,
@@ -23,10 +25,25 @@ from pretix.base.services.orders import (
OrderChangeManager, OrderError, _create_order, approve_order, cancel_order,
deny_order, expire_orders, send_download_reminders, send_expiry_warnings,
)
+from pretix.base.signals import register_sales_channels
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.testutils.scope import classscope
+class FoobarSalesChannel(SalesChannel):
+ identifier = "bar"
+ verbose_name = "Foobar"
+ icon = "home"
+ testmode_supported = False
+
+
+@receiver(register_sales_channels, dispatch_uid="test_orders_register_sales_channels")
+def base_sales_channels(sender, **kwargs):
+ return (
+ FoobarSalesChannel(),
+ )
+
+
@pytest.fixture(scope='function')
def event():
o = Organizer.objects.create(name='Dummy', slug='dummy')
@@ -1960,6 +1977,38 @@ def test_autocheckin(clist_autocheckin, event):
assert order.positions.first().checkins.count() == 0
+@pytest.mark.django_db
+def test_saleschannel_testmode_restriction(event):
+ today = now()
+ tr7 = event.tax_rules.create(rate=Decimal('17.00'))
+ ticket = Item.objects.create(event=event, name='Early-bird ticket', tax_rule=tr7,
+ default_price=Decimal('23.00'), admission=True)
+ cp1 = CartPosition.objects.create(
+ item=ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123"
+ )
+
+ order = _create_order(event, email='dummy@example.org', positions=[cp1],
+ now_dt=today, payment_provider=FreeOrderProvider(event),
+ locale='de', sales_channel='web')[0]
+ assert not order.testmode
+
+ order = _create_order(event, email='dummy@example.org', positions=[cp1],
+ now_dt=today, payment_provider=FreeOrderProvider(event),
+ locale='de', sales_channel=FoobarSalesChannel.identifier)[0]
+ assert not order.testmode
+
+ event.testmode = True
+ order = _create_order(event, email='dummy@example.org', positions=[cp1],
+ now_dt=today, payment_provider=FreeOrderProvider(event),
+ locale='de', sales_channel='web')[0]
+ assert order.testmode
+
+ order = _create_order(event, email='dummy@example.org', positions=[cp1],
+ now_dt=today, payment_provider=FreeOrderProvider(event),
+ locale='de', sales_channel=FoobarSalesChannel.identifier)[0]
+ assert not order.testmode
+
+
@pytest.mark.django_db
def test_giftcard_multiple(event):
ticket = Item.objects.create(event=event, name='Early-bird ticket',
diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py
index 52b438f63f..85cf8324a5 100644
--- a/src/tests/presale/test_cart.py
+++ b/src/tests/presale/test_cart.py
@@ -9,6 +9,7 @@ from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scopes_disabled
+from pretix.base.channels import SalesChannel
from pretix.base.decimal import round_decimal
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, ItemCategory, ItemVariation,
@@ -24,6 +25,13 @@ from pretix.testutils.scope import classscope
from pretix.testutils.sessions import get_cart_session_key
+class FoobarSalesChannel(SalesChannel):
+ identifier = "bar"
+ verbose_name = "Foobar"
+ icon = "home"
+ testmode_supported = True
+
+
class CartTestMixin:
@scopes_disabled()
def setUp(self):
@@ -759,7 +767,7 @@ class CartTest(CartTestMixin, TestCase):
self.ticket.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
- }, follow=True, PRETIX_SALES_CHANNEL='bar')
+ }, follow=True, PRETIX_SALES_CHANNEL=FoobarSalesChannel)
with scopes_disabled():
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1)
diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py
index 00784d6a05..5bcc6696cd 100644
--- a/src/tests/presale/test_event.py
+++ b/src/tests/presale/test_event.py
@@ -12,6 +12,7 @@ from django_scopes import scopes_disabled
from pytz import timezone
from tests.base import SoupTest
+from pretix.base.channels import SalesChannel
from pretix.base.models import (
Event, Item, ItemCategory, ItemVariation, Order, Organizer, Quota, Team,
User, WaitingListEntry,
@@ -19,6 +20,13 @@ from pretix.base.models import (
from pretix.base.models.items import SubEventItem, SubEventItemVariation
+class FoobarSalesChannel(SalesChannel):
+ identifier = "bar"
+ verbose_name = "Foobar"
+ icon = "home"
+ testmode_supported = True
+
+
class EventTestMixin:
@scopes_disabled()
def setUp(self):
@@ -109,7 +117,7 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
q.items.add(item)
html = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)).rendered_content
self.assertNotIn("Early-bird", html)
- html = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug), PRETIX_SALES_CHANNEL="bar").rendered_content
+ html = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug), PRETIX_SALES_CHANNEL=FoobarSalesChannel).rendered_content
self.assertIn("Early-bird", html)
def test_timely_available(self):