mirror of
https://github.com/pretix/pretix.git
synced 2026-05-09 15:54:03 +00:00
Restructure our python module. A lot.
This commit is contained in:
60
src/pretix/presale/views/__init__.py
Normal file
60
src/pretix/presale/views/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import uuid
|
||||
from itertools import groupby
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from pretix.base.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
|
||||
|
||||
positions = []
|
||||
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
|
||||
positions.append(group)
|
||||
|
||||
return {
|
||||
'positions': positions,
|
||||
'total': sum(p.total for p in positions),
|
||||
}
|
||||
|
||||
|
||||
class EventViewMixin:
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['event'] = self.request.event
|
||||
return context
|
||||
210
src/pretix/presale/views/cart.py
Normal file
210
src/pretix/presale/views/cart.py
Normal file
@@ -0,0 +1,210 @@
|
||||
from datetime import timedelta
|
||||
|
||||
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 pretix.base.models import Item, ItemVariation, Quota, CartPosition
|
||||
from pretix.presale.views import CartMixin, EventViewMixin
|
||||
|
||||
|
||||
class CartActionMixin(CartMixin):
|
||||
|
||||
def get_next_url(self):
|
||||
if "next" in self.request.GET and '://' not in self.request.GET:
|
||||
return self.request.GET.get('next')
|
||||
elif "HTTP_REFERER" in self.request.META:
|
||||
return self.request.META.get('HTTP_REFERER')
|
||||
else:
|
||||
return reverse('presale:event.index', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
})
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_next_url()
|
||||
|
||||
def get_failure_url(self):
|
||||
return self.get_next_url()
|
||||
|
||||
def _items_from_post_data(self):
|
||||
"""
|
||||
Parses the POST data and returns a list of tuples in the
|
||||
form (item id, variation id or None, number)
|
||||
"""
|
||||
items = []
|
||||
for key, value in self.request.POST.items():
|
||||
if value.strip() == '':
|
||||
continue
|
||||
if key.startswith('item_'):
|
||||
try:
|
||||
items.append((key.split("_")[1], None, int(value)))
|
||||
except ValueError:
|
||||
messages.error(self.request, _('Please enter numbers only.'))
|
||||
return False
|
||||
elif key.startswith('variation_'):
|
||||
try:
|
||||
items.append((key.split("_")[1], key.split("_")[2], int(value)))
|
||||
except ValueError:
|
||||
messages.error(self.request, _('Please enter numbers only.'))
|
||||
return False
|
||||
if len(items) == 0:
|
||||
messages.warning(self.request, _('You did not select any items.'))
|
||||
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:
|
||||
return redirect(self.get_failure_url())
|
||||
|
||||
if sum(i[2] for i in items) > self.request.event.max_items_per_order:
|
||||
# TODO: i18n plurals
|
||||
messages.error(self.request,
|
||||
_("You cannot select more than %d items per order") % self.event.max_items_per_order)
|
||||
return redirect(self.get_failure_url())
|
||||
|
||||
# Fetch items from the database
|
||||
items_cache = {
|
||||
i.identity: i for i
|
||||
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.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:
|
||||
# Check whether the specified items are part of what we just fetched from the database
|
||||
# If they are not, the user supplied item IDs which either do not exist or belong to
|
||||
# a different event
|
||||
if i[0] not in items_cache or (i[1] is not None and i[1] not in variations_cache):
|
||||
messages.error(self.request, _('You selected an item which is not available for sale.'))
|
||||
return redirect(self.get_failure_url())
|
||||
|
||||
item = items_cache[i[0]]
|
||||
variation = variations_cache[i[1]] if i[1] is not None else None
|
||||
|
||||
# Execute restriction plugins to check whether they (a) change the price or
|
||||
# (b) make the item/variation unavailable. If neither is the case, check_restriction
|
||||
# will correctly return the default price
|
||||
price = item.check_restrictions() if variation is None else variation.check_restrictions()
|
||||
if price is False:
|
||||
if not msg_some_unavailable:
|
||||
msg_some_unavailable = True
|
||||
messages.error(self.request,
|
||||
_('Some of the items you selected were no longer available. '
|
||||
'Please see below for details.'))
|
||||
continue
|
||||
|
||||
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
|
||||
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
|
||||
if len(quotas) == 0:
|
||||
if not msg_some_unavailable:
|
||||
msg_some_unavailable = True
|
||||
messages.error(self.request,
|
||||
_('Some of the items you selected were no longer available. '
|
||||
'Please see below for details.'))
|
||||
continue
|
||||
|
||||
# Assume that all quotas allow us to buy i[2] instances of the object
|
||||
quota_ok = i[2]
|
||||
try:
|
||||
for quota in quotas:
|
||||
# Lock the quota, so no other thread is allowed to perform sales covered by this
|
||||
# quota while we're doing so.
|
||||
quota.lock()
|
||||
avail = quota.availability()
|
||||
if avail[0] != Quota.AVAILABILITY_OK:
|
||||
# This quota is sold out/currently unavailable, so do not sell this at all
|
||||
if not msg_some_unavailable:
|
||||
msg_some_unavailable = True
|
||||
messages.error(self.request,
|
||||
_('Some of the items you selected were no longer available. '
|
||||
'Please see below for details.'))
|
||||
quota_ok = 0
|
||||
break
|
||||
elif avail[1] < i[2]:
|
||||
# This quota is available, but with less than i[2] items left, so we have to
|
||||
# reduce the number of bought items
|
||||
if not msg_some_unavailable:
|
||||
msg_some_unavailable = True
|
||||
messages.error(self.request,
|
||||
_('Some of the items you selected were no longer available in '
|
||||
'the quantity you selected. Please see below for details.'))
|
||||
quota_ok = min(quota_ok, avail[1])
|
||||
|
||||
# Create a CartPosition for as much items as we can
|
||||
for k in range(quota_ok):
|
||||
CartPosition.objects.create(
|
||||
event=self.request.event,
|
||||
session=self.get_session_key(),
|
||||
user=(self.request.user if self.request.user.is_authenticated() else None),
|
||||
item=item,
|
||||
variation=variation,
|
||||
price=price,
|
||||
expires=now() + timedelta(minutes=30)
|
||||
)
|
||||
except Quota.LockTimeoutException:
|
||||
# Is raised when there are too many threads asking for quota locks and we were
|
||||
# unaible to get one
|
||||
if not msg_some_unavailable:
|
||||
msg_some_unavailable = True
|
||||
messages.error(self.request,
|
||||
_('We were not able to process your request completely as the '
|
||||
'server was too busy. Please try again.'))
|
||||
finally:
|
||||
# Release the locks. This is important ;)
|
||||
for quota in quotas:
|
||||
quota.release()
|
||||
|
||||
if not msg_some_unavailable:
|
||||
messages.success(self.request, _('The items have been successfully added to your cart.'))
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
45
src/pretix/presale/views/event.py
Normal file
45
src/pretix/presale/views/event.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.db.models import Count
|
||||
from django.views.generic import TemplateView
|
||||
from pretix.presale.views import EventViewMixin, CartDisplayMixin
|
||||
|
||||
|
||||
class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/index.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Fetch all items
|
||||
items = self.request.event.items.all().select_related(
|
||||
'category', # for re-grouping
|
||||
).prefetch_related(
|
||||
'properties', # for .get_all_available_variations()
|
||||
'quotas', 'variations__quotas' # for .availability()
|
||||
).annotate(quotac=Count('quotas')).filter(
|
||||
quotac__gt=0
|
||||
).order_by('category__position', 'category_id', 'name')
|
||||
|
||||
for item in items:
|
||||
item.available_variations = sorted(item.get_all_available_variations(),
|
||||
key=lambda vd: vd.ordered_values())
|
||||
item.has_variations = (len(item.available_variations) != 1
|
||||
or not item.available_variations[0].empty())
|
||||
if not item.has_variations:
|
||||
item.cached_availability = list(item.check_quotas())
|
||||
item.cached_availability[1] = min(item.cached_availability[1],
|
||||
self.request.event.max_items_per_order)
|
||||
item.price = item.available_variations[0]['price']
|
||||
else:
|
||||
for var in item.available_variations:
|
||||
var.cached_availability = list(var['variation'].check_quotas())
|
||||
var.cached_availability[1] = min(var.cached_availability[1],
|
||||
self.request.event.max_items_per_order)
|
||||
|
||||
# Regroup those by category
|
||||
context['items_by_category'] = sorted([
|
||||
# a group is a tuple of a category and a list of items
|
||||
(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
|
||||
Reference in New Issue
Block a user