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, null=True, blank=True,
verbose_name=_("Plugins"), verbose_name=_("Plugins"),
) )
max_items_per_order = models.IntegerField(
verbose_name=_("Maximum number of items per order"),
default=10
)
class Meta: class Meta:
verbose_name = _("Event") verbose_name = _("Event")
@@ -1223,7 +1219,6 @@ class Quota(Versionable):
# TODO: Test for interference with old versions of Item-Quota-relations, etc. # 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 # TODO: Prevent corner-cases like people having ordered an item before it got
# its first variationsadded # its first variationsadded
cache = self.event.get_cache()
quotalookup = ( quotalookup = (
( # Orders for items which do not have any variations ( # Orders for items which do not have any variations
Q(variation__isnull=True) Q(variation__isnull=True)
@@ -1233,13 +1228,10 @@ class Quota(Versionable):
) )
) )
paid_orders = cache.get('quota_paid_%s' % self.identity) paid_orders = OrderPosition.objects.current.filter(
if paid_orders is None: Q(order__status=Order.STATUS_PAID)
paid_orders = OrderPosition.objects.current.filter( & quotalookup
Q(order__status=Order.STATUS_PAID) ).count()
& quotalookup
).count()
cache.set('quota_paid_%s' % self.identity, paid_orders)
if paid_orders >= self.size: if paid_orders >= self.size:
return Quota.AVAILABILITY_GONE, 0 return Quota.AVAILABILITY_GONE, 0
@@ -1304,7 +1296,6 @@ class Quota(Versionable):
) )
self.locked_here = None self.locked_here = None
self.locked = None self.locked = None
self.event.get_cache().delete('quota_paid_%s' % self.identity)
return updated return updated
@@ -1488,7 +1479,8 @@ class OrganizerSetting(Versionable):
organizer. It will be inherited by the events of this organizer organizer. It will be inherited by the events of this organizer
""" """
DEFAULTS = { DEFAULTS = {
'user_mail_required': 'False' 'user_mail_required': 'False',
'max_items_per_order': '10'
} }
organizer = VersionedForeignKey(Organizer, related_name='setting_objects') organizer = VersionedForeignKey(Organizer, related_name='setting_objects')
key = models.CharField(max_length=255) key = models.CharField(max_length=255)

View File

@@ -1,60 +1,14 @@
import os import os
import sys import sys
import time
from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.conf import settings from django.conf import settings
from selenium import webdriver 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 use Chrome, Firefox, etc... here
# could add Chrome, Firefox, etc... here BROWSER = os.environ.get('TEST_BROWSER', 'PhantomJS')
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
class BrowserTest(StaticLiveServerTestCase): class BrowserTest(StaticLiveServerTestCase):
@@ -64,48 +18,19 @@ class BrowserTest(StaticLiveServerTestCase):
settings.DEBUG = ('--debug' in sys.argv) settings.DEBUG = ('--debug' in sys.argv)
def setUp(self): def setUp(self):
if RUN_LOCAL: self.driver = getattr(webdriver, BROWSER)()
self.setUpLocal() self.driver.set_window_size(1920, 1080)
else: self.driver.implicitly_wait(10)
self.setUpSauce()
def tearDown(self): 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() self.driver.quit()
def tearDownSauce(self): def scroll_into_view(self, element):
print("\nLink to your job: \n " """Scroll element into view"""
"https://saucelabs.com/jobs/%s \n" % self.driver.session_id) y = element.location['y']
try: self.driver.execute_script('window.scrollTo(0, {0})'.format(y))
if sys.exc_info() == (None, None, None):
sauce.jobs.update_job(self.driver.session_id, passed=True) def scroll_and_click(self, element):
else: self.scroll_into_view(element)
sauce.jobs.update_job(self.driver.session_id, passed=False) time.sleep(0.5)
finally: element.click()
self.driver.quit()

View File

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

View File

@@ -1,10 +1,9 @@
from django.test import TestCase, Client from django.test import TestCase, Client
from pretix.base.models import User 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): class LoginFormBrowserTest(BrowserTest):
def setUp(self): def setUp(self):

View File

@@ -1,9 +1,8 @@
import datetime import datetime
from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission 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): class EventsTest(BrowserTest):
def setUp(self): def setUp(self):

View File

@@ -5,7 +5,7 @@ import unittest
from selenium.webdriver.support.select import Select from selenium.webdriver.support.select import Select
from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission, ItemCategory, Property, \ from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission, ItemCategory, Property, \
PropertyValue, Question, Quota, Item PropertyValue, Question, Quota, Item
from pretix.base.tests import BrowserTest, on_platforms from pretix.base.tests import BrowserTest
class ItemFormTest(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_css_selector('button[type="submit"]').click()
self.driver.find_element_by_class_name("navbar-right") 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): class CategoriesTest(ItemFormTest):
def test_create(self): 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) self.assertNotIn("Entry tickets", self.driver.find_element_by_css_selector(".container table").text)
@on_platforms()
class PropertiesTest(ItemFormTest): class PropertiesTest(ItemFormTest):
def test_create(self): def test_create(self):
@@ -156,7 +144,6 @@ class PropertiesTest(ItemFormTest):
self.assertNotIn("Size", self.driver.find_element_by_css_selector(".container table").text) self.assertNotIn("Size", self.driver.find_element_by_css_selector(".container table").text)
@on_platforms()
class QuestionsTest(ItemFormTest): class QuestionsTest(ItemFormTest):
def test_create(self): 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) self.assertNotIn("shoe size", self.driver.find_element_by_css_selector(".container table").text)
@on_platforms()
class QuotaTest(ItemFormTest): class QuotaTest(ItemFormTest):
def test_create(self): def test_create(self):

View File

@@ -44,7 +44,6 @@ class EventUpdateForm(VersionedModelForm):
'presale_end', 'presale_end',
'payment_term_days', 'payment_term_days',
'payment_term_last', '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.instance.pk is not None and isinstance(self.instance, Versionable):
if self.has_changed(): if self.has_changed():
self.instance = self.instance.clone_shallow() 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) return super().save(commit)
class Meta: class Meta:

View File

@@ -6,4 +6,10 @@ from pretix.base.signals import determine_availability
@receiver(determine_availability) @receiver(determine_availability)
def availability_handler(sender, **kwargs): def availability_handler(sender, **kwargs):
kwargs['sender'] = sender 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 %} {% block content %}
{% if cart.positions %} {% if cart.positions %}
<div class="panel panel-primary"> <div class="panel panel-primary cart">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{% trans "Your cart" %}</h3> <h3 class="panel-title">{% trans "Your cart" %}</h3>
</div> </div>

View File

@@ -36,7 +36,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne"> <div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title"> <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 %} {% if global_registration_form %}
{% trans "I want to create a new account just for this event" %} {% trans "I want to create a new account just for this event" %}
{% else %} {% else %}
@@ -45,7 +45,7 @@
</a> </a>
</h4> </h4>
</div> </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">
<div class="panel-body"> <div class="panel-body">
<form class="form-horizontal" method="post"> <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 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.models import Item, Organizer, Event, ItemCategory, Quota, Property, PropertyValue, ItemVariation, User
from pretix.base.tests import BrowserTest, on_platforms from pretix.base.tests import BrowserTest
@on_platforms()
class EventMiddlewareTest(BrowserTest): class EventMiddlewareTest(BrowserTest):
def setUp(self): def setUp(self):
@@ -26,7 +25,6 @@ class EventMiddlewareTest(BrowserTest):
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
@on_platforms()
class ItemDisplayTest(BrowserTest): class ItemDisplayTest(BrowserTest):
def setUp(self): def setUp(self):

View File

