diff --git a/src/pretix/base/migrations/0011_remove_event_max_items_per_order.py b/src/pretix/base/migrations/0011_remove_event_max_items_per_order.py new file mode 100644 index 0000000000..898f55eb91 --- /dev/null +++ b/src/pretix/base/migrations/0011_remove_event_max_items_per_order.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0010_auto_20150218_2048'), + ] + + operations = [ + migrations.RemoveField( + model_name='event', + name='max_items_per_order', + ), + ] diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index fbc04af932..2a16388f0d 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -415,10 +415,6 @@ class Event(Versionable): null=True, blank=True, verbose_name=_("Plugins"), ) - max_items_per_order = models.IntegerField( - verbose_name=_("Maximum number of items per order"), - default=10 - ) class Meta: verbose_name = _("Event") @@ -1460,7 +1456,8 @@ class OrganizerSetting(Versionable): organizer. It will be inherited by the events of this organizer """ DEFAULTS = { - 'user_mail_required': 'False' + 'user_mail_required': 'False', + 'max_items_per_order': '10' } organizer = VersionedForeignKey(Organizer, related_name='setting_objects') key = models.CharField(max_length=255) diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index 2ad320aae7..cce15645ef 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -28,7 +28,6 @@ {% trans "Presale settings" %} {% bootstrap_field form.presale_start layout="horizontal" %} {% bootstrap_field form.presale_end layout="horizontal" %} - {% bootstrap_field form.max_items_per_order layout="horizontal" %}
{% trans "Payment settings" %} diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 1d49f52c64..1b04bc0f7c 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -44,7 +44,6 @@ class EventUpdateForm(VersionedModelForm): 'presale_end', 'payment_term_days', 'payment_term_last', - 'max_items_per_order' ] diff --git a/src/pretix/presale/templates/pretixpresale/event/login.html b/src/pretix/presale/templates/pretixpresale/event/login.html index b4e74ea89a..413aa4987e 100644 --- a/src/pretix/presale/templates/pretixpresale/event/login.html +++ b/src/pretix/presale/templates/pretixpresale/event/login.html @@ -36,7 +36,7 @@
-
+
diff --git a/src/pretix/presale/tests/test_cart.py b/src/pretix/presale/tests/test_cart.py index e798ccfa6e..cb8f2eb03c 100644 --- a/src/pretix/presale/tests/test_cart.py +++ b/src/pretix/presale/tests/test_cart.py @@ -3,7 +3,8 @@ import time from bs4 import BeautifulSoup from django.test import TestCase -from pretix.base.models import Item, Organizer, Event, ItemCategory, Quota, Property, PropertyValue, ItemVariation, User +from pretix.base.models import Item, Organizer, Event, ItemCategory, Quota, Property, PropertyValue, ItemVariation, User, \ + CartPosition from pretix.base.tests import BrowserTest @@ -14,8 +15,8 @@ class CartTestMixin: self.orga = Organizer.objects.create(name='CCC', slug='ccc') self.event = Event.objects.create( organizer=self.orga, name='30C3', slug='30c3', - date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc), - ) + date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc) + ) self.user = User.objects.create_local_user(self.event, 'demo', 'demo') self.category = ItemCategory.objects.create(event=self.event, name="Everything", position=0) self.quota_shirts = Quota.objects.create(event=self.event, name='Shirts', size=2) @@ -63,6 +64,41 @@ class CartBrowserTest(CartTestMixin, BrowserTest): # should display our ticket self.assertIn('Early-bird', self.driver.find_element_by_css_selector('.cart-row:first-child').text) + def test_local_registration(self): + self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug)) + # add the entry ticket to cart + self.driver.find_element_by_css_selector('input[type=number][name=item_%s]' % self.ticket.identity).send_keys('1') + self.scroll_and_click(self.driver.find_element_by_css_selector('.checkout-button-row button')) + # should redirect to login page + # open the login accordion + self.scroll_and_click(self.driver.find_element_by_css_selector('a[href*=localRegistrationForm]')) + time.sleep(1) + # enter login details + self.driver.find_element_by_css_selector('#localRegistrationForm input[name=username]').send_keys('demo2') + self.driver.find_element_by_css_selector('#localRegistrationForm input[name=email]').send_keys('demo@demo.demo') + self.driver.find_element_by_css_selector('#localRegistrationForm input[name=password]').send_keys('demo') + self.driver.find_element_by_css_selector('#localRegistrationForm input[name=password_repeat]').send_keys('demo') + self.scroll_and_click(self.driver.find_element_by_css_selector('#localRegistrationForm button.btn-primary')) + # should display our ticket + self.assertIn('Early-bird', self.driver.find_element_by_css_selector('.cart-row:first-child').text) + + def test_global_registration(self): + self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug)) + # add the entry ticket to cart + self.driver.find_element_by_css_selector('input[type=number][name=item_%s]' % self.ticket.identity).send_keys('1') + self.scroll_and_click(self.driver.find_element_by_css_selector('.checkout-button-row button')) + # should redirect to login page + # open the login accordion + self.scroll_and_click(self.driver.find_element_by_css_selector('a[href*=globalRegistrationForm]')) + time.sleep(1) + # enter login details + self.driver.find_element_by_css_selector('#globalRegistrationForm input[name=email]').send_keys('demo@example.com') + self.driver.find_element_by_css_selector('#globalRegistrationForm input[name=password]').send_keys('demo') + self.driver.find_element_by_css_selector('#globalRegistrationForm input[name=password_repeat]').send_keys('demo') + self.scroll_and_click(self.driver.find_element_by_css_selector('#globalRegistrationForm button.btn-primary')) + # should display our ticket + self.assertIn('Early-bird', self.driver.find_element_by_css_selector('.cart-row:first-child').text) + class CartTest(CartTestMixin, TestCase): @@ -81,6 +117,11 @@ class CartTest(CartTestMixin, TestCase): self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text) + objs = list(CartPosition.objects.filter(user=self.user, event=self.event)) + self.assertEqual(len(objs), 1) + self.assertEqual(objs[0].item, self.ticket) + self.assertIsNone(objs[0].variation) + self.assertEqual(objs[0].price, 23) def test_variation(self): response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { @@ -94,6 +135,11 @@ class CartTest(CartTestMixin, TestCase): self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text) self.assertIn('14', doc.select('.cart .cart-row')[0].select('.price')[0].text) self.assertIn('14', doc.select('.cart .cart-row')[0].select('.price')[1].text) + objs = list(CartPosition.objects.filter(user=self.user, event=self.event)) + self.assertEqual(len(objs), 1) + self.assertEqual(objs[0].item, self.shirt) + self.assertEqual(objs[0].variation, self.shirt_red) + self.assertEqual(objs[0].price, 14) def test_count(self): response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { @@ -106,3 +152,112 @@ class CartTest(CartTestMixin, TestCase): self.assertIn('2', doc.select('.cart .cart-row')[0].select('.count')[0].text) self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text) self.assertIn('46', doc.select('.cart .cart-row')[0].select('.price')[1].text) + objs = list(CartPosition.objects.filter(user=self.user, event=self.event)) + self.assertEqual(len(objs), 2) + for obj in objs: + self.assertEqual(obj.item, self.ticket) + self.assertIsNone(obj.variation) + self.assertEqual(obj.price, 23) + + def test_multiple(self): + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: '2', + 'variation_' + self.shirt.identity + '_' + self.shirt_red.identity: '1' + }, follow=True) + self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('Early-bird', doc.select('.cart')[0].text) + self.assertIn('Shirt', doc.select('.cart')[0].text) + objs = list(CartPosition.objects.filter(user=self.user, event=self.event)) + self.assertEqual(len(objs), 3) + self.assertIn(self.shirt, [obj.item for obj in objs]) + self.assertIn(self.shirt_red, [obj.variation for obj in objs]) + self.assertIn(self.ticket, [obj.item for obj in objs]) + + def test_fuzzy_input(self): + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: 'a', + }, follow=True) + self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('numbers only', doc.select('.alert-danger')[0].text) + self.assertFalse(CartPosition.objects.filter(user=self.user, event=self.event).exists()) + + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + }, follow=True) + self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('did not select any items', doc.select('.alert-warning')[0].text) + self.assertFalse(CartPosition.objects.filter(user=self.user, event=self.event).exists()) + + def test_wrong_event(self): + event2 = Event.objects.create( + organizer=self.orga, name='MRMCD', slug='mrmcd', + date_from=datetime.datetime(2014, 9, 6, tzinfo=datetime.timezone.utc) + ) + shirt2 = Item.objects.create(event=event2, name='T-Shirt', default_price=12) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_' + shirt2.identity: '1', + }, follow=True) + self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('not available', doc.select('.alert-danger')[0].text) + self.assertFalse(CartPosition.objects.filter(user=self.user, event=self.event).exists()) + + def test_no_quota(self): + shirt2 = Item.objects.create(event=self.event, name='T-Shirt', default_price=12) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_' + shirt2.identity: '1', + }, follow=True) + self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('no longer available', doc.select('.alert-danger')[0].text) + self.assertFalse(CartPosition.objects.filter(user=self.user, event=self.event).exists()) + + def test_quota_max_items(self): + self.event.settings.max_items_per_order = 5 + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: '6', + }, follow=True) + self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('more than', doc.select('.alert-danger')[0].text) + self.assertFalse(CartPosition.objects.filter(user=self.user, event=self.event).exists()) + + def test_quota_full(self): + self.quota_tickets.size = 0 + self.quota_tickets.save() + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: '1', + }, follow=True) + self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('no longer available', doc.select('.alert-danger')[0].text) + self.assertFalse(CartPosition.objects.filter(user=self.user, event=self.event).exists()) + + def test_quota_partly(self): + self.quota_tickets.size = 1 + self.quota_tickets.save() + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: '2', + }, follow=True) + self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('no longer available', doc.select('.alert-danger')[0].text) + self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text) + self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text) + self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text) + self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text) + objs = list(CartPosition.objects.filter(user=self.user, event=self.event)) + self.assertEqual(len(objs), 1) + self.assertEqual(objs[0].item, self.ticket) + self.assertIsNone(objs[0].variation) + self.assertEqual(objs[0].price, 23) diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index b8213a48b0..48f7a8e673 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -111,10 +111,10 @@ class CartAdd(EventViewMixin, CartActionMixin, View): if not items: return redirect(self.get_failure_url()) existing = CartPosition.objects.current.filter(user=self.request.user, event=self.request.event).count() - if sum(i[2] for i in items) + existing > self.request.event.max_items_per_order: + if sum(i[2] for i in items) + existing > int(self.request.event.settings.max_items_per_order): # TODO: i18n plurals messages.error(self.request, - _("You cannot select more than %d items per order") % self.event.max_items_per_order) + _("You cannot select more than %s items per order") % self.request.event.settings.max_items_per_order) return redirect(self.get_failure_url()) # Extend this user's cart session to 30 minutes from now to ensure all items in the diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 865d11c551..6e7dd336b4 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -40,13 +40,13 @@ class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): if not item.has_variations: item.cached_availability = list(item.check_quotas()) item.cached_availability[1] = min(item.cached_availability[1], - self.request.event.max_items_per_order) + int(self.request.event.settings.max_items_per_order)) item.price = item.available_variations[0]['price'] else: for var in item.available_variations: var.cached_availability = list(var['variation'].check_quotas()) var.cached_availability[1] = min(var.cached_availability[1], - self.request.event.max_items_per_order) + int(self.request.event.settings.max_items_per_order)) items = [item for item in items if len(item.available_variations) > 0] @@ -65,7 +65,11 @@ class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): class LoginForm(BaseAuthenticationForm): username = forms.CharField( label=_('Username'), - help_text=_('If you registered for multiple events, your username is your email address.') + help_text=( + _('If you registered for multiple events, your username is your email address.') + if settings.PRETIX_GLOBAL_REGISTRATION + else None + ) ) password = forms.CharField( label=_('Password'), @@ -245,7 +249,7 @@ class EventLogin(EventViewMixin, TemplateView): user = authenticate(identifier=user.identifier, password=form.cleaned_data['password']) login(request, user) return self.redirect_to_next() - elif request.POST.get('form') == 'global_registration': + elif request.POST.get('form') == 'global_registration' and settings.PRETIX_GLOBAL_REGISTRATION: form = self.global_registration_form if form.is_valid(): user = User.objects.create_global_user(