forked from CGM_Public/pretix_original
Refactor quota calculation (#1668)
This commit is contained in:
@@ -214,8 +214,10 @@ class EventMixin:
|
||||
vars_reserved = set()
|
||||
items_gone = set()
|
||||
vars_gone = set()
|
||||
|
||||
r = getattr(self, '_quota_cache', {})
|
||||
for q in self.active_quotas:
|
||||
res = q.availability(allow_cache=True)
|
||||
res = r[q] if q in r else q.availability(allow_cache=True)
|
||||
|
||||
if res[0] == Quota.AVAILABILITY_OK:
|
||||
if q.active_items:
|
||||
|
||||
@@ -7,11 +7,10 @@ from typing import Tuple
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import F, Func, Q, Sum
|
||||
from django.db.models import Q
|
||||
from django.utils import formats
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -25,7 +24,6 @@ from pretix.base.models import fields
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.signals import quota_availability
|
||||
|
||||
from .event import Event, SubEvent
|
||||
|
||||
@@ -1350,6 +1348,7 @@ class Quota(LoggedModel):
|
||||
self.event.cache.clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# This is *not* called when the db-level cache is upated, since we use bulk_update there
|
||||
clear_cache = kwargs.pop('clear_cache', True)
|
||||
super().save(*args, **kwargs)
|
||||
if self.event and clear_cache:
|
||||
@@ -1384,6 +1383,8 @@ class Quota(LoggedModel):
|
||||
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
|
||||
and the second is the number of available tickets.
|
||||
"""
|
||||
from ..services.quotas import QuotaAvailability
|
||||
|
||||
if allow_cache and self.cache_is_hot() and count_waitinglist:
|
||||
return self.cached_availability_state, self.cached_availability_number
|
||||
|
||||
@@ -1392,141 +1393,16 @@ class Quota(LoggedModel):
|
||||
|
||||
if _cache is not None and self.pk in _cache:
|
||||
return _cache[self.pk]
|
||||
now_dt = now_dt or now()
|
||||
res = self._availability(now_dt, count_waitinglist)
|
||||
for recv, resp in quota_availability.send(sender=self.event, quota=self, result=res,
|
||||
count_waitinglist=count_waitinglist):
|
||||
res = resp
|
||||
|
||||
if res[0] <= Quota.AVAILABILITY_ORDERED and self.close_when_sold_out and not self.closed:
|
||||
self.closed = True
|
||||
self.save(update_fields=['closed'])
|
||||
self.log_action('pretix.event.quota.closed')
|
||||
|
||||
self.event.cache.delete('item_quota_cache')
|
||||
rewrite_cache = count_waitinglist and (
|
||||
not self.cache_is_hot(now_dt) or res[0] > self.cached_availability_state
|
||||
)
|
||||
if rewrite_cache:
|
||||
self.cached_availability_state = res[0]
|
||||
self.cached_availability_number = res[1]
|
||||
self.cached_availability_time = now_dt
|
||||
if self.size is None:
|
||||
self.cached_availability_paid_orders = self.count_paid_orders()
|
||||
self.save(
|
||||
update_fields=[
|
||||
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
|
||||
'cached_availability_paid_orders'
|
||||
],
|
||||
clear_cache=False,
|
||||
using='default'
|
||||
)
|
||||
qa = QuotaAvailability(count_waitinglist=count_waitinglist, early_out=False)
|
||||
qa.queue(self)
|
||||
qa.compute(now_dt=now_dt)
|
||||
res = qa.results[self]
|
||||
|
||||
if _cache is not None:
|
||||
_cache[self.pk] = res
|
||||
_cache['_count_waitinglist'] = count_waitinglist
|
||||
return res
|
||||
|
||||
def _availability(self, now_dt: datetime=None, count_waitinglist=True, ignore_closed=False):
|
||||
now_dt = now_dt or now()
|
||||
if self.closed and not ignore_closed:
|
||||
return Quota.AVAILABILITY_ORDERED, 0
|
||||
|
||||
size_left = self.size
|
||||
if size_left is None:
|
||||
return Quota.AVAILABILITY_OK, None
|
||||
|
||||
paid_orders = self.count_paid_orders()
|
||||
self.cached_availability_paid_orders = paid_orders
|
||||
size_left -= paid_orders
|
||||
if size_left <= 0:
|
||||
return Quota.AVAILABILITY_GONE, 0
|
||||
|
||||
size_left -= self.count_pending_orders()
|
||||
if size_left <= 0:
|
||||
return Quota.AVAILABILITY_ORDERED, 0
|
||||
|
||||
size_left -= self.count_blocking_vouchers(now_dt)
|
||||
if size_left <= 0:
|
||||
return Quota.AVAILABILITY_ORDERED, 0
|
||||
|
||||
if count_waitinglist:
|
||||
size_left -= self.count_waiting_list_pending()
|
||||
if size_left <= 0:
|
||||
return Quota.AVAILABILITY_ORDERED, 0
|
||||
|
||||
size_left -= self.count_in_cart(now_dt)
|
||||
if size_left <= 0:
|
||||
return Quota.AVAILABILITY_RESERVED, 0
|
||||
|
||||
return Quota.AVAILABILITY_OK, size_left
|
||||
|
||||
def count_blocking_vouchers(self, now_dt: datetime=None) -> int:
|
||||
from pretix.base.models import Voucher
|
||||
|
||||
now_dt = now_dt or now()
|
||||
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
|
||||
func = 'MAX'
|
||||
else: # NOQA
|
||||
func = 'GREATEST'
|
||||
|
||||
return Voucher.objects.filter(
|
||||
Q(event=self.event) & Q(subevent=self.subevent) &
|
||||
Q(block_quota=True) &
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
|
||||
Q(Q(self._position_lookup) | Q(quota=self))
|
||||
).values('id').aggregate(
|
||||
free=Sum(Func(F('max_usages') - F('redeemed'), 0, function=func))
|
||||
)['free'] or 0
|
||||
|
||||
def count_waiting_list_pending(self) -> int:
|
||||
from pretix.base.models import WaitingListEntry
|
||||
return WaitingListEntry.objects.filter(
|
||||
Q(voucher__isnull=True) & Q(subevent=self.subevent) &
|
||||
self._position_lookup
|
||||
).distinct().count()
|
||||
|
||||
def count_in_cart(self, now_dt: datetime=None) -> int:
|
||||
from pretix.base.models import CartPosition
|
||||
|
||||
now_dt = now_dt or now()
|
||||
return CartPosition.objects.filter(
|
||||
Q(event=self.event) & Q(subevent=self.subevent) &
|
||||
Q(expires__gte=now_dt) &
|
||||
Q(
|
||||
Q(voucher__isnull=True)
|
||||
| Q(voucher__block_quota=False)
|
||||
| Q(voucher__valid_until__lt=now_dt)
|
||||
) &
|
||||
self._position_lookup
|
||||
).count()
|
||||
|
||||
def count_pending_orders(self) -> dict:
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
|
||||
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
|
||||
return OrderPosition.objects.filter(
|
||||
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event, subevent=self.subevent
|
||||
).count()
|
||||
|
||||
def count_paid_orders(self):
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
|
||||
return OrderPosition.objects.filter(
|
||||
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event, subevent=self.subevent
|
||||
).count()
|
||||
|
||||
@cached_property
|
||||
def _position_lookup(self) -> Q:
|
||||
return (
|
||||
( # Orders for items which do not have any variations
|
||||
Q(variation__isnull=True) &
|
||||
Q(item_id__in=Quota.items.through.objects.filter(quota_id=self.pk).values_list('item_id', flat=True))
|
||||
) | ( # Orders for items which do have any variations
|
||||
Q(variation__in=Quota.variations.through.objects.filter(quota_id=self.pk).values_list('itemvariation_id', flat=True))
|
||||
)
|
||||
)
|
||||
|
||||
class QuotaExceededException(Exception):
|
||||
pass
|
||||
|
||||
@@ -1535,7 +1411,6 @@ class Quota(LoggedModel):
|
||||
for variation in (variations or []):
|
||||
if variation.item not in items:
|
||||
raise ValidationError(_('All variations must belong to an item contained in the items list.'))
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def clean_items(event, items, variations):
|
||||
|
||||
Reference in New Issue
Block a user