@@ -47,16 +47,16 @@ class CartActionMixin:
items.append((key.split("_")[1], None, int(value))) items.append((key.split("_")[1], None, int(value)))
except ValueError: except ValueError:
messages.error(self.request, _('Please enter numbers only.')) messages.error(self.request, _('Please enter numbers only.'))
return False return []
elif key.startswith('variation_'): elif key.startswith('variation_'):
try: try:
items.append((key.split("_")[1], key.split("_")[2], int(value))) items.append((key.split("_")[1], key.split("_")[2], int(value)))
except ValueError: except ValueError:
messages.error(self.request, _('Please enter numbers only.')) messages.error(self.request, _('Please enter numbers only.'))
return False return []
if len(items) == 0: if len(items) == 0:
messages.warning(self.request, _('You did not select any items.')) messages.warning(self.request, _('You did not select any items.'))
return False return []
return items return items
def _re_add_position(self, items, position): def _re_add_position(self, items, position):
@@ -91,36 +91,46 @@ class CartRemove(EventViewMixin, CartActionMixin, EventLoginRequiredMixin, View)
class CartAdd(EventViewMixin, CartActionMixin, 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): def post(self, request, *args, **kwargs):
items = self._items_from_post_data() items = self._items_from_post_data()
# We do not use EventLoginRequiredMixin here, as we want to store stuff into the # We do not use EventLoginRequiredMixin here, as we want to store stuff into the
# session beforehand # session beforehand
if not request.user.is_authenticated() or \ 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) request.session['cart_tmp'] = json.dumps(items)
return redirect_to_login( 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, 'organizer': request.event.organizer.slug,
'event': request.event.slug, 'event': request.event.slug,
}), 'next' }), 'next'
) )
return self.process(items) return self.process(items)
def process(self, items): def error_message(self, msg, important=False):
if not items: if not self.msg_some_unavailable or important:
return redirect(self.get_failure_url()) self.msg_some_unavailable = True
existing = CartPosition.objects.current.filter(user=self.request.user, event=self.request.event).count() messages.error(self.request, msg)
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 process(self, items):
# Extend this user's cart session to 30 minutes from now to ensure all items in the # Extend this user's cart session to 30 minutes from now to ensure all items in the
# cart expire at the same # cart expire at the same time
# We can extend the reservation of items which are not yet expired without # We can extend the reservation of items which are not yet expired without risk
# risk
CartPosition.objects.current.filter( CartPosition.objects.current.filter(
Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__gt=now()) Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__gt=now())
).update(expires=now() + timedelta(minutes=30)) ).update(expires=now() + timedelta(minutes=30))
@@ -132,6 +142,15 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
items = self._re_add_position(items, cp) items = self._re_add_position(items, cp)
cp.delete() 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 # Fetch items from the database
items_cache = { items_cache = {
i.identity: i for i i.identity: i for i
@@ -149,13 +168,12 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
} }
# Process the request itself # Process the request itself
msg_some_unavailable = False
for i in items: for i in items:
# Check whether the specified items are part of what we just fetched from the database # 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 # If they are not, the user supplied item IDs which either do not exist or belong to
# a different event # a different event
if i[0] not in items_cache or (i[1] is not None and i[1] not in variations_cache): 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()) return redirect(self.get_failure_url())
item = items_cache[i[0]] item = items_cache[i[0]]
@@ -166,21 +184,13 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
# will correctly return the default price # will correctly return the default price
price = item.check_restrictions() if variation is None else variation.check_restrictions() price = item.check_restrictions() if variation is None else variation.check_restrictions()
if price is False: if price is False:
if not msg_some_unavailable: self.error_message(self.error_messages['unavailable'])
msg_some_unavailable = True
messages.error(self.request,
_('Some of the items you selected were no longer available. '
'Please see below for details.'))
continue continue
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold. # 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()) quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
if len(quotas) == 0: if len(quotas) == 0:
if not msg_some_unavailable: self.error_message(self.error_messages['unavailable'])
msg_some_unavailable = True
messages.error(self.request,
_('Some of the items you selected were no longer available. '
'Please see below for details.'))
continue continue
# Assume that all quotas allow us to buy i[2] instances of the object # 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() avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK: if avail[0] != Quota.AVAILABILITY_OK:
# This quota is sold out/currently unavailable, so do not sell this at all # This quota is sold out/currently unavailable, so do not sell this at all
if not msg_some_unavailable: self.error_message(self.error_messages['unavailable'])
msg_some_unavailable = True
messages.error(self.request,
_('Some of the items you selected were no longer available. '
'Please see below for details.'))
quota_ok = 0 quota_ok = 0
break break
elif avail[1] < i[2]: elif avail[1] < i[2]:
# This quota is available, but with less than i[2] items left, so we have to # This quota is available, but with less than i[2] items left, so we have to
# reduce the number of bought items # reduce the number of bought items
if not msg_some_unavailable: self.error_message(self.error_messages['in_part'])
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.'))
quota_ok = min(quota_ok, avail[1]) quota_ok = min(quota_ok, avail[1])
# Create a CartPosition for as much items as we can # Create a CartPosition for as much items as we can
@@ -223,24 +225,13 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
except Quota.LockTimeoutException: except Quota.LockTimeoutException:
# Is raised when there are too many threads asking for quota locks and we were # Is raised when there are too many threads asking for quota locks and we were
# unaible to get one # unaible to get one
if not msg_some_unavailable: self.error_message(self.error_messages['busy'], important=True)
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.'))
finally: finally:
# Release the locks. This is important ;) # Release the locks. This is important ;)
for quota in quotas: for quota in quotas:
quota.release() 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.')) messages.success(self.request, _('The items have been successfully added to your cart.'))
return redirect(self.get_success_url()) 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.contrib.auth import authenticate
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
@@ -13,6 +14,7 @@ from django.conf import settings
from pretix.base.models import User from pretix.base.models import User
from pretix.presale.views import EventViewMixin, CartDisplayMixin from pretix.presale.views import EventViewMixin, CartDisplayMixin
from pretix.presale.views.cart import CartAdd
class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView):
@@ -38,13 +40,13 @@ class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView):
if not item.has_variations: if not item.has_variations:
item.cached_availability = list(item.check_quotas()) item.cached_availability = list(item.check_quotas())
item.cached_availability[1] = min(item.cached_availability[1], 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'] item.price = item.available_variations[0]['price']
else: else:
for var in item.available_variations: for var in item.available_variations:
var.cached_availability = list(var['variation'].check_quotas()) var.cached_availability = list(var['variation'].check_quotas())
var.cached_availability[1] = min(var.cached_availability[1], 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] items = [item for item in items if len(item.available_variations) > 0]
@@ -63,7 +65,11 @@ class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView):
class LoginForm(BaseAuthenticationForm): class LoginForm(BaseAuthenticationForm):
username = forms.CharField( username = forms.CharField(
label=_('Username'), 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( password = forms.CharField(
label=_('Password'), label=_('Password'),
@@ -205,6 +211,12 @@ class EventLogin(EventViewMixin, TemplateView):
template_name = 'pretixpresale/event/login.html' template_name = 'pretixpresale/event/login.html'
def redirect_to_next(self): 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: if 'next' in self.request.GET:
return redirect(self.request.GET.get('next')) return redirect(self.request.GET.get('next'))
else: else:
@@ -237,7 +249,7 @@ class EventLogin(EventViewMixin, TemplateView):
user = authenticate(identifier=user.identifier, password=form.cleaned_data['password']) user = authenticate(identifier=user.identifier, password=form.cleaned_data['password'])
login(request, user) login(request, user)
return self.redirect_to_next() 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 form = self.global_registration_form
if form.is_valid(): if form.is_valid():
user = User.objects.create_global_user( user = User.objects.create_global_user(