Complete test suite for cart actions

This commit is contained in:
Raphael Michel
2015-02-21 17:00:52 +01:00
parent 497cbe17af
commit fd252b0ff5
3 changed files with 202 additions and 54 deletions

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
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 []

View File

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

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,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())