diff --git a/src/pretix/base/migrations/0024_auto_20160728_1725.py b/src/pretix/base/migrations/0024_auto_20160728_1725.py new file mode 100644 index 000000000..cfa29a6a0 --- /dev/null +++ b/src/pretix/base/migrations/0024_auto_20160728_1725.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-07-28 17:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0023_auto_20160601_1039'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='hide_without_voucher', + field=models.BooleanField(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).', verbose_name='This product will only be shown if a voucher is redeemed.'), + ), + migrations.AddField( + model_name='item', + name='require_voucher', + field=models.BooleanField(default=False, help_text='To buy this product, the user needs a voucher that applies to this product either directly or via a quota.', verbose_name='This product can only be bought using a voucher.'), + ), + ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 15d90e6d2..09ee726f1 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -173,6 +173,18 @@ class Item(LoggedModel): null=True, blank=True, help_text=_('This product will not be sold after the given date.') ) + require_voucher = models.BooleanField( + verbose_name=_('This product can only be bought using a voucher.'), + default=False, + help_text=_('To buy this product, the user needs a voucher that applies to this product ' + 'either directly or via a quota.') + ) + hide_without_voucher = models.BooleanField( + verbose_name=_('This product will only be shown if a voucher 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).') + ) class Meta: verbose_name = _("Product") diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index b8edaafa2..fd285a4ef 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -32,7 +32,8 @@ error_messages = { 'voucher_invalid': _('This voucher code is not known in our database.'), 'voucher_redeemed': _('This voucher code has already been used an can only be used once.'), 'voucher_expired': _('This voucher is expired.'), - 'voucher_invalid_item': _('This voucher is not valid for this item.'), + 'voucher_invalid_item': _('This voucher is not valid for this product.'), + 'voucher_required': _('You need a valid voucher code to order this product.'), } @@ -130,6 +131,12 @@ def _add_new_items(event: Event, items: List[dict], if voucher and voucher.quota and voucher.quota.pk not in [q.pk for q in quotas]: return error_messages['voucher_invalid_item'] + if item.require_voucher and voucher is None: + return error_messages['voucher_required'] + + if item.hide_without_voucher and (voucher is None or voucher.item is None or voucher.item.pk != item.pk): + return error_messages['voucher_required'] + if len(quotas) == 0 or not item.is_available(): err = err or error_messages['unavailable'] continue diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index d27c00b46..cedb9c87d 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -142,6 +142,8 @@ class ItemUpdateForm(I18nModelForm): 'tax_rate', 'available_from', 'available_until', + 'require_voucher', + 'hide_without_voucher', ] diff --git a/src/pretix/control/templates/pretixcontrol/item/index.html b/src/pretix/control/templates/pretixcontrol/item/index.html index 470abc4ce..556607ec5 100644 --- a/src/pretix/control/templates/pretixcontrol/item/index.html +++ b/src/pretix/control/templates/pretixcontrol/item/index.html @@ -23,6 +23,8 @@ {% trans "Availability" %} {% bootstrap_field form.available_from layout="horizontal" %} {% bootstrap_field form.available_until layout="horizontal" %} + {% bootstrap_field form.require_voucher layout="horizontal" %} + {% bootstrap_field form.hide_without_voucher layout="horizontal" %}
- {% if item.cached_availability.0 == 100 %} + {% if item.require_voucher %} +
+ + {% trans "Enter a voucher code below to buy this ticket." %} + +
+ {% elif item.cached_availability.0 == 100 %}
diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index ee59cbd19..9dc048f64 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -154,12 +154,15 @@ class RedeemView(EventViewMixin, TemplateView): & Q(Q(available_until__isnull=True) | Q(available_until__gte=now())) ) + vouchq = Q(hide_without_voucher=False) + if self.voucher.item_id: + vouchq |= Q(pk=self.voucher.item_id) items = items.filter(pk=self.voucher.item_id) elif self.voucher.quota_id: items = items.filter(quotas__in=[self.voucher.quota_id]) - items = items.select_related( + items = items.filter(vouchq).select_related( 'category', # for re-grouping ).prefetch_related( 'quotas', 'variations__quotas', 'quotas__event' # for .availability() diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index eb6de7fd5..18e74a9a5 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -31,6 +31,7 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView): Q(active=True) & Q(Q(available_from__isnull=True) | Q(available_from__lte=now())) & Q(Q(available_until__isnull=True) | Q(available_until__gte=now())) + & Q(hide_without_voucher=False) ).select_related( 'category', # for re-grouping ).prefetch_related( diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index 48ba32523..222583e8a 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -578,3 +578,45 @@ class CartTest(CartTestMixin, TestCase): doc = BeautifulSoup(response.rendered_content) self.assertIn('already been used', doc.select('.alert-danger')[0].text) self.assertEqual(1, CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count()) + + def test_require_voucher(self): + v = Voucher.objects.create(quota=self.quota_shirts, event=self.event) + self.shirt.require_voucher = True + self.shirt.save() + self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + }, follow=True) + objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) + self.assertEqual(len(objs), 1) + self.assertEqual(objs[0].item, self.shirt) + self.assertEqual(objs[0].variation, self.shirt_red) + + def test_require_voucher_failed(self): + self.shirt.require_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', + }, follow=True) + objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) + self.assertEqual(len(objs), 0) + + def test_hide_without_voucher(self): + v = Voucher.objects.create(item=self.shirt, event=self.event) + 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_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + }, follow=True) + objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) + self.assertEqual(len(objs), 1) + self.assertEqual(objs[0].item, self.shirt) + self.assertEqual(objs[0].variation, self.shirt_red) + + def test_hide_without_voucher_failed(self): + 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', + }, follow=True) + objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) + self.assertEqual(len(objs), 0) diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py index ccbf204bd..d63d730ba 100644 --- a/src/tests/presale/test_event.py +++ b/src/tests/presale/test_event.py @@ -96,6 +96,14 @@ class ItemDisplayTest(EventTestMixin, BrowserTest): self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug)) self.assertNotIn("Early-bird", self.driver.find_element_by_css_selector("body").text) + def test_hidden_without_voucher(self): + q = Quota.objects.create(event=self.event, name='Quota', size=2) + item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True, + hide_without_voucher=True) + q.items.add(item) + self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug)) + self.assertNotIn("Early-bird", self.driver.find_element_by_css_selector("body").text) + def test_simple_with_category(self): c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0) q = Quota.objects.create(event=self.event, name='Quota', size=2)