diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py
index 1a6528d8bd..67a831694d 100644
--- a/src/pretix/base/services/cart.py
+++ b/src/pretix/base/services/cart.py
@@ -27,6 +27,7 @@ error_messages = {
'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'),
'empty': _('You did not select any products.'),
+ 'unknown_position': _('Unknown cart position.'),
'not_for_sale': _('You selected a product which is not available for sale.'),
'unavailable': _('Some of the products you selected are no longer available. '
'Please see below for details.'),
@@ -269,29 +270,26 @@ class CartManager:
self._voucher_use_diff += voucher_use_diff
self._operations += operations
- def remove_items(self, items: List[dict]):
+ def remove_item(self, pos_id: int):
# TODO: We could calculate quotadiffs and voucherdiffs here, which would lead to more
# flexible usages (e.g. a RemoveOperation and an AddOperation in the same transaction
# could cancel each other out quota-wise). However, we are not taking this performance
# penalty for now as there is currently no outside interface that would allow building
# such a transaction.
- for i in items:
- cw = Q(cart_id=self.cart_id) & Q(item_id=i['item']) & Q(event=self.event)
- if i['variation']:
- cw &= Q(variation_id=i['variation'])
- else:
- cw &= Q(variation__isnull=True)
- # Prefer to delete positions that have the same price as the one the user clicked on, after thet
- # prefer the most expensive ones.
- cnt = i['count']
- if i['price']:
- correctprice = CartPosition.objects.filter(cw).filter(price=Decimal(i['price'].replace(",", ".")))[:cnt]
- for cp in correctprice:
- self._operations.append(self.RemoveOperation(position=cp))
- cnt -= len(correctprice)
- if cnt > 0:
- for cp in CartPosition.objects.filter(cw).order_by("-price")[:cnt]:
- self._operations.append(self.RemoveOperation(position=cp))
+ try:
+ cp = self.positions.get(pk=pos_id)
+ except CartPosition.DoesNotExist:
+ raise CartError(error_messages['unknown_position'])
+ self._operations.append(self.RemoveOperation(position=cp))
+
+ def clear(self):
+ # TODO: We could calculate quotadiffs and voucherdiffs here, which would lead to more
+ # flexible usages (e.g. a RemoveOperation and an AddOperation in the same transaction
+ # could cancel each other out quota-wise). However, we are not taking this performance
+ # penalty for now as there is currently no outside interface that would allow building
+ # such a transaction.
+ for cp in self.positions.all():
+ self._operations.append(self.RemoveOperation(position=cp))
def set_addons(self, addons):
self._update_items_cache(
@@ -566,11 +564,11 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
-def remove_items_from_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en') -> None:
+def remove_cart_position(self, event: int, position: int, cart_id: str=None, locale='en') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
- :param items: A list of dicts with the keys item, variation, number, custom_price, voucher
+ :param position: A cart position ID
:param session: Session ID of a guest
"""
with language(locale):
@@ -578,7 +576,7 @@ def remove_items_from_cart(self, event: int, items: List[dict], cart_id: str=Non
try:
try:
cm = CartManager(event=event, cart_id=cart_id)
- cm.remove_items(items)
+ cm.remove_item(position)
cm.commit()
except LockTimeoutException:
self.retry()
@@ -587,20 +585,41 @@ def remove_items_from_cart(self, event: int, items: List[dict], cart_id: str=Non
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
-def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None) -> None:
+def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
+ """
+ Removes a list of items from a user's cart.
+ :param event: The event ID in question
+ :param session: Session ID of a guest
+ """
+ with language(locale):
+ event = Event.objects.get(id=event)
+ try:
+ try:
+ cm = CartManager(event=event, cart_id=cart_id)
+ cm.clear()
+ cm.commit()
+ except LockTimeoutException:
+ self.retry()
+ except (MaxRetriesExceededError, LockTimeoutException):
+ raise CartError(error_messages['busy'])
+
+
+@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
+def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param addons: A list of dicts with the keys addon_to, item, variation
:param session: Session ID of a guest
"""
- event = Event.objects.get(id=event)
- try:
+ with language(locale):
+ event = Event.objects.get(id=event)
try:
- cm = CartManager(event=event, cart_id=cart_id)
- cm.set_addons(addons)
- cm.commit()
- except LockTimeoutException:
- self.retry()
- except (MaxRetriesExceededError, LockTimeoutException):
- raise CartError(error_messages['busy'])
+ try:
+ cm = CartManager(event=event, cart_id=cart_id)
+ cm.set_addons(addons)
+ cm.commit()
+ except LockTimeoutException:
+ self.retry()
+ except (MaxRetriesExceededError, LockTimeoutException):
+ raise CartError(error_messages['busy'])
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
index 8462fbae94..faaf250877 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
@@ -45,19 +45,10 @@
{% endif %}
{{ line.count }}
diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html
index 44f51f8382..29ecf82c03 100644
--- a/src/pretix/presale/templates/pretixpresale/event/index.html
+++ b/src/pretix/presale/templates/pretixpresale/event/index.html
@@ -26,17 +26,8 @@
-
diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py
index 96fceebda5..c4049e0715 100644
--- a/src/pretix/presale/urls.py
+++ b/src/pretix/presale/urls.py
@@ -15,6 +15,7 @@ import pretix.presale.views.waiting
event_patterns = [
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
+ url(r'^cart/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'),
url(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'),
url(r'^checkout/start$', pretix.presale.views.checkout.CheckoutView.as_view(), name='event.checkout.start'),
url(r'^redeem$', pretix.presale.views.cart.RedeemView.as_view(),
diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py
index 1c9e2d72de..f209b7281d 100644
--- a/src/pretix/presale/views/cart.py
+++ b/src/pretix/presale/views/cart.py
@@ -10,7 +10,7 @@ from django.views.generic import TemplateView, View
from pretix.base.decimal import round_decimal
from pretix.base.models import CartPosition, Quota, Voucher
from pretix.base.services.cart import (
- CartError, add_items_to_cart, remove_items_from_cart,
+ CartError, add_items_to_cart, clear_cart, remove_cart_position,
)
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.views import EventViewMixin
@@ -105,19 +105,18 @@ class CartActionMixin:
class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
- task = remove_items_from_cart
+ task = remove_cart_position
known_errortypes = ['CartError']
def get_success_message(self, value):
if CartPosition.objects.filter(cart_id=self.request.session.session_key).exists():
return _('Your cart has been updated.')
else:
- return _('Your cart is empty.')
+ return _('Your cart is now empty.')
def post(self, request, *args, **kwargs):
- items = self._items_from_post_data()
- if items:
- return self.do(self.request.event.id, items, self.request.session.session_key, translation.get_language())
+ if 'id' in request.POST:
+ return self.do(self.request.event.id, request.POST.get('id'), self.request.session.session_key, translation.get_language())
else:
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
return JsonResponse({
@@ -127,6 +126,17 @@ class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
return redirect(self.get_error_url())
+class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
+ task = clear_cart
+ known_errortypes = ['CartError']
+
+ def get_success_message(self, value):
+ return _('Your cart is now empty.')
+
+ def post(self, request, *args, **kwargs):
+ return self.do(self.request.event.id, self.request.session.session_key, translation.get_language())
+
+
class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
task = add_items_to_cart
known_errortypes = ['CartError']
diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py
index 5a9f5afb99..dfd76bed69 100644
--- a/src/tests/presale/test_cart.py
+++ b/src/tests/presale/test_cart.py
@@ -495,12 +495,12 @@ class CartTest(CartTestMixin, TestCase):
self.assertFalse(CartPosition.objects.filter(id=cp1.id).exists())
def test_remove_simple(self):
- CartPosition.objects.create(
+ cp = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, 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_%d' % self.ticket.id: '1',
+ 'id': cp.pk
}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('empty', doc.select('.alert-success')[0].text)
@@ -525,19 +525,19 @@ class CartTest(CartTestMixin, TestCase):
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
def test_remove_variation(self):
- CartPosition.objects.create(
+ cp = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, 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_%d_%d' % (self.shirt.id, self.shirt_red.id): '1',
+ 'id': cp.pk
}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('empty', doc.select('.alert-success')[0].text)
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
def test_remove_one_of_multiple(self):
- CartPosition.objects.create(
+ cp = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
@@ -546,28 +546,12 @@ class CartTest(CartTestMixin, TestCase):
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.post('/%s/%s/cart/remove' % (self.orga.slug, self.event.slug), {
- 'item_%d' % self.ticket.id: '1',
+ 'id': cp.pk
}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('updated', doc.select('.alert-success')[0].text)
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1)
- def test_remove_multiple(self):
- CartPosition.objects.create(
- event=self.event, cart_id=self.session_key, item=self.ticket,
- price=23, expires=now() + timedelta(minutes=10)
- )
- CartPosition.objects.create(
- event=self.event, cart_id=self.session_key, 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_%d' % self.ticket.id: '2',
- }, follow=True)
- doc = BeautifulSoup(response.rendered_content, "lxml")
- self.assertIn('empty', doc.select('.alert-success')[0].text)
- self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
-
def test_remove_all(self):
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
@@ -581,50 +565,11 @@ class CartTest(CartTestMixin, TestCase):
event=self.event, cart_id=self.session_key, 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), {
- 'item_%d' % self.ticket.id: '2',
- 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1',
- }, follow=True)
+ response = self.client.post('/%s/%s/cart/clear' % (self.orga.slug, self.event.slug), {}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('empty', doc.select('.alert-success')[0].text)
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
- def test_remove_all_same_variation_different_price(self):
- CartPosition.objects.create(
- event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_red,
- price=14, expires=now() + timedelta(minutes=10)
- )
- v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, value=Decimal('10.00'), event=self.event)
- self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
- 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code,
- }, follow=True)
- response = self.client.post('/%s/%s/cart/remove' % (self.orga.slug, self.event.slug), {
- 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): ('1', '1'),
- }, follow=True)
- doc = BeautifulSoup(response.rendered_content, "lxml")
- self.assertIn('empty', doc.select('.alert-success')[0].text)
- self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
-
- def test_remove_most_expensive(self):
- CartPosition.objects.create(
- event=self.event, cart_id=self.session_key, item=self.ticket,
- price=23, expires=now() + timedelta(minutes=10)
- )
- CartPosition.objects.create(
- event=self.event, cart_id=self.session_key, 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_%d' % self.ticket.id: '1',
- }, follow=True)
- doc = BeautifulSoup(response.rendered_content, "lxml")
- self.assertIn('updated', doc.select('.alert-success')[0].text)
- objs = list(CartPosition.objects.filter(cart_id=self.session_key, 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)
-
def test_voucher(self):
v = Voucher.objects.create(item=self.ticket, event=self.event)
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {