diff --git a/src/pretix/base/channels.py b/src/pretix/base/channels.py index e3295c4b5..f6db148df 100644 --- a/src/pretix/base/channels.py +++ b/src/pretix/base/channels.py @@ -53,6 +53,14 @@ class SalesChannel: """ return True + @property + def unlimited_items_per_order(self) -> bool: + """ + If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of + items defined in the event settings. + """ + return False + def get_all_sales_channels(): global _ALL_CHANNELS diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index f92c754fa..8e0d8c0f8 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -12,6 +12,7 @@ from django.utils.timezone import make_aware, now from django.utils.translation import pgettext_lazy, ugettext as _ from django_scopes import scopes_disabled +from pretix.base.channels import get_all_sales_channels from pretix.base.i18n import language from pretix.base.models import ( CartPosition, Event, InvoiceAddress, Item, ItemBundle, ItemVariation, Seat, @@ -218,13 +219,14 @@ class CartManager: }) def _check_max_cart_size(self): - cartsize = self.positions.filter(addon_to__isnull=True).count() - cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to]) - cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if - not op.position.addon_to_id]) - if cartsize > int(self.event.settings.max_items_per_order): - # TODO: i18n plurals - raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,)) + if not get_all_sales_channels()[self._sales_channel].unlimited_items_per_order: + cartsize = self.positions.filter(addon_to__isnull=True).count() + cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to]) + cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if + not op.position.addon_to_id]) + if cartsize > int(self.event.settings.max_items_per_order): + # TODO: i18n plurals + raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,)) def _check_item_constraints(self, op): if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation): diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 92fcce5a7..7dd01f87c 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -17,6 +17,7 @@ from django.views import View from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView +from pretix.base.channels import get_all_sales_channels from pretix.base.models import ItemVariation, Quota, SeatCategoryMapping from pretix.base.models.event import SubEvent from pretix.base.models.items import ItemBundle @@ -119,7 +120,10 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require item.available_variations = [v for v in item.available_variations if v.pk == voucher.variation_id] - max_per_order = item.max_per_order or int(event.settings.max_items_per_order) + if get_all_sales_channels()[channel].unlimited_items_per_order: + max_per_order = sys.maxsize + else: + max_per_order = item.max_per_order or int(event.settings.max_items_per_order) if item.hidden_if_available: q = item.hidden_if_available.availability(_cache=quota_cache) diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index 94f03124d..e553c9025 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -4,6 +4,7 @@ from datetime import timedelta from decimal import Decimal from bs4 import BeautifulSoup +from django.dispatch import receiver from django.test import TestCase from django.utils.timezone import now from django_countries.fields import Country @@ -21,6 +22,7 @@ from pretix.base.models.items import ( from pretix.base.services.cart import ( CartError, CartManager, error_messages, update_tax_rates, ) +from pretix.base.signals import register_sales_channels from pretix.testutils.scope import classscope from pretix.testutils.sessions import get_cart_session_key @@ -29,7 +31,15 @@ class FoobarSalesChannel(SalesChannel): identifier = "bar" verbose_name = "Foobar" icon = "home" - testmode_supported = True + testmode_supported = False + unlimited_items_per_order = True + + +@receiver(register_sales_channels, dispatch_uid="test_cart_register_sales_channels") +def base_sales_channels(sender, **kwargs): + return ( + FoobarSalesChannel(), + ) class CartTestMixin: @@ -816,6 +826,23 @@ class CartTest(CartTestMixin, TestCase): with scopes_disabled(): self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1) + def test_max_items_unlimited_sales_channel(self): + with scopes_disabled(): + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + self.event.settings.max_items_per_order = 5 + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '5', + }, follow=True, PRETIX_SALES_CHANNEL=FoobarSalesChannel) + self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertNotIn('more than', doc.select('.alert-danger')[0].text) + with scopes_disabled(): + self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1) + def test_max_per_item_failed(self): self.ticket.max_per_order = 2 self.ticket.save()