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