From 89a5c65df7521697284760d07f1f699149e7fd48 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 19 Feb 2015 23:50:58 +0100 Subject: [PATCH 1/8] Premature optimization indeed is the root of all evil --- src/pretix/base/models.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 302a6f1e75..fbc04af932 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -1215,7 +1215,6 @@ class Quota(Versionable): # TODO: Test for interference with old versions of Item-Quota-relations, etc. # TODO: Prevent corner-cases like people having ordered an item before it got # its first variationsadded - cache = self.event.get_cache() quotalookup = ( ( # Orders for items which do not have any variations Q(variation__isnull=True) @@ -1225,13 +1224,10 @@ class Quota(Versionable): ) ) - paid_orders = cache.get('quota_paid_%s' % self.identity) - if paid_orders is None: - paid_orders = OrderPosition.objects.current.filter( - Q(order__status=Order.STATUS_PAID) - & quotalookup - ).count() - cache.set('quota_paid_%s' % self.identity, paid_orders) + paid_orders = OrderPosition.objects.current.filter( + Q(order__status=Order.STATUS_PAID) + & quotalookup + ).count() if paid_orders >= self.size: return Quota.AVAILABILITY_GONE, 0 @@ -1296,7 +1292,6 @@ class Quota(Versionable): ) self.locked_here = None self.locked = None - self.event.get_cache().delete('quota_paid_%s' % self.identity) return updated From 38a56e06d203d616d4f019d3092abaef1021493e Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 20 Feb 2015 00:07:08 +0100 Subject: [PATCH 2/8] Remove a TODO statement --- src/pretix/control/views/item.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 51e7315f0c..868c05d321 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -473,8 +473,6 @@ class QuotaForm(ModelForm): if self.instance.pk is not None and isinstance(self.instance, Versionable): if self.has_changed(): self.instance = self.instance.clone_shallow() - # TODO: order_cache, lock_cache are emptied by that but you'll have - # to rebuild them anyway return super().save(commit) class Meta: From 4da23d33b552f018f83bd4cf8b40e4f392a2aa64 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 20 Feb 2015 01:05:23 +0100 Subject: [PATCH 3/8] Remove legacy multi-browser and Sauce test runner code --- src/pretix/base/tests/__init__.py | 103 ++++-------------------- src/pretix/control/tests/test_auth.py | 3 +- src/pretix/control/tests/test_events.py | 3 +- src/pretix/control/tests/test_items.py | 16 +--- 4 files changed, 17 insertions(+), 108 deletions(-) diff --git a/src/pretix/base/tests/__init__.py b/src/pretix/base/tests/__init__.py index c2a580227f..fd32190b80 100644 --- a/src/pretix/base/tests/__init__.py +++ b/src/pretix/base/tests/__init__.py @@ -1,60 +1,14 @@ import os import sys +import time from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.conf import settings from selenium import webdriver -RUN_LOCAL = ('SAUCE_USERNAME' not in os.environ) -""" -For a long time, we used SauceLabs for CI testing, because they provide free -browser VMs for Open Source projects. However, more tests failed because of -connection timeouts to SauceLabs than for real reasons, so we're using -PhantomJS now. However, we'll keep the SauceClient code here as it might prove -useful some day. -""" -if RUN_LOCAL: - # could add Chrome, Firefox, etc... here - BROWSERS = [os.environ.get('TEST_BROWSER', 'PhantomJS')] -else: - from sauceclient import SauceClient - USERNAME = os.environ.get('SAUCE_USERNAME') - ACCESS_KEY = os.environ.get('SAUCE_ACCESS_KEY') - sauce = SauceClient(USERNAME, ACCESS_KEY) - - BROWSERS = [ - {"platform": "Mac OS X 10.9", - "browserName": "chrome", - "version": "35"}, - {"platform": "Windows 8.1", - "browserName": "internet explorer", - "version": "11"}, - {"platform": "Linux", - "browserName": "firefox", - "version": "29"}] - - -def on_platforms(): - if RUN_LOCAL: - def decorator(base_class): - module = sys.modules[base_class.__module__].__dict__ - for i, platform in enumerate(BROWSERS): - d = dict(base_class.__dict__) - d['browser'] = platform - name = "%s_%s" % (base_class.__name__, i + 1) - module[name] = type(name, (base_class,), d) - pass - return decorator - - def decorator(base_class): - module = sys.modules[base_class.__module__].__dict__ - for i, platform in enumerate(BROWSERS): - d = dict(base_class.__dict__) - d['desired_capabilities'] = platform - name = "%s_%s" % (base_class.__name__, i + 1) - module[name] = type(name, (base_class,), d) - return decorator +# could use Chrome, Firefox, etc... here +BROWSER = os.environ.get('TEST_BROWSER', 'PhantomJS') class BrowserTest(StaticLiveServerTestCase): @@ -64,48 +18,19 @@ class BrowserTest(StaticLiveServerTestCase): settings.DEBUG = ('--debug' in sys.argv) def setUp(self): - if RUN_LOCAL: - self.setUpLocal() - else: - self.setUpSauce() - - def tearDown(self): - if RUN_LOCAL: - self.tearDownLocal() - else: - self.tearDownSauce() - - def setUpSauce(self): - if 'TRAVIS_JOB_NUMBER' in os.environ: - self.desired_capabilities['tunnel-identifier'] = \ - os.environ['TRAVIS_JOB_NUMBER'] - self.desired_capabilities['build'] = os.environ['TRAVIS_BUILD_NUMBER'] - self.desired_capabilities['tags'] = \ - [os.environ['TRAVIS_PYTHON_VERSION'], 'CI'] - self.desired_capabilities['name'] = self.id() - - sauce_url = "http://%s:%s@ondemand.saucelabs.com:80/wd/hub" - self.driver = webdriver.Remote( - desired_capabilities=self.desired_capabilities, - command_executor=sauce_url % (USERNAME, ACCESS_KEY) - ) - self.driver.implicitly_wait(5) - - def setUpLocal(self): - self.driver = getattr(webdriver, self.browser)() + self.driver = getattr(webdriver, BROWSER)() self.driver.set_window_size(1920, 1080) self.driver.implicitly_wait(3) - def tearDownLocal(self): + def tearDown(self): self.driver.quit() - def tearDownSauce(self): - print("\nLink to your job: \n " - "https://saucelabs.com/jobs/%s \n" % self.driver.session_id) - try: - if sys.exc_info() == (None, None, None): - sauce.jobs.update_job(self.driver.session_id, passed=True) - else: - sauce.jobs.update_job(self.driver.session_id, passed=False) - finally: - self.driver.quit() + def scroll_into_view(self, element): + """Scroll element into view""" + y = element.location['y'] + self.driver.execute_script('window.scrollTo(0, {0})'.format(y)) + + def scroll_and_click(self, element): + self.scroll_into_view(element) + time.sleep(0.5) + element.click() diff --git a/src/pretix/control/tests/test_auth.py b/src/pretix/control/tests/test_auth.py index b870936303..c6351ad886 100644 --- a/src/pretix/control/tests/test_auth.py +++ b/src/pretix/control/tests/test_auth.py @@ -1,10 +1,9 @@ from django.test import TestCase, Client from pretix.base.models import User -from pretix.base.tests import BrowserTest, on_platforms +from pretix.base.tests import BrowserTest -@on_platforms() class LoginFormBrowserTest(BrowserTest): def setUp(self): diff --git a/src/pretix/control/tests/test_events.py b/src/pretix/control/tests/test_events.py index 1596cc647e..2a3f7c4bcd 100644 --- a/src/pretix/control/tests/test_events.py +++ b/src/pretix/control/tests/test_events.py @@ -1,9 +1,8 @@ import datetime from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission -from pretix.base.tests import BrowserTest, on_platforms +from pretix.base.tests import BrowserTest -@on_platforms() class EventsTest(BrowserTest): def setUp(self): diff --git a/src/pretix/control/tests/test_items.py b/src/pretix/control/tests/test_items.py index ae9d1858b2..28df55a921 100644 --- a/src/pretix/control/tests/test_items.py +++ b/src/pretix/control/tests/test_items.py @@ -5,7 +5,7 @@ import unittest from selenium.webdriver.support.select import Select from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission, ItemCategory, Property, \ PropertyValue, Question, Quota, Item -from pretix.base.tests import BrowserTest, on_platforms +from pretix.base.tests import BrowserTest class ItemFormTest(BrowserTest): @@ -30,18 +30,7 @@ class ItemFormTest(BrowserTest): self.driver.find_element_by_css_selector('button[type="submit"]').click() self.driver.find_element_by_class_name("navbar-right") - def scroll_into_view(self, element): - """Scroll element into view""" - y = element.location['y'] - self.driver.execute_script('window.scrollTo(0, {0})'.format(y)) - def scroll_and_click(self, element): - self.scroll_into_view(element) - time.sleep(0.5) - element.click() - - -@on_platforms() class CategoriesTest(ItemFormTest): def test_create(self): @@ -109,7 +98,6 @@ class CategoriesTest(ItemFormTest): self.assertNotIn("Entry tickets", self.driver.find_element_by_css_selector(".container table").text) -@on_platforms() class PropertiesTest(ItemFormTest): def test_create(self): @@ -156,7 +144,6 @@ class PropertiesTest(ItemFormTest): self.assertNotIn("Size", self.driver.find_element_by_css_selector(".container table").text) -@on_platforms() class QuestionsTest(ItemFormTest): def test_create(self): @@ -193,7 +180,6 @@ class QuestionsTest(ItemFormTest): self.assertNotIn("shoe size", self.driver.find_element_by_css_selector(".container table").text) -@on_platforms() class QuotaTest(ItemFormTest): def test_create(self): From 504081cce330415d528554949f0fabe6796569af Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 20 Feb 2015 01:14:23 +0100 Subject: [PATCH 4/8] Basic tests for cart features --- .../templates/pretixpresale/event/index.html | 2 +- src/pretix/presale/tests/test_event.py | 63 +++++++++++++++++-- src/pretix/presale/views/cart.py | 6 +- src/pretix/presale/views/event.py | 8 +++ 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html index 1a0977ea7a..39c8e495ec 100644 --- a/src/pretix/presale/templates/pretixpresale/event/index.html +++ b/src/pretix/presale/templates/pretixpresale/event/index.html @@ -3,7 +3,7 @@ {% block content %} {% if cart.positions %} -
+

