From ca1c387a413cc67b913965cce4e419352786aab7 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Sun, 7 Jul 2019 13:36:04 +0200 Subject: [PATCH] Allow quota-level vouchers for hidden products (#1123) * Changes in checks * Backwards-compatible implementation * Add test * Fix voucher bulk form --- doc/api/resources/vouchers.rst | 5 ++++ src/pretix/api/serializers/voucher.py | 2 +- .../0125_voucher_show_hidden_items.py | 26 +++++++++++++++++++ src/pretix/base/models/items.py | 9 +++---- src/pretix/base/models/vouchers.py | 4 +++ src/pretix/base/services/cart.py | 5 +--- src/pretix/base/services/orders.py | 4 +-- src/pretix/control/forms/vouchers.py | 4 +-- .../pretixcontrol/vouchers/bulk.html | 1 + .../pretixcontrol/vouchers/detail.html | 1 + src/tests/api/test_vouchers.py | 1 + src/tests/presale/test_cart.py | 13 ++++++++++ src/tests/presale/test_event.py | 8 ++++++ 13 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 src/pretix/base/migrations/0125_voucher_show_hidden_items.py diff --git a/doc/api/resources/vouchers.rst b/doc/api/resources/vouchers.rst index 1999fe0ffb..d1b15e8764 100644 --- a/doc/api/resources/vouchers.rst +++ b/doc/api/resources/vouchers.rst @@ -41,6 +41,7 @@ quota integer An ID of a quot tag string A string that is used for grouping vouchers comment string An internal comment on the voucher subevent integer ID of the date inside an event series this voucher belongs to (or ``null``). +show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``. ===================================== ========================== ======================================================= @@ -48,6 +49,10 @@ subevent integer ID of the date The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added. +.. versionchanged:: 3.0 + + The attribute ``show_hidden_items`` has been added. + Endpoints --------- diff --git a/src/pretix/api/serializers/voucher.py b/src/pretix/api/serializers/voucher.py index b3e9e3bfb7..f3c493b662 100644 --- a/src/pretix/api/serializers/voucher.py +++ b/src/pretix/api/serializers/voucher.py @@ -27,7 +27,7 @@ class VoucherSerializer(I18nAwareModelSerializer): model = Voucher fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota', 'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota', - 'tag', 'comment', 'subevent') + 'tag', 'comment', 'subevent', 'show_hidden_items') read_only_fields = ('id', 'redeemed') list_serializer_class = VoucherListSerializer diff --git a/src/pretix/base/migrations/0125_voucher_show_hidden_items.py b/src/pretix/base/migrations/0125_voucher_show_hidden_items.py new file mode 100644 index 0000000000..cec393e3b3 --- /dev/null +++ b/src/pretix/base/migrations/0125_voucher_show_hidden_items.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.1 on 2019-07-07 10:10 + +from django.db import migrations, models + + +def set_show_hidden_items(apps, schema_editor): + Voucher = apps.get_model('pretixbase', 'Voucher') + Voucher.objects.filter(quota__isnull=False).update(show_hidden_items=False) + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0124_seat_seat_guid'), + ] + + operations = [ + migrations.AddField( + model_name='voucher', + name='show_hidden_items', + field=models.BooleanField(default=True), + ), + migrations.RunPython( + set_show_hidden_items, + migrations.RunPython.noop, + ) + ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 0119bd45c1..ef652a3871 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -171,12 +171,11 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False): qs = qs.filter(q) vouchq = Q(hide_without_voucher=False) - if voucher: + if voucher and voucher.show_hidden_items: if voucher.item_id: - vouchq |= Q(pk=voucher.item_id) - qs = qs.filter(pk=voucher.item_id) + vouchq = Q(pk=voucher.item_id) elif voucher.quota_id: - qs = qs.filter(quotas__in=[voucher.quota_id]) + vouchq = Q(quotas__in=[voucher.quota_id]) return qs.filter(vouchq) @@ -343,7 +342,7 @@ class Item(LoggedModel): verbose_name=_('This product will only be shown if a voucher matching the product is redeemed.'), default=False, help_text=_('This product will be hidden from the event page until the user enters a voucher ' - 'code that is specifically tied to this product (and not via a quota).') + 'that unlocks this product.') ) require_bundling = models.BooleanField( verbose_name=_('Only sell this product as part of a bundle'), diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 3c81b89b06..4c694b585a 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -176,6 +176,10 @@ class Voucher(LoggedModel): help_text=_("The text entered in this field will not be visible to the user and is available for your " "convenience.") ) + show_hidden_items = models.BooleanField( + verbose_name=_("Shows hidden products that match this voucher"), + default=True + ) objects = ScopedManager(organizer='event__organizer') diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index ffd35e9e96..770d29db9b 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -225,10 +225,7 @@ class CartManager: def _check_item_constraints(self, op): if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation): - if op.item.require_voucher and op.voucher is None: - raise CartError(error_messages['voucher_required']) - - if op.item.hide_without_voucher and (op.voucher is None or op.voucher.item is None or op.voucher.item.pk != op.item.pk): + if (op.item.require_voucher or op.item.hide_without_voucher) and (op.voucher is None or not op.voucher.show_hidden_items): raise CartError(error_messages['voucher_required']) if not op.item.is_available() or (op.variation and not op.variation.active): diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 33a406b494..822b8931e3 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -505,9 +505,9 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio err = err or error_messages['voucher_required'] break - if cp.item.hide_without_voucher and (cp.voucher is None or cp.voucher.item is None - or cp.voucher.item.pk != cp.item.pk): + if cp.item.hide_without_voucher and (cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item.pk, cp.variation.pk)): delete(cp) + cp.delete() err = error_messages['voucher_required'] break diff --git a/src/pretix/control/forms/vouchers.py b/src/pretix/control/forms/vouchers.py index 5288b855eb..506035155a 100644 --- a/src/pretix/control/forms/vouchers.py +++ b/src/pretix/control/forms/vouchers.py @@ -32,7 +32,7 @@ class VoucherForm(I18nModelForm): localized_fields = '__all__' fields = [ 'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', - 'comment', 'max_usages', 'price_mode', 'subevent' + 'comment', 'max_usages', 'price_mode', 'subevent', 'show_hidden_items' ] field_classes = { 'valid_until': SplitDateTimeField, @@ -197,7 +197,7 @@ class VoucherBulkForm(VoucherForm): localized_fields = '__all__' fields = [ 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', - 'max_usages', 'price_mode', 'subevent' + 'max_usages', 'price_mode', 'subevent', 'show_hidden_items' ] field_classes = { 'valid_until': SplitDateTimeField, diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html b/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html index 8896a18687..04997f53f5 100644 --- a/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html +++ b/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html @@ -72,6 +72,7 @@ {% bootstrap_field form.allow_ignore_quota layout="control" %} {% bootstrap_field form.tag layout="control" %} {% bootstrap_field form.comment layout="control" %} + {% bootstrap_field form.show_hidden_items layout="control" %} {% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html index 55d9324448..7b18faa552 100644 --- a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html +++ b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html @@ -74,6 +74,7 @@ {% bootstrap_field form.allow_ignore_quota layout="control" %} {% bootstrap_field form.tag layout="control" %} {% bootstrap_field form.comment layout="control" %} + {% bootstrap_field form.show_hidden_items layout="control" %} {% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
diff --git a/src/tests/api/test_vouchers.py b/src/tests/api/test_vouchers.py index 4fe0906a52..b0a12a3e97 100644 --- a/src/tests/api/test_vouchers.py +++ b/src/tests/api/test_vouchers.py @@ -43,6 +43,7 @@ TEST_VOUCHER_RES = { 'quota': None, 'tag': 'Foo', 'comment': '', + 'show_hidden_items': True, 'subevent': None } diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index da865283d5..52b438f63f 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -1529,6 +1529,19 @@ class CartTest(CartTestMixin, TestCase): self.assertEqual(objs[0].item, self.shirt) self.assertEqual(objs[0].variation, self.shirt_red) + def test_hide_without_voucher_failed_because_of_voucher(self): + with scopes_disabled(): + v = Voucher.objects.create(item=self.shirt, event=self.event, show_hidden_items=False) + self.shirt.hide_without_voucher = True + self.shirt.save() + self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code + }, follow=True) + with scopes_disabled(): + objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) + self.assertEqual(len(objs), 0) + def test_hide_without_voucher_failed(self): self.shirt.hide_without_voucher = True self.shirt.save() diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py index 1cf4dae055..512ac6ce23 100644 --- a/src/tests/presale/test_event.py +++ b/src/tests/presale/test_event.py @@ -435,6 +435,14 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest): assert "Early-bird" in html.rendered_content def test_hide_wo_voucher_quota(self): + self.item.hide_without_voucher = True + self.item.save() + html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code)) + assert "Early-bird" in html.rendered_content + + def test_hide_wo_voucher_quota_dont_show(self): + self.v.show_hidden_items = False + self.v.save() self.item.hide_without_voucher = True self.item.save() html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))