Merge branch 'master' of github.com:pretix/pretix

This commit is contained in:
Raphael Michel
2015-03-04 11:40:05 +01:00
16 changed files with 524 additions and 192 deletions

View File

@@ -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',
),
]

View File

@@ -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")
@@ -1223,7 +1219,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)
@@ -1233,13 +1228,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
@@ -1304,7 +1296,6 @@ class Quota(Versionable):
)
self.locked_here = None
self.locked = None
self.event.get_cache().delete('quota_paid_%s' % self.identity)
return updated
@@ -1488,7 +1479,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)

View File

@@ -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()
self.driver = getattr(webdriver, BROWSER)()
self.driver.set_window_size(1920, 1080)
self.driver.implicitly_wait(10)
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.set_window_size(1920, 1080)
self.driver.implicitly_wait(3)
def tearDownLocal(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()

View File

@@ -28,7 +28,6 @@
<legend>{% trans "Presale settings" %}</legend>
{% bootstrap_field form.presale_start layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" %}
{% bootstrap_field form.max_items_per_order layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Payment settings" %}</legend>

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -44,7 +44,6 @@ class EventUpdateForm(VersionedModelForm):
'presale_end',
'payment_term_days',
'payment_term_last',
'max_items_per_order'
]

View File

@@ -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:

View File

@@ -6,4 +6,10 @@ from pretix.base.signals import determine_availability
@receiver(determine_availability)
def availability_handler(sender, **kwargs):
kwargs['sender'] = sender
return kwargs
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')
return variations
return []

View File

@@ -3,7 +3,7 @@
{% block content %}
{% if cart.positions %}
<div class="panel panel-primary">
<div class="panel panel-primary cart">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Your cart" %}</h3>
</div>

View File

@@ -36,7 +36,7 @@
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a data-toggle="collapse" href="#localRegisterForm" data-parent="#login_accordion">
<a data-toggle="collapse" href="#localRegistrationForm" data-parent="#login_accordion">
{% if global_registration_form %}
{% trans "I want to create a new account just for this event" %}
{% else %}
@@ -45,7 +45,7 @@
</a>
</h4>
</div>
<div id="localRegisterForm" class="panel-collapse collapse {% if request.POST.form == 'local_registration' %}in{% endif %}">
<div id="localRegistrationForm" class="panel-collapse collapse {% if request.POST.form == 'local_registration' %}in{% endif %}">
<div class="panel-body">
<div class="panel-body">
<form class="form-horizontal" method="post">

View File

@@ -0,0 +1,410 @@
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
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)
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):
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)
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), {
'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)
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), {
'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)
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_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: '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.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)
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)
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)

View File

@@ -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):

View File

@@ -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,36 +91,46 @@ 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()
# 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(
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'
)
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 > self.request.event.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)
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,24 +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):
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())

View File

@@ -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):
@@ -38,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]
@@ -63,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'),
@@ -205,6 +211,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:
@@ -237,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(