diff --git a/src/pretix/base/services/stats.py b/src/pretix/base/services/stats.py
index 931eb87c38..d8fd8fb3d4 100644
--- a/src/pretix/base/services/stats.py
+++ b/src/pretix/base/services/stats.py
@@ -28,48 +28,51 @@ def tuplesum(tuples: Iterable[Tuple]) -> Tuple:
return tuple(map(mysum, zip(*list(tuples))))
+def dictsum(*dicts) -> dict:
+ res = {}
+ keys = set()
+ for d in dicts:
+ keys |= set(d.keys())
+ for k in keys:
+ res[k] = (sum(d[k][0] for d in dicts if k in d), sum(d[k][1] for d in dicts if k in d))
+ return res
+
+
def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related(
'category', # for re-grouping
+ ).prefetch_related(
+ 'variations'
).order_by('category__position', 'category_id', 'name')
- num_total = {
- (p['item'], p['variation']): (p['cnt'], p['price'])
- for p in (OrderPosition.objects
- .filter(order__event=event,
- order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED, Order.STATUS_PAID])
- .values('item', 'variation')
- .annotate(cnt=Count('id'), price=Sum('price')).order_by())
- }
+ counters = OrderPosition.objects.filter(
+ order__event=event
+ ).values(
+ 'item', 'variation', 'order__status'
+ ).annotate(cnt=Count('id'), price=Sum('price')).order_by()
+
num_canceled = {
(p['item'], p['variation']): (p['cnt'], p['price'])
- for p in (OrderPosition.objects
- .filter(order__event=event, order__status=Order.STATUS_CANCELED)
- .values('item', 'variation')
- .annotate(cnt=Count('id'), price=Sum('price')).order_by())
+ for p in counters if p['order__status'] == Order.STATUS_CANCELED
}
num_refunded = {
(p['item'], p['variation']): (p['cnt'], p['price'])
- for p in (OrderPosition.objects
- .filter(order__event=event, order__status=Order.STATUS_REFUNDED)
- .values('item', 'variation')
- .annotate(cnt=Count('id'), price=Sum('price')).order_by())
- }
- num_pending = {
- (p['item'], p['variation']): (p['cnt'], p['price'])
- for p in (OrderPosition.objects
- .filter(order__event=event,
- order__status__in=(Order.STATUS_PENDING, Order.STATUS_EXPIRED))
- .values('item', 'variation')
- .annotate(cnt=Count('id'), price=Sum('price')).order_by())
+ for p in counters if p['order__status'] == Order.STATUS_REFUNDED
}
num_paid = {
(p['item'], p['variation']): (p['cnt'], p['price'])
- for p in (OrderPosition.objects
- .filter(order__event=event, order__status=Order.STATUS_PAID)
- .values('item', 'variation')
- .annotate(cnt=Count('id'), price=Sum('price')).order_by())
+ for p in counters if p['order__status'] == Order.STATUS_PAID
}
+ num_s_pending = {
+ (p['item'], p['variation']): (p['cnt'], p['price'])
+ for p in counters if p['order__status'] == Order.STATUS_PENDING
+ }
+ num_expired = {
+ (p['item'], p['variation']): (p['cnt'], p['price'])
+ for p in counters if p['order__status'] == Order.STATUS_EXPIRED
+ }
+ num_pending = dictsum(num_s_pending, num_expired)
+ num_total = dictsum(num_pending, num_paid)
for item in items:
item.all_variations = list(item.variations.all())
@@ -120,41 +123,33 @@ def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]],
payment_cat_obj = DummyObject()
payment_cat_obj.name = _('Payment method fees')
payment_items = []
- num_total = {
- o['payment_provider']: (o['cnt'], o['payment_fee'])
- for o in (Order.objects
- .filter(event=event)
- .values('payment_provider')
- .annotate(cnt=Count('id'), payment_fee=Sum('payment_fee')).order_by())
- }
+
+ counters = event.orders.values('payment_provider', 'status').annotate(
+ cnt=Count('id'), payment_fee=Sum('payment_fee')
+ ).order_by()
+
num_canceled = {
o['payment_provider']: (o['cnt'], o['payment_fee'])
- for o in (Order.objects
- .filter(event=event, status=Order.STATUS_CANCELED)
- .values('payment_provider')
- .annotate(cnt=Count('id'), payment_fee=Sum('payment_fee')).order_by())
+ for o in counters if o['status'] == Order.STATUS_CANCELED
}
num_refunded = {
o['payment_provider']: (o['cnt'], o['payment_fee'])
- for o in (Order.objects
- .filter(event=event, status=Order.STATUS_REFUNDED)
- .values('payment_provider')
- .annotate(cnt=Count('id'), payment_fee=Sum('payment_fee')).order_by())
+ for o in counters if o['status'] == Order.STATUS_REFUNDED
}
- num_pending = {
+ num_s_pending = {
o['payment_provider']: (o['cnt'], o['payment_fee'])
- for o in (Order.objects
- .filter(event=event, status__in=(Order.STATUS_PENDING, Order.STATUS_EXPIRED))
- .values('payment_provider')
- .annotate(cnt=Count('id'), payment_fee=Sum('payment_fee')).order_by())
+ for o in counters if o['status'] == Order.STATUS_PENDING
+ }
+ num_expired = {
+ o['payment_provider']: (o['cnt'], o['payment_fee'])
+ for o in counters if o['status'] == Order.STATUS_EXPIRED
}
num_paid = {
o['payment_provider']: (o['cnt'], o['payment_fee'])
- for o in (Order.objects
- .filter(event=event, status=Order.STATUS_PAID)
- .values('payment_provider')
- .annotate(cnt=Count('id'), payment_fee=Sum('payment_fee')).order_by())
+ for o in counters if o['status'] == Order.STATUS_PAID
}
+ num_pending = dictsum(num_s_pending, num_expired)
+ num_total = dictsum(num_pending, num_paid)
provider_names = {}
responses = register_payment_providers.send(event)
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index bfa21a6581..b5da8ca6bc 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -514,8 +514,15 @@ class SettingsSandbox:
self._event.settings.set(self._convert_key(key), value)
-class GlobalSettingsObject:
+class GlobalSettingsObject():
+ data_dict = None
+
def __init__(self):
- self.settings = SettingsProxy(self, type=GlobalSetting)
- self.setting_objects = GlobalSetting.objects
- self.slug = '_global'
+ # This is a singleton-like object. Multiple objects can exist, but they share their state
+ if GlobalSettingsObject.data_dict:
+ self.__dict__ = GlobalSettingsObject.data_dict
+ else:
+ self.settings = SettingsProxy(self, type=GlobalSetting)
+ self.setting_objects = GlobalSetting.objects
+ self.slug = '_global'
+ GlobalSettingsObject.data_dict = self.__dict__
diff --git a/src/pretix/control/templates/pretixcontrol/items/quota.html b/src/pretix/control/templates/pretixcontrol/items/quota.html
index 268604c286..cb9d964935 100644
--- a/src/pretix/control/templates/pretixcontrol/items/quota.html
+++ b/src/pretix/control/templates/pretixcontrol/items/quota.html
@@ -31,26 +31,16 @@
{% if quota.size == None %}{% trans "Infinite" %}{% else %}{{ quota.size }}{% endif %}
-
-
{% trans "Paid orders" %}
-
– {{ quota.count_paid_orders }}
-
-
-
{% trans "Pending orders" %}
-
– {{ quota.count_pending_orders }}
-
-
-
{% trans "Vouchers" %}
-
– {{ quota.count_blocking_vouchers }}
-
-
-
{% trans "Current user's carts" %}
-
– {{ quota.count_in_cart }}
-
+ {% for row in quota_table_rows %}
+
+
{{ row.label }}
+
– {{ row.value }}
+
+ {% endfor %}
{% trans "Current availability" %}
- {% if quota.size == None %}{% trans "Infinite" %}{% else %}{{ quota.availability.1 }}{% endif %}
+ {% if quota.size == None %}{% trans "Infinite" %}{% else %}{{ avail.1 }}{% endif %}
diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py
index 7a29bf09d6..938030953c 100644
--- a/src/pretix/control/views/item.py
+++ b/src/pretix/control/views/item.py
@@ -330,8 +330,8 @@ class QuestionMixin:
can_order=False, can_delete=True, extra=0
)
return formsetclass(self.request.POST if self.request.method == "POST" else None,
- queryset=(QuestionOption.objects.filter(question=self.get_object())
- if self.get_object() else QuestionOption.objects.none()),
+ queryset=(QuestionOption.objects.filter(question=self.object)
+ if self.object else QuestionOption.objects.none()),
event=self.request.event)
def save_formset(self, obj):
@@ -423,8 +423,9 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['items'] = self.object.items.all()
- ctx['stats'] = self.get_answer_statistics()
- ctx['stats_json'] = json.dumps(self.get_answer_statistics())
+ stats = self.get_answer_statistics()
+ ctx['stats'] = stats
+ ctx['stats_json'] = json.dumps(stats)
return ctx
def get_object(self, queryset=None) -> Question:
@@ -599,6 +600,10 @@ class QuotaView(ChartContainingView, DetailView):
def get_context_data(self, *args, **kwargs):
ctx = super().get_context_data()
+
+ avail = self.object.availability()
+ ctx['avail'] = avail
+
data = [
{
'label': ugettext('Paid orders'),
@@ -617,10 +622,12 @@ class QuotaView(ChartContainingView, DetailView):
'value': self.object.count_in_cart()
}
]
+ ctx['quota_table_rows'] = list(data)
+
if self.object.size is not None:
data.append({
'label': ugettext('Current availability'),
- 'value': self.object.availability()[1]
+ 'value': avail[1]
})
ctx['quota_chart_data'] = json.dumps(data)
return ctx
diff --git a/src/pretix/control/views/main.py b/src/pretix/control/views/main.py
index 3a30d25d5f..0c8795f3de 100644
--- a/src/pretix/control/views/main.py
+++ b/src/pretix/control/views/main.py
@@ -20,8 +20,8 @@ class EventList(ListView):
def get_queryset(self):
return Event.objects.filter(
permitted__id__exact=self.request.user.pk
- ).prefetch_related(
- "organizer",
+ ).select_related("organizer").prefetch_related(
+ "setting_objects", "organizer__setting_objects"
)
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py
index 9c9f868345..817e4bbe65 100644
--- a/src/pretix/presale/views/__init__.py
+++ b/src/pretix/presale/views/__init__.py
@@ -15,33 +15,24 @@ class CartMixin:
"""
A list of this users cart position
"""
- return list(CartPosition.objects.filter(
- cart_id=self.request.session.session_key, event=self.request.event
- ).order_by(
- 'item', 'variation'
- ).select_related(
- 'item', 'variation'
- ).prefetch_related(
- 'item__questions', 'answers'
- ))
+ return list(get_cart(self.request))
def get_cart(self, answers=False, queryset=None, payment_fee=None, payment_fee_tax_rate=None, downloads=False):
- queryset = queryset or CartPosition.objects.filter(
- cart_id=self.request.session.session_key, event=self.request.event
- )
+ if queryset:
+ prefetch = []
+ if answers:
+ prefetch.append('item__questions')
+ prefetch.append('answers')
- prefetch = []
- if answers:
- prefetch.append('item__questions')
- prefetch.append('answers')
-
- cartpos = queryset.order_by(
- 'item', 'variation'
- ).select_related(
- 'item', 'variation'
- ).prefetch_related(
- *prefetch
- )
+ cartpos = queryset.order_by(
+ 'item', 'variation'
+ ).select_related(
+ 'item', 'variation'
+ ).prefetch_related(
+ *prefetch
+ )
+ else:
+ cartpos = self.positions
# Group items of the same variation
# We do this by list manipulations instead of a GROUP BY query, as
@@ -101,6 +92,20 @@ class CartMixin:
return payment_fee
+def get_cart(request):
+ if not hasattr(request, '_cart_cache'):
+ request._cart_cache = CartPosition.objects.filter(
+ cart_id=request.session.session_key, event=request.event
+ ).order_by(
+ 'item', 'variation'
+ ).select_related(
+ 'item', 'variation'
+ ).prefetch_related(
+ 'item__questions', 'answers'
+ )
+ return request._cart_cache
+
+
class EventViewMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py
index 55ce2a1211..fc8782fe72 100644
--- a/src/pretix/presale/views/checkout.py
+++ b/src/pretix/presale/views/checkout.py
@@ -4,15 +4,17 @@ from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
+from pretix.base.models import CartPosition
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.checkoutflow import get_checkout_flow
-from pretix.presale.views import CartMixin
-class CheckoutView(CartMixin, View):
+class CheckoutView(View):
def dispatch(self, request, *args, **kwargs):
self.request = request
- if not self.positions and "async_id" not in request.GET:
+ has_cart = CartPosition.objects.filter(
+ cart_id=self.request.session.session_key, event=self.request.event).exists()
+ if not has_cart and "async_id" not in request.GET:
messages.error(request, _("Your cart is empty"))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py
index 3322612027..9b854aa52d 100644
--- a/src/pretix/presale/views/event.py
+++ b/src/pretix/presale/views/event.py
@@ -1,9 +1,11 @@
import sys
-from django.db.models import Count, Q
+from django.db.models import Count, Prefetch, Q
from django.utils.timezone import now
from django.views.generic import TemplateView
+from pretix.base.models import ItemVariation
+
from . import CartMixin, EventViewMixin
@@ -21,48 +23,56 @@ def item_group_by_category(items):
)
+def get_grouped_items(event):
+ items = event.items.all().filter(
+ Q(active=True)
+ & Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
+ & Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
+ & Q(hide_without_voucher=False)
+ ).select_related(
+ 'category', # for re-grouping
+ ).prefetch_related(
+ 'quotas', 'variations__quotas', 'quotas__event', # for .availability()
+ Prefetch('variations', to_attr='available_variations',
+ queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).distinct()),
+ ).annotate(
+ quotac=Count('quotas'),
+ has_variations=Count('variations')
+ ).filter(
+ quotac__gt=0
+ ).order_by('category__position', 'category_id', 'position', 'name')
+ display_add_to_cart = False
+ for item in items:
+ if not item.has_variations:
+ item.cached_availability = list(item.check_quotas())
+ item.order_max = min(item.cached_availability[1]
+ if item.cached_availability[1] is not None else sys.maxsize,
+ int(event.settings.max_items_per_order))
+ item.price = item.default_price
+ display_add_to_cart = display_add_to_cart or item.order_max > 0
+ else:
+ for var in item.available_variations:
+ var.cached_availability = list(var.check_quotas())
+ var.order_max = min(var.cached_availability[1]
+ if var.cached_availability[1] is not None else sys.maxsize,
+ int(event.settings.max_items_per_order))
+ display_add_to_cart = display_add_to_cart or var.order_max > 0
+ var.price = var.default_price if var.default_price is not None else item.default_price
+ if len(item.available_variations) > 0:
+ item.min_price = min([v.price for v in item.available_variations])
+ item.max_price = max([v.price for v in item.available_variations])
+
+ items = [item for item in items if len(item.available_variations) > 0 or not item.has_variations]
+ return items, display_add_to_cart
+
+
class EventIndex(EventViewMixin, CartMixin, 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().filter(
- Q(active=True)
- & Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
- & Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
- & Q(hide_without_voucher=False)
- ).select_related(
- 'category', # for re-grouping
- ).prefetch_related(
- 'quotas', 'variations__quotas', 'quotas__event' # for .availability()
- ).annotate(quotac=Count('quotas')).filter(
- quotac__gt=0
- ).order_by('category__position', 'category_id', 'position', 'name')
- display_add_to_cart = False
- for item in items:
- item.available_variations = list(item.variations.filter(active=True, quotas__isnull=False).distinct())
- item.has_variations = item.variations.exists()
- if not item.has_variations:
- item.cached_availability = list(item.check_quotas())
- item.order_max = min(item.cached_availability[1]
- if item.cached_availability[1] is not None else sys.maxsize,
- int(self.request.event.settings.max_items_per_order))
- item.price = item.default_price
- display_add_to_cart = display_add_to_cart or item.order_max > 0
- else:
- for var in item.available_variations:
- var.cached_availability = list(var.check_quotas())
- var.order_max = min(var.cached_availability[1]
- if var.cached_availability[1] is not None else sys.maxsize,
- int(self.request.event.settings.max_items_per_order))
- display_add_to_cart = display_add_to_cart or var.order_max > 0
- var.price = var.default_price if var.default_price is not None else item.default_price
- if len(item.available_variations) > 0:
- item.min_price = min([v.price for v in item.available_variations])
- item.max_price = max([v.price for v in item.available_variations])
-
- items = [item for item in items if len(item.available_variations) > 0 or not item.has_variations]
+ items, display_add_to_cart = get_grouped_items(self.request.event)
# Regroup those by category
context['items_by_category'] = item_group_by_category(items)
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
index c5086dd245..5967778482 100644
--- a/src/pretix/presale/views/order.py
+++ b/src/pretix/presale/views/order.py
@@ -34,7 +34,7 @@ class OrderDetailMixin:
@cached_property
def order(self):
try:
- order = Order.objects.get(event=self.request.event, code=self.kwargs['order'])
+ order = self.request.event.orders.get(code=self.kwargs['order'])
if order.secret.lower() == self.kwargs['secret'].lower():
return order
else: