mirror of
https://github.com/pretix/pretix.git
synced 2026-08-24 13:02:00 +00:00
Display and manipulate cart
This commit is contained in:
@@ -872,6 +872,9 @@ class ItemVariation(Versionable):
|
|||||||
verbose_name = _("Item variation")
|
verbose_name = _("Item variation")
|
||||||
verbose_name_plural = _("Item variations")
|
verbose_name_plural = _("Item variations")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.to_variation_dict())
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
if self.item:
|
if self.item:
|
||||||
@@ -1099,14 +1102,14 @@ class Quota(Versionable):
|
|||||||
Q(variation__quotas__in=[self])
|
Q(variation__quotas__in=[self])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
paid_orders = OrderPosition.objects.filter(
|
paid_orders = OrderPosition.objects.current.filter(
|
||||||
Q(order__status=Order.STATUS_PAID)
|
Q(order__status=Order.STATUS_PAID)
|
||||||
& quotalookup
|
& quotalookup
|
||||||
).count()
|
).count()
|
||||||
if paid_orders >= self.size:
|
if paid_orders >= self.size:
|
||||||
return Quota.AVAILABILITY_GONE, 0
|
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__status=Order.STATUS_PENDING)
|
||||||
& Q(order__expires__gte=now())
|
& Q(order__expires__gte=now())
|
||||||
& quotalookup
|
& quotalookup
|
||||||
@@ -1114,7 +1117,7 @@ class Quota(Versionable):
|
|||||||
if (paid_orders + pending_valid_orders) >= self.size:
|
if (paid_orders + pending_valid_orders) >= self.size:
|
||||||
return Quota.AVAILABILITY_ORDERED, 0
|
return Quota.AVAILABILITY_ORDERED, 0
|
||||||
|
|
||||||
valid_cart_positions = CartPosition.objects.filter(
|
valid_cart_positions = CartPosition.objects.current.filter(
|
||||||
Q(expires__gte=now())
|
Q(expires__gte=now())
|
||||||
& quotalookup
|
& quotalookup
|
||||||
).count()
|
).count()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.product-row {
|
.product-row {
|
||||||
padding: 10px 0;
|
|
||||||
border-top: 1px solid @table-border-color;
|
border-top: 1px solid @table-border-color;
|
||||||
|
|
||||||
&.headline, &.simple {
|
&.headline, &.simple {
|
||||||
@@ -25,7 +24,14 @@
|
|||||||
color: @alert-warning-text;
|
color: @alert-warning-text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.price {
|
}
|
||||||
|
.cart-row, .product-row {
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.count form {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.price, .count {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.price small,
|
.price small,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% if cart %}
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">{% trans "Your cart" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% for line in cart %}
|
||||||
|
<div class="row-fluid cart-row">
|
||||||
|
<div class="col-md-4 col-xs-6">
|
||||||
|
<strong>{{ line.item }}</strong>
|
||||||
|
{% if line.variation %}
|
||||||
|
– {{ line.variation }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-xs-6 price">
|
||||||
|
{{ event.currency }} {{ line.price|floatformat:2 }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-xs-6 count">
|
||||||
|
<form action="{% url "presale:event.cart.remove" event=event.slug organizer=event.organizer.slug %}"
|
||||||
|
method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if line.variation %}
|
||||||
|
<input type="hidden" name="variation_{{ line.item.identity }}_{{ line.variation.identity }}"
|
||||||
|
value="1" />
|
||||||
|
{% else %}
|
||||||
|
<input type="hidden" name="item_{{ line.item.identity }}"
|
||||||
|
value="1" />
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-mini btn-link"><i class="fa fa-minus"></i></button>
|
||||||
|
</form>
|
||||||
|
{{ line.count }}
|
||||||
|
<form action="{% url "presale:event.cart.add" event=event.slug organizer=event.organizer.slug %}"
|
||||||
|
method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if line.variation %}
|
||||||
|
<input type="hidden" name="variation_{{ line.item.identity }}_{{ line.variation.identity }}"
|
||||||
|
value="1" />
|
||||||
|
{% else %}
|
||||||
|
<input type="hidden" name="item_{{ line.item.identity }}"
|
||||||
|
value="1" />
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-mini btn-link"><i class="fa fa-plus"></i></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-xs-6 price">
|
||||||
|
<strong>{{ event.currency }} {{ line.total|floatformat:2 }}</strong>
|
||||||
|
{% if line.item.tax_rate %}
|
||||||
|
<br /><small>{% blocktrans trimmed with rate=line.item.tax_rate %}
|
||||||
|
incl. {{ rate }}% taxes
|
||||||
|
{% endblocktrans %}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{% url "presale:event.cart.add" organizer=request.event.organizer.slug event=request.event.slug %}?next={{ request.path_info|urlencode }}">
|
action="{% url "presale:event.cart.add" organizer=request.event.organizer.slug event=request.event.slug %}?next={{ request.path_info|urlencode }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ urlpatterns = patterns(
|
|||||||
'pretixpresale.views.event',
|
'pretixpresale.views.event',
|
||||||
url(r'^$', pretixpresale.views.event.EventIndex.as_view(), name='event.index'),
|
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/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'),
|
||||||
)
|
)
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db.models import Q
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .event import EventViewMixin
|
|
||||||
from pretixbase.models import Item, ItemVariation, Quota, CartPosition
|
from pretixbase.models import Item, ItemVariation, Quota, CartPosition
|
||||||
|
from pretixpresale.views import CartMixin, EventViewMixin
|
||||||
|
|
||||||
|
|
||||||
class CartActionMixin:
|
class CartActionMixin(CartMixin):
|
||||||
|
|
||||||
def get_next_url(self):
|
def get_next_url(self):
|
||||||
if "next" in self.request.GET and '://' not in self.request.GET:
|
if "next" in self.request.GET and '://' not in self.request.GET:
|
||||||
@@ -29,16 +29,6 @@ class CartActionMixin:
|
|||||||
def get_failure_url(self):
|
def get_failure_url(self):
|
||||||
return self.get_next_url()
|
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):
|
def _items_from_post_data(self):
|
||||||
"""
|
"""
|
||||||
Parses the POST data and returns a list of tuples in the
|
Parses the POST data and returns a list of tuples in the
|
||||||
@@ -65,6 +55,32 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
|
|||||||
return False
|
return False
|
||||||
return items
|
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):
|
def post(self, *args, **kwargs):
|
||||||
items = self._items_from_post_data()
|
items = self._items_from_post_data()
|
||||||
if not items:
|
if not items:
|
||||||
@@ -79,19 +95,27 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
|
|||||||
# Fetch items from the database
|
# Fetch items from the database
|
||||||
items_cache = {
|
items_cache = {
|
||||||
i.identity: i for i
|
i.identity: i for i
|
||||||
in Item.objects.filter(
|
in Item.objects.current.filter(
|
||||||
event=self.request.event,
|
event=self.request.event,
|
||||||
identity__in=[i[0] for i in items]
|
identity__in=[i[0] for i in items]
|
||||||
).prefetch_related("quotas")
|
).prefetch_related("quotas")
|
||||||
}
|
}
|
||||||
variations_cache = {
|
variations_cache = {
|
||||||
v.identity: v for v
|
v.identity: v for v
|
||||||
in ItemVariation.objects.filter(
|
in ItemVariation.objects.current.filter(
|
||||||
item__event=self.request.event,
|
item__event=self.request.event,
|
||||||
identity__in=[i[1] for i in items if i[1] is not None]
|
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")
|
).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
|
# Process the request itself
|
||||||
msg_some_unavailable = False
|
msg_some_unavailable = False
|
||||||
for i in items:
|
for i in items:
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
from pretixpresale.views import EventViewMixin, CartDisplayMixin
|
||||||
|
|
||||||
|
|
||||||
class EventViewMixin:
|
class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView):
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['event'] = self.request.event
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventIndex(EventViewMixin, TemplateView):
|
|
||||||
template_name = "pretixpresale/event/index.html"
|
template_name = "pretixpresale/event/index.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
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])
|
(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
|
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
|
], 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
|
return context
|
||||||
|
|||||||
Reference in New Issue
Block a user