{% trans "Your cart" %}

diff --git a/src/pretix/presale/tests/test_event.py b/src/pretix/presale/tests/test_event.py index ad84a732be..4de978a73a 100644 --- a/src/pretix/presale/tests/test_event.py +++ b/src/pretix/presale/tests/test_event.py @@ -1,11 +1,10 @@ import datetime -from django.test import TestCase, Client +import time -from pretix.base.models import Item, Organizer, Event, ItemCategory, Quota, Property, PropertyValue, ItemVariation -from pretix.base.tests import BrowserTest, on_platforms +from pretix.base.models import Item, Organizer, Event, ItemCategory, Quota, Property, PropertyValue, ItemVariation, User +from pretix.base.tests import BrowserTest -@on_platforms() class EventMiddlewareTest(BrowserTest): def setUp(self): @@ -26,7 +25,6 @@ class EventMiddlewareTest(BrowserTest): self.assertEqual(resp.status_code, 404) -@on_platforms() class ItemDisplayTest(BrowserTest): def setUp(self): @@ -120,3 +118,58 @@ class ItemDisplayTest(BrowserTest): self.driver.find_elements_by_css_selector("section:nth-of-type(1) div.variation")[1].text) self.assertIn("12.00", self.driver.find_elements_by_css_selector("section:nth-of-type(1) div.variation")[1].text) + + +class CartTest(BrowserTest): + + def setUp(self): + super().setUp() + 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), + ) + self.user = User.objects.create_local_user(self.event, 'demo', 'demo') + self.driver.implicitly_wait(10) + 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) + self.shirt = Item.objects.create(event=self.event, name='T-Shirt', category=self.category, default_price=12) + prop1 = Property.objects.create(event=self.event, name="Color") + self.shirt.properties.add(prop1) + val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0) + val2 = PropertyValue.objects.create(prop=prop1, value="Black", position=1) + self.quota_shirts.items.add(self.shirt) + self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14) + self.shirt_red.values.add(val1) + var2 = ItemVariation.objects.create(item=self.shirt) + var2.values.add(val2) + self.quota_shirts.variations.add(self.shirt_red) + self.quota_shirts.variations.add(var2) + self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=1) + self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', + category=self.category, default_price=23) + self.quota_tickets.items.add(self.ticket) + + def test_not_logged_in(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 + self.driver.find_element_by_name('username') + + def test_simple_login(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*=loginForm]')) + time.sleep(1) + # enter login details + self.driver.find_element_by_css_selector('#loginForm input[name=username]').send_keys('demo') + self.driver.find_element_by_css_selector('#loginForm input[name=password]').send_keys('demo') + self.scroll_and_click(self.driver.find_element_by_css_selector('#loginForm button.btn-primary')) + # should display our ticket + self.assertIn('Early-bird', self.driver.find_element_by_css_selector('.cart-row:first-child').text) diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 77b1fb8391..c61c063fa0 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -100,7 +100,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): (request.user.event is None or request.user.event == request.event): request.session['cart_tmp'] = json.dumps(items) return redirect_to_login( - request.path, reverse('presale:event.checkout.login', kwargs={ + self.get_success_url(), reverse('presale:event.checkout.login', kwargs={ 'organizer': request.event.organizer.slug, 'event': request.event.slug, }), 'next' @@ -239,8 +239,4 @@ class CartAdd(EventViewMixin, CartActionMixin, View): return redirect(self.get_success_url()) def get(self, request, *args, **kwargs): - if 'cart_tmp' in request.session and request.user.is_authenticated(): - items = json.loads(request.session['cart_tmp']) - del request.session['cart_tmp'] - return self.process(items) return redirect(self.get_failure_url()) diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 4a822196d0..865d11c551 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -1,3 +1,4 @@ +import json from django.contrib.auth import authenticate from django.core.urlresolvers import reverse from django.core.validators import RegexValidator @@ -13,6 +14,7 @@ from django.conf import settings from pretix.base.models import User from pretix.presale.views import EventViewMixin, CartDisplayMixin +from pretix.presale.views.cart import CartAdd class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): @@ -205,6 +207,12 @@ class EventLogin(EventViewMixin, TemplateView): template_name = 'pretixpresale/event/login.html' def redirect_to_next(self): + if 'cart_tmp' in self.request.session and self.request.user.is_authenticated(): + items = json.loads(self.request.session['cart_tmp']) + del self.request.session['cart_tmp'] + ca = CartAdd() + ca.request = self.request + return ca.process(items) if 'next' in self.request.GET: return redirect(self.request.GET.get('next')) else: From e13d11be78bd3b1feedbc1e9be536617798944d6 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 20 Feb 2015 22:00:07 +0100 Subject: [PATCH 5/8] Cart tests --- src/pretix/base/tests/__init__.py | 2 +- src/pretix/presale/tests/test_cart.py | 108 +++++++++++++++++++++++++ src/pretix/presale/tests/test_event.py | 55 ------------- src/pretix/presale/views/cart.py | 2 +- 4 files changed, 110 insertions(+), 57 deletions(-) create mode 100644 src/pretix/presale/tests/test_cart.py diff --git a/src/pretix/base/tests/__init__.py b/src/pretix/base/tests/__init__.py index fd32190b80..dfc720572f 100644 --- a/src/pretix/base/tests/__init__.py +++ b/src/pretix/base/tests/__init__.py @@ -20,7 +20,7 @@ class BrowserTest(StaticLiveServerTestCase): def setUp(self): self.driver = getattr(webdriver, BROWSER)() self.driver.set_window_size(1920, 1080) - self.driver.implicitly_wait(3) + self.driver.implicitly_wait(10) def tearDown(self): self.driver.quit() diff --git a/src/pretix/presale/tests/test_cart.py b/src/pretix/presale/tests/test_cart.py new file mode 100644 index 0000000000..e798ccfa6e --- /dev/null +++ b/src/pretix/presale/tests/test_cart.py @@ -0,0 +1,108 @@ +import datetime +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.tests import BrowserTest + + +class CartTestMixin: + + def setUp(self): + super().setUp() + 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), + ) + 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) + self.shirt = Item.objects.create(event=self.event, name='T-Shirt', category=self.category, default_price=12) + prop1 = Property.objects.create(event=self.event, name="Color") + self.shirt.properties.add(prop1) + val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0) + val2 = PropertyValue.objects.create(prop=prop1, value="Black", position=1) + self.quota_shirts.items.add(self.shirt) + self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14) + self.shirt_red.values.add(val1) + var2 = ItemVariation.objects.create(item=self.shirt) + var2.values.add(val2) + self.quota_shirts.variations.add(self.shirt_red) + self.quota_shirts.variations.add(var2) + self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=5) + self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', + category=self.category, default_price=23) + self.quota_tickets.items.add(self.ticket) + + +class CartBrowserTest(CartTestMixin, BrowserTest): + + def test_not_logged_in(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 + self.driver.find_element_by_name('username') + + def test_simple_login(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*=loginForm]')) + time.sleep(1) + # enter login details + self.driver.find_element_by_css_selector('#loginForm input[name=username]').send_keys('demo') + self.driver.find_element_by_css_selector('#loginForm input[name=password]').send_keys('demo') + self.scroll_and_click(self.driver.find_element_by_css_selector('#loginForm 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): + + def setUp(self): + super().setUp() + self.assertTrue(self.client.login(username='demo@%s.event.pretix' % self.event.identity, password='demo')) + + def test_simple(self): + 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('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) + + def test_variation(self): + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + '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('Shirt', doc.select('.cart .cart-row')[0].select('strong')[0].text) + self.assertIn('Red', doc.select('.cart .cart-row')[0].text) + 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) + + def test_count(self): + 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('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text) + 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) diff --git a/src/pretix/presale/tests/test_event.py b/src/pretix/presale/tests/test_event.py index 4de978a73a..f72040181a 100644 --- a/src/pretix/presale/tests/test_event.py +++ b/src/pretix/presale/tests/test_event.py @@ -118,58 +118,3 @@ class ItemDisplayTest(BrowserTest): self.driver.find_elements_by_css_selector("section:nth-of-type(1) div.variation")[1].text) self.assertIn("12.00", self.driver.find_elements_by_css_selector("section:nth-of-type(1) div.variation")[1].text) - - -class CartTest(BrowserTest): - - def setUp(self): - super().setUp() - 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), - ) - self.user = User.objects.create_local_user(self.event, 'demo', 'demo') - self.driver.implicitly_wait(10) - 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) - self.shirt = Item.objects.create(event=self.event, name='T-Shirt', category=self.category, default_price=12) - prop1 = Property.objects.create(event=self.event, name="Color") - self.shirt.properties.add(prop1) - val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0) - val2 = PropertyValue.objects.create(prop=prop1, value="Black", position=1) - self.quota_shirts.items.add(self.shirt) - self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14) - self.shirt_red.values.add(val1) - var2 = ItemVariation.objects.create(item=self.shirt) - var2.values.add(val2) - self.quota_shirts.variations.add(self.shirt_red) - self.quota_shirts.variations.add(var2) - self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=1) - self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', - category=self.category, default_price=23) - self.quota_tickets.items.add(self.ticket) - - def test_not_logged_in(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 - self.driver.find_element_by_name('username') - - def test_simple_login(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*=loginForm]')) - time.sleep(1) - # enter login details - self.driver.find_element_by_css_selector('#loginForm input[name=username]').send_keys('demo') - self.driver.find_element_by_css_selector('#loginForm input[name=password]').send_keys('demo') - self.scroll_and_click(self.driver.find_element_by_css_selector('#loginForm button.btn-primary')) - # should display our ticket - self.assertIn('Early-bird', self.driver.find_element_by_css_selector('.cart-row:first-child').text) diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index c61c063fa0..b8213a48b0 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -97,7 +97,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # We do not use EventLoginRequiredMixin here, as we want to store stuff into the # session beforehand if not request.user.is_authenticated() or \ - (request.user.event is None or request.user.event == request.event): + (request.user.event is not None and request.user.event != request.event): request.session['cart_tmp'] = json.dumps(items) return redirect_to_login( self.get_success_url(), reverse('presale:event.checkout.login', kwargs={ From 497cbe17afe96c974b05b653d86efb69f91183aa Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Sat, 21 Feb 2015 15:33:53 +0100 Subject: [PATCH 6/8] Add more tests + Event.max_items_pre_order should be a settings --- .../0011_remove_event_max_items_per_order.py | 18 ++ src/pretix/base/models.py | 7 +- .../pretixcontrol/event/settings.html | 1 - src/pretix/control/views/event.py | 1 - .../templates/pretixpresale/event/login.html | 4 +- src/pretix/presale/tests/test_cart.py | 161 +++++++++++++++++- src/pretix/presale/views/cart.py | 4 +- src/pretix/presale/views/event.py | 12 +- 8 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 src/pretix/base/migrations/0011_remove_event_max_items_per_order.py 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( From fd252b0ff5b7eeb6c406122af5da28c9832e09b0 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Sat, 21 Feb 2015 17:00:52 +0100 Subject: [PATCH 7/8] Complete test suite for cart actions --- src/pretix/plugins/testdummy/signals.py | 8 +- src/pretix/presale/tests/test_cart.py | 161 ++++++++++++++++++++++-- src/pretix/presale/views/cart.py | 87 ++++++------- 3 files changed, 202 insertions(+), 54 deletions(-) diff --git a/src/pretix/plugins/testdummy/signals.py b/src/pretix/plugins/testdummy/signals.py index 3e8248e4f9..a1301a2c41 100644 --- a/src/pretix/plugins/testdummy/signals.py +++ b/src/pretix/plugins/testdummy/signals.py @@ -6,4 +6,10 @@ from pretix.base.signals import determine_availability @receiver(determine_availability) def availability_handler(sender, **kwargs): kwargs['sender'] = sender - return kwargs + variations = kwargs['variations'] + if sender.settings.testdummy_available is not None: + variations = [d.copy() for d in variations] + for v in variations: + v['available'] = (sender.settings.testdummy_available == 'yes') + return variations + return [] diff --git a/src/pretix/presale/tests/test_cart.py b/src/pretix/presale/tests/test_cart.py index cb8f2eb03c..3feb23423e 100644 --- a/src/pretix/presale/tests/test_cart.py +++ b/src/pretix/presale/tests/test_cart.py @@ -2,6 +2,8 @@ import datetime import time from bs4 import BeautifulSoup from django.test import TestCase +from django.utils.timezone import now +from datetime import timedelta from pretix.base.models import Item, Organizer, Event, ItemCategory, Quota, Property, PropertyValue, ItemVariation, User, \ CartPosition @@ -219,23 +221,27 @@ class CartTest(CartTestMixin, TestCase): 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): + def test_max_items(self): + CartPosition.objects.create( + event=self.event, user=self.user, 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_' + self.ticket.identity: '6', - }, follow=True) + 'item_' + self.ticket.identity: '5', + }, 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()) + self.assertEqual(CartPosition.objects.filter(user=self.user, event=self.event).count(), 1) 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) + }, follow=True) self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), target_status_code=200) doc = BeautifulSoup(response.rendered_content) @@ -246,8 +252,8 @@ class CartTest(CartTestMixin, TestCase): 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) + '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) @@ -261,3 +267,144 @@ class CartTest(CartTestMixin, TestCase): self.assertEqual(objs[0].item, self.ticket) self.assertIsNone(objs[0].variation) self.assertEqual(objs[0].price, 23) + + def test_renew_in_time(self): + cp = CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + }, follow=True) + cp = CartPosition.objects.current.get(identity=cp.identity) + self.assertGreater(cp.expires, now()) + + def test_renew_expired_successfully(self): + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() - timedelta(minutes=10) + ) + self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + }, follow=True) + objs = list(CartPosition.objects.current.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) + self.assertGreater(objs[0].expires, now()) + + def test_renew_expired_failed(self): + self.quota_tickets.size = 0 + self.quota_tickets.save() + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() - timedelta(minutes=10) + ) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + }, follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('no longer available', doc.select('.alert-danger')[0].text) + self.assertFalse(CartPosition.objects.current.filter(user=self.user, event=self.event).exists()) + + def test_restriction_failed(self): + self.event.plugins = 'pretix.plugins.testdummy' + self.event.save() + self.event.settings.testdummy_available = 'yes' + 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) + objs = list(CartPosition.objects.current.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_restriction_ok(self): + self.event.plugins = 'pretix.plugins.testdummy' + self.event.save() + self.event.settings.testdummy_available = 'no' + 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_remove_simple(self): + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + response = self.client.post('/%s/%s/cart/remove' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: '1', + }, follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('updated', doc.select('.alert-success')[0].text) + self.assertFalse(CartPosition.objects.current.filter(user=self.user, event=self.event).exists()) + + def test_remove_variation(self): + CartPosition.objects.create( + event=self.event, user=self.user, item=self.shirt, variation=self.shirt_red, + price=14, expires=now() + timedelta(minutes=10) + ) + response = self.client.post('/%s/%s/cart/remove' % (self.orga.slug, self.event.slug), { + 'variation_' + self.shirt.identity + '_' + self.shirt_red.identity: '1', + }, follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('updated', doc.select('.alert-success')[0].text) + self.assertFalse(CartPosition.objects.current.filter(user=self.user, event=self.event).exists()) + + def test_remove_one_of_multiple(self): + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + response = self.client.post('/%s/%s/cart/remove' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: '1', + }, follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('updated', doc.select('.alert-success')[0].text) + self.assertEqual(CartPosition.objects.current.filter(user=self.user, event=self.event).count(), 1) + + def test_remove_multiple(self): + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + response = self.client.post('/%s/%s/cart/remove' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: '2', + }, follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('updated', doc.select('.alert-success')[0].text) + self.assertFalse(CartPosition.objects.current.filter(user=self.user, event=self.event).exists()) + + def test_remove_most_expensive(self): + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + CartPosition.objects.create( + event=self.event, user=self.user, item=self.ticket, + price=20, expires=now() + timedelta(minutes=10) + ) + response = self.client.post('/%s/%s/cart/remove' % (self.orga.slug, self.event.slug), { + 'item_' + self.ticket.identity: '1', + }, follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertIn('updated', doc.select('.alert-success')[0].text) + objs = list(CartPosition.objects.current.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, 20) diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 48f7a8e673..0052d75220 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -47,16 +47,16 @@ class CartActionMixin: items.append((key.split("_")[1], None, int(value))) except ValueError: messages.error(self.request, _('Please enter numbers only.')) - return False + return [] elif key.startswith('variation_'): try: items.append((key.split("_")[1], key.split("_")[2], int(value))) except ValueError: messages.error(self.request, _('Please enter numbers only.')) - return False + return [] if len(items) == 0: messages.warning(self.request, _('You did not select any items.')) - return False + return [] return items def _re_add_position(self, items, position): @@ -91,6 +91,21 @@ class CartRemove(EventViewMixin, CartActionMixin, EventLoginRequiredMixin, View) class CartAdd(EventViewMixin, CartActionMixin, View): + error_messages = { + 'unavailable': _('Some of the items you selected were no longer available. ' + 'Please see below for details.'), + 'in_part': _('Some of the items you selected were no longer available in ' + 'the quantity you selected. Please see below for details.'), + 'busy': _('We were not able to process your request completely as the ' + 'server was too busy. Please try again.'), + 'not_for_sale': _('You selected an item which is not available for sale.'), + 'max_items': _("You cannot select more than %s items per order"), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.msg_some_unavailable = False + def post(self, request, *args, **kwargs): items = self._items_from_post_data() @@ -107,20 +122,15 @@ class CartAdd(EventViewMixin, CartActionMixin, View): ) return self.process(items) - def process(self, items): - 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 > int(self.request.event.settings.max_items_per_order): - # TODO: i18n plurals - messages.error(self.request, - _("You cannot select more than %s items per order") % self.request.event.settings.max_items_per_order) - return redirect(self.get_failure_url()) + def error_message(self, msg, important=False): + if not self.msg_some_unavailable or important: + self.msg_some_unavailable = True + messages.error(self.request, msg) + def process(self, items): # Extend this user's cart session to 30 minutes from now to ensure all items in the - # cart expire at the same - # We can extend the reservation of items which are not yet expired without - # risk + # cart expire at the same time + # We can extend the reservation of items which are not yet expired without risk CartPosition.objects.current.filter( Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__gt=now()) ).update(expires=now() + timedelta(minutes=30)) @@ -132,6 +142,15 @@ class CartAdd(EventViewMixin, CartActionMixin, View): items = self._re_add_position(items, cp) cp.delete() + 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 > int(self.request.event.settings.max_items_per_order): + # TODO: i18n plurals + self.error_message(self.error_messages['max_items'] % self.request.event.settings.max_items_per_order) + return redirect(self.get_failure_url()) + # Fetch items from the database items_cache = { i.identity: i for i @@ -149,13 +168,12 @@ class CartAdd(EventViewMixin, CartActionMixin, View): } # Process the request itself - msg_some_unavailable = False for i in items: # Check whether the specified items are part of what we just fetched from the database # If they are not, the user supplied item IDs which either do not exist or belong to # a different event if i[0] not in items_cache or (i[1] is not None and i[1] not in variations_cache): - messages.error(self.request, _('You selected an item which is not available for sale.')) + self.error_message(self.error_messages['not_for_sale']) return redirect(self.get_failure_url()) item = items_cache[i[0]] @@ -166,21 +184,13 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # will correctly return the default price price = item.check_restrictions() if variation is None else variation.check_restrictions() if price is False: - if not msg_some_unavailable: - msg_some_unavailable = True - messages.error(self.request, - _('Some of the items you selected were no longer available. ' - 'Please see below for details.')) + self.error_message(self.error_messages['unavailable']) continue # Fetch all quotas. If there are no quotas, this item is not allowed to be sold. quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all()) if len(quotas) == 0: - if not msg_some_unavailable: - msg_some_unavailable = True - messages.error(self.request, - _('Some of the items you selected were no longer available. ' - 'Please see below for details.')) + self.error_message(self.error_messages['unavailable']) continue # Assume that all quotas allow us to buy i[2] instances of the object @@ -193,21 +203,13 @@ class CartAdd(EventViewMixin, CartActionMixin, View): avail = quota.availability() if avail[0] != Quota.AVAILABILITY_OK: # This quota is sold out/currently unavailable, so do not sell this at all - if not msg_some_unavailable: - msg_some_unavailable = True - messages.error(self.request, - _('Some of the items you selected were no longer available. ' - 'Please see below for details.')) + self.error_message(self.error_messages['unavailable']) quota_ok = 0 break elif avail[1] < i[2]: # This quota is available, but with less than i[2] items left, so we have to # reduce the number of bought items - if not msg_some_unavailable: - msg_some_unavailable = True - messages.error(self.request, - _('Some of the items you selected were no longer available in ' - 'the quantity you selected. Please see below for details.')) + self.error_message(self.error_messages['in_part']) quota_ok = min(quota_ok, avail[1]) # Create a CartPosition for as much items as we can @@ -223,20 +225,13 @@ class CartAdd(EventViewMixin, CartActionMixin, View): except Quota.LockTimeoutException: # Is raised when there are too many threads asking for quota locks and we were # unaible to get one - if not msg_some_unavailable: - msg_some_unavailable = True - messages.error(self.request, - _('We were not able to process your request completely as the ' - 'server was too busy. Please try again.')) + self.error_message(self.error_messages['busy'], important=True) finally: # Release the locks. This is important ;) for quota in quotas: quota.release() - if not msg_some_unavailable: + if not self.msg_some_unavailable: messages.success(self.request, _('The items have been successfully added to your cart.')) return redirect(self.get_success_url()) - - def get(self, request, *args, **kwargs): - return redirect(self.get_failure_url()) From dade162154bd561d2d9d957a013ed07eda3f0319 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Sat, 21 Feb 2015 17:02:57 +0100 Subject: [PATCH 8/8] Fix failing plugins test --- src/pretix/plugins/testdummy/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pretix/plugins/testdummy/signals.py b/src/pretix/plugins/testdummy/signals.py index a1301a2c41..d1a2e608ee 100644 --- a/src/pretix/plugins/testdummy/signals.py +++ b/src/pretix/plugins/testdummy/signals.py @@ -6,8 +6,8 @@ from pretix.base.signals import determine_availability @receiver(determine_availability) def availability_handler(sender, **kwargs): kwargs['sender'] = sender - variations = kwargs['variations'] if sender.settings.testdummy_available is not None: + variations = kwargs['variations'] variations = [d.copy() for d in variations] for v in variations: v['available'] = (sender.settings.testdummy_available == 'yes')