diff --git a/src/pretixbase/models.py b/src/pretixbase/models.py index 8fe8932d25..d66ca80511 100644 --- a/src/pretixbase/models.py +++ b/src/pretixbase/models.py @@ -872,6 +872,9 @@ class ItemVariation(Versionable): verbose_name = _("Item variation") verbose_name_plural = _("Item variations") + def __str__(self): + return str(self.to_variation_dict()) + def delete(self, *args, **kwargs): super().delete(*args, **kwargs) if self.item: @@ -1099,14 +1102,14 @@ class Quota(Versionable): Q(variation__quotas__in=[self]) ) ) - paid_orders = OrderPosition.objects.filter( + paid_orders = OrderPosition.objects.current.filter( Q(order__status=Order.STATUS_PAID) & quotalookup ).count() if paid_orders >= self.size: return Quota.AVAILABILITY_GONE, 0 - pending_valid_orders = OrderPosition.objects.filter( + pending_valid_orders = OrderPosition.objects.current.filter( Q(order__status=Order.STATUS_PENDING) & Q(order__expires__gte=now()) & quotalookup @@ -1114,7 +1117,7 @@ class Quota(Versionable): if (paid_orders + pending_valid_orders) >= self.size: return Quota.AVAILABILITY_ORDERED, 0 - valid_cart_positions = CartPosition.objects.filter( + valid_cart_positions = CartPosition.objects.current.filter( Q(expires__gte=now()) & quotalookup ).count() diff --git a/src/pretixpresale/static/pretixpresale/less/event.less b/src/pretixpresale/static/pretixpresale/less/event.less index d6304450e5..2f9d3297ff 100644 --- a/src/pretixpresale/static/pretixpresale/less/event.less +++ b/src/pretixpresale/static/pretixpresale/less/event.less @@ -1,5 +1,4 @@ .product-row { - padding: 10px 0; border-top: 1px solid @table-border-color; &.headline, &.simple { @@ -25,7 +24,14 @@ color: @alert-warning-text; } } - .price { +} +.cart-row, .product-row { + padding: 10px 0; + + .count form { + display: inline; + } + .price, .count { text-align: center; } .price small, diff --git a/src/pretixpresale/templates/pretixpresale/event/fragment_cart.html b/src/pretixpresale/templates/pretixpresale/event/fragment_cart.html new file mode 100644 index 0000000000..4d3b80e0ee --- /dev/null +++ b/src/pretixpresale/templates/pretixpresale/event/fragment_cart.html @@ -0,0 +1,59 @@ +{% load i18n %} +{% if cart %} +
+
+

{% trans "Your cart" %}

+
+
+ {% for line in cart %} +
+
+ {{ line.item }} + {% if line.variation %} + – {{ line.variation }} + {% endif %} +
+
+ {{ event.currency }} {{ line.price|floatformat:2 }} +
+
+
+ {% csrf_token %} + {% if line.variation %} + + {% else %} + + {% endif %} + +
+ {{ line.count }} +
+ {% csrf_token %} + {% if line.variation %} + + {% else %} + + {% endif %} + +
+
+
+ {{ event.currency }} {{ line.total|floatformat:2 }} + {% if line.item.tax_rate %} +
{% blocktrans trimmed with rate=line.item.tax_rate %} + incl. {{ rate }}% taxes + {% endblocktrans %} + {% endif %} +
+
+
+ {% endfor %} +
+
+{% endif %} \ No newline at end of file diff --git a/src/pretixpresale/templates/pretixpresale/event/index.html b/src/pretixpresale/templates/pretixpresale/event/index.html index 3119de4d61..d4bbe47b08 100644 --- a/src/pretixpresale/templates/pretixpresale/event/index.html +++ b/src/pretixpresale/templates/pretixpresale/event/index.html @@ -2,6 +2,7 @@ {% load i18n %} {% block content %} + {% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event %}
{% csrf_token %} diff --git a/src/pretixpresale/urls.py b/src/pretixpresale/urls.py index e230cf0953..97929ffdbb 100644 --- a/src/pretixpresale/urls.py +++ b/src/pretixpresale/urls.py @@ -10,6 +10,7 @@ urlpatterns = patterns( 'pretixpresale.views.event', url(r'^$', pretixpresale.views.event.EventIndex.as_view(), name='event.index'), url(r'^cart/add$', pretixpresale.views.cart.CartAdd.as_view(), name='event.cart.add'), + url(r'^cart/remove$', pretixpresale.views.cart.CartRemove.as_view(), name='event.cart.remove'), ) )), ) diff --git a/src/pretixpresale/views/__init__.py b/src/pretixpresale/views/__init__.py index e69de29bb2..89b69edff9 100644 --- a/src/pretixpresale/views/__init__.py +++ b/src/pretixpresale/views/__init__.py @@ -0,0 +1,57 @@ +import uuid +from itertools import groupby + +from django.db.models import Q + +from pretixbase.models import CartPosition + + +class CartMixin: + def get_session_key(self): + if 'cart_key' in self.request.session: + return self.request.session.get('cart_key') + key = str(uuid.uuid4()) + self.request.session['cart_key'] = key + return key + + +class CartDisplayMixin(CartMixin): + + def get_cart(self): + qw = Q(session=self.get_session_key()) + if self.request.user.is_authenticated(): + qw |= Q(user=self.request.user) + + cartpos = list(CartPosition.objects.current.filter( + qw & Q(event=self.request.event) + ).order_by( + 'item', 'variation' + ).select_related( + 'item', 'variation' + ).prefetch_related( + 'variation__values', 'variation__values__prop' + )) + + # Group items of the same variation + # We do this by list manipulations instead of a GROUP BY query, as + # Django is unable to join related models in a .values() query + def keyfunc(pos): + return pos.item_id, pos.variation_id, pos.price + + cart = [] + for k, g in groupby(sorted(cartpos, key=keyfunc), key=keyfunc): + g = list(g) + group = g[0] + group.count = len(g) + group.total = group.count * group.price + cart.append(group) + + return cart + + +class EventViewMixin: + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['event'] = self.request.event + return context diff --git a/src/pretixpresale/views/cart.py b/src/pretixpresale/views/cart.py index cbb19bc1da..e75c642bf8 100644 --- a/src/pretixpresale/views/cart.py +++ b/src/pretixpresale/views/cart.py @@ -1,18 +1,18 @@ from datetime import timedelta -import uuid from django.contrib import messages from django.core.urlresolvers import reverse +from django.db.models import Q from django.shortcuts import redirect from django.utils.timezone import now from django.views.generic import View from django.utils.translation import ugettext_lazy as _ -from .event import EventViewMixin from pretixbase.models import Item, ItemVariation, Quota, CartPosition +from pretixpresale.views import CartMixin, EventViewMixin -class CartActionMixin: +class CartActionMixin(CartMixin): def get_next_url(self): if "next" in self.request.GET and '://' not in self.request.GET: @@ -29,16 +29,6 @@ class CartActionMixin: def get_failure_url(self): return self.get_next_url() - def get_session_key(self): - if 'cart_key' in self.request.session: - return self.request.session.get('cart_key') - key = str(uuid.uuid4()) - self.request.session['cart_key'] = key - return key - - -class CartAdd(EventViewMixin, CartActionMixin, View): - def _items_from_post_data(self): """ Parses the POST data and returns a list of tuples in the @@ -65,6 +55,32 @@ class CartAdd(EventViewMixin, CartActionMixin, View): return False return items + +class CartRemove(EventViewMixin, CartActionMixin, View): + + def post(self, *args, **kwargs): + items = self._items_from_post_data() + if not items: + return redirect(self.get_failure_url()) + qw = Q(session=self.get_session_key()) + if self.request.user.is_authenticated(): + qw |= Q(user=self.request.user) + + for item, variation, cnt in items: + cw = qw & Q(item_id=item) + if variation: + cw &= Q(variation_id=variation) + else: + cw &= Q(variation__isnull=True) + for cp in CartPosition.objects.current.filter(cw).order_by("-price")[:cnt]: + cp.delete() + + messages.success(self.request, _('Your cart has been updated.')) + return redirect(self.get_success_url()) + + +class CartAdd(EventViewMixin, CartActionMixin, View): + def post(self, *args, **kwargs): items = self._items_from_post_data() if not items: @@ -79,19 +95,27 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # Fetch items from the database items_cache = { i.identity: i for i - in Item.objects.filter( + in Item.objects.current.filter( event=self.request.event, identity__in=[i[0] for i in items] ).prefetch_related("quotas") } variations_cache = { v.identity: v for v - in ItemVariation.objects.filter( + in ItemVariation.objects.current.filter( item__event=self.request.event, identity__in=[i[1] for i in items if i[1] is not None] ).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/pretixpresale/views/event.py b/src/pretixpresale/views/event.py index 7aeac158ed..fea1c49e93 100644 --- a/src/pretixpresale/views/event.py +++ b/src/pretixpresale/views/event.py @@ -1,15 +1,9 @@ from django.db.models import Count from django.views.generic import TemplateView +from pretixpresale.views import EventViewMixin, CartDisplayMixin -class EventViewMixin: - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['event'] = self.request.event - return context - - -class EventIndex(EventViewMixin, TemplateView): +class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): template_name = "pretixpresale/event/index.html" def get_context_data(self, **kwargs): @@ -46,4 +40,6 @@ class EventIndex(EventViewMixin, TemplateView): (cat, [i for i in items if i.category_id == cat.identity]) for cat in set([i.category for i in items]) # insert categories into a set for uniqueness ], key=lambda group: (group[0].position, group[0].pk)) # a set is unsorted, so sort again by category + + context['cart'] = self.get_cart() return context