diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html
index 5fe06c58a..583cff30f 100644
--- a/src/pretix/presale/templates/pretixpresale/event/index.html
+++ b/src/pretix/presale/templates/pretixpresale/event/index.html
@@ -9,6 +9,24 @@
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
+
+
+ {% if cart.minutes_left > 0 %}
+ {% blocktrans trimmed with minutes=cart.minutes_left %}
+ The items in your cart are reserved for you for {{ minutes }} minutes.
+ {% endblocktrans %}
+ {% else %}
+ {% trans "The items in your cart are no longer reserved for you." %}
+ {% endif %}
+
+
+
+
{% endif %}
diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py
index f93c631cf..9491ed578 100644
--- a/src/pretix/presale/urls.py
+++ b/src/pretix/presale/urls.py
@@ -2,6 +2,8 @@ from django.conf.urls import patterns, url, include
import pretix.presale.views.event
import pretix.presale.views.cart
+import pretix.presale.views.checkout
+
urlpatterns = patterns(
'',
@@ -11,6 +13,7 @@ urlpatterns = patterns(
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
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'^checkout$', pretix.presale.views.checkout.CheckoutStart.as_view(), name='event.checkout.start'),
)
)),
)
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py
index 962a2df79..7a0285c1d 100644
--- a/src/pretix/presale/views/__init__.py
+++ b/src/pretix/presale/views/__init__.py
@@ -1,7 +1,9 @@
import uuid
from itertools import groupby
+from datetime import timedelta
from django.db.models import Q
+from django.utils.timezone import now
from pretix.base.models import CartPosition
@@ -49,6 +51,10 @@ class CartDisplayMixin(CartMixin):
return {
'positions': positions,
'total': sum(p.total for p in positions),
+ 'minutes_left': (
+ max(min(p.expires for p in positions) - now(), timedelta()).seconds // 60
+ if positions else 0
+ ),
}
diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py
index 35d1e6b35..89cffc76b 100644
--- a/src/pretix/presale/views/cart.py
+++ b/src/pretix/presale/views/cart.py
@@ -57,6 +57,14 @@ class CartActionMixin(CartMixin):
return False
return items
+ def _re_add_position(self, items, position):
+ for i, tup in enumerate(items):
+ if tup[0] == position.item_id and tup[1] == position.variation_id:
+ items[i] = (tup[0], tup[1], tup[2] + 1)
+ return items
+ items.append((position.item_id, position.variation_id, 1))
+ return items
+
class CartRemove(EventViewMixin, CartActionMixin, View):
@@ -94,6 +102,25 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
_("You cannot select more than %d items per order") % self.event.max_items_per_order)
return redirect(self.get_failure_url())
+ # Extend this user's cart session to 30 minutes from now to ensure all items in the
+ # cart expire at the same time
+ qw = Q(session=self.get_session_key())
+ if self.request.user.is_authenticated():
+ qw |= Q(user=self.request.user)
+
+ # We can extend the reservation of items which are not yet expired without
+ # risk
+ CartPosition.objects.current.filter(
+ qw & Q(event=self.request.event) & Q(expires__gt=now())
+ ).update(expires=now() + timedelta(minutes=30))
+
+ # For items that are already expired, we have to delete and re-add them, as they might
+ # be no longer available. Sorry!
+ for cp in CartPosition.objects.current.filter(
+ qw & Q(event=self.request.event) & Q(expires__lte=now())):
+ items = self._re_add_position(items, cp)
+ cp.delete()
+
# Fetch items from the database
items_cache = {
i.identity: i for i
@@ -110,14 +137,6 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
).select_related("item", "item__event").prefetch_related("quotas", "values", "values__prop")
}
- # Extend this user's cart session to 30 minutes from now to ensure all items in the
- # cart expire at the same time
- qw = Q(session=self.get_session_key())
- if self.request.user.is_authenticated():
- qw |= Q(user=self.request.user)
- CartPosition.objects.current.filter(
- qw & Q(event=self.request.event)).update(expires=now() + timedelta(minutes=30))
-
# Process the request itself
msg_some_unavailable = False
for i in items:
diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py
new file mode 100644
index 000000000..ed494e626
--- /dev/null
+++ b/src/pretix/presale/views/checkout.py
@@ -0,0 +1,23 @@
+from django.contrib import messages
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.views.generic import View
+from django.utils.translation import ugettext_lazy as _
+
+from pretix.presale.views import EventViewMixin, CartDisplayMixin
+
+
+class CheckoutStart(EventViewMixin, CartDisplayMixin, View):
+
+ def get_failure_url(self):
+ return reverse('presale:event.index', kwargs={
+ 'event': self.request.event.slug,
+ 'organizer': self.request.event.organizer.slug,
+ })
+
+ def get(self, *args, **kwargs):
+ cart = self.get_cart()
+ if not cart['positions']:
+ messages.error(self.request,
+ _("Your cart is empty") % self.event.max_items_per_order)
+ return redirect(self.get_failure_url())