mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
Refactor quota calculation (#1668)
This commit is contained in:
@@ -14,6 +14,7 @@ from pretix.base.models import (
|
||||
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
from ..exporter import ListExporter, MultiSheetListExporter
|
||||
@@ -516,16 +517,21 @@ class QuotaListExporter(ListExporter):
|
||||
]
|
||||
yield headers
|
||||
|
||||
for quota in self.event.quotas.all():
|
||||
avail = quota.availability()
|
||||
quotas = list(self.event.quotas.all())
|
||||
qa = QuotaAvailability(full_results=True)
|
||||
qa.queue(*quotas)
|
||||
qa.compute()
|
||||
|
||||
for quota in quotas:
|
||||
avail = qa.results[quota]
|
||||
row = [
|
||||
quota.name,
|
||||
_('Infinite') if quota.size is None else quota.size,
|
||||
quota.count_paid_orders(),
|
||||
quota.count_pending_orders(),
|
||||
quota.count_blocking_vouchers(),
|
||||
quota.count_in_cart(),
|
||||
quota.count_waiting_list_pending(),
|
||||
qa.count_paid_orders[quota],
|
||||
qa.count_pending_orders[quota],
|
||||
qa.count_vouchers[quota],
|
||||
qa.count_cart[quota],
|
||||
qa.count_waitinglist[quota],
|
||||
_('Infinite') if avail[1] is None else avail[1]
|
||||
]
|
||||
yield row
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -25,6 +25,7 @@ from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.services.checkin import _save_answers
|
||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import validate_cart_addons
|
||||
@@ -728,10 +729,14 @@ class CartManager:
|
||||
|
||||
def _get_quota_availability(self):
|
||||
quotas_ok = defaultdict(int)
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*[k for k, v in self._quota_diff.items() if v > 0])
|
||||
qa.compute(now_dt=self.now_dt)
|
||||
for quota, count in self._quota_diff.items():
|
||||
if count <= 0:
|
||||
quotas_ok[quota] = 0
|
||||
avail = quota.availability(self.now_dt)
|
||||
break
|
||||
avail = qa.results[quota]
|
||||
if avail[1] is not None and avail[1] < count:
|
||||
quotas_ok[quota] = min(count, avail[1])
|
||||
else:
|
||||
|
||||
@@ -44,6 +44,7 @@ from pretix.base.services.invoices import (
|
||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||
from pretix.base.signals import (
|
||||
allow_ticket_download, order_approved, order_canceled, order_changed,
|
||||
@@ -1391,10 +1392,13 @@ class OrderChangeManager:
|
||||
raise OrderError(self.error_messages['seat_subevent_mismatch'].format(seat=v['seat'].name))
|
||||
|
||||
def _check_quotas(self):
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*[k for k, v in self._quotadiff.items() if v > 0])
|
||||
qa.compute()
|
||||
for quota, diff in self._quotadiff.items():
|
||||
if diff <= 0:
|
||||
continue
|
||||
avail = quota.availability()
|
||||
avail = qa.results[quota]
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < diff):
|
||||
raise OrderError(self.error_messages['quota'].format(name=quota.name))
|
||||
|
||||
|
||||
@@ -1,15 +1,357 @@
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Max, Q
|
||||
from django.db.models import Count, F, Func, Max, Q, Sum
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Event, LogEntry
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, LogEntry, Order, OrderPosition, Quota, Voucher,
|
||||
WaitingListEntry,
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
|
||||
from ..signals import periodic_task
|
||||
from ..signals import periodic_task, quota_availability
|
||||
|
||||
|
||||
class QuotaAvailability:
|
||||
"""
|
||||
This special object allows so compute the availability of multiple quotas, even across events, and inspect their
|
||||
results. The maximum number of SQL queries is constant and not dependent on the number of quotas.
|
||||
|
||||
Usage example::
|
||||
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(quota1, quota2, …)
|
||||
qa.compute()
|
||||
print(qa.results)
|
||||
|
||||
Properties you can access after computation.
|
||||
|
||||
* results (dict mapping quotas to availability tuples)
|
||||
* count_paid_orders (dict mapping quotas to ints)
|
||||
* count_paid_orders (dict mapping quotas to ints)
|
||||
* count_pending_orders (dict mapping quotas to ints)
|
||||
* count_vouchers (dict mapping quotas to ints)
|
||||
* count_waitinglist (dict mapping quotas to ints)
|
||||
* count_cart (dict mapping quotas to ints)
|
||||
"""
|
||||
|
||||
def __init__(self, count_waitinglist=True, ignore_closed=False, full_results=False, early_out=True):
|
||||
"""
|
||||
Initialize a new quota availability calculator
|
||||
|
||||
:param count_waitinglist: If ``True`` (default), the waiting list is considered. If ``False``, it is ignored.
|
||||
|
||||
:param ignore_closed: Quotas have a ``closed`` state that always makes the quota return as sold out. If you set
|
||||
``ignore_closed`` to ``True``, we will ignore this completely. Default is ``False``.
|
||||
|
||||
:param full_results: Usually, the computation is as efficient as possible, i.e. if after counting the sold
|
||||
orders we already see that the quota is sold out, we're not going to count the carts,
|
||||
since it does not matter. This also means that you will not be able to get that number from
|
||||
``.count_cart``. If you want all parts to be calculated (i.e. because you want to show
|
||||
statistics to the user), pass ``full_results`` and we'll skip that optimization.
|
||||
items
|
||||
|
||||
:param early_out: Usually, if a quota is ``closed`` or if its ``size`` is ``None`` (i.e. unlimited), we will
|
||||
not need database access to determine the availability and return it right away. If you set
|
||||
this to ``False``, however, we will *still* count the number of orders, which is required to
|
||||
keep the database-level quota cache up to date so backend overviews render quickly. If you
|
||||
do not care about keeping the cache up to date, you can set this to ``False`` for further
|
||||
performance improvements.
|
||||
"""
|
||||
self._queue = []
|
||||
self._count_waitinglist = count_waitinglist
|
||||
self._ignore_closed = ignore_closed
|
||||
self._full_results = full_results
|
||||
self._item_to_quotas = defaultdict(list)
|
||||
self._var_to_quotas = defaultdict(list)
|
||||
self._early_out = early_out
|
||||
self._quota_objects = {}
|
||||
self.results = {}
|
||||
self.count_paid_orders = defaultdict(int)
|
||||
self.count_pending_orders = defaultdict(int)
|
||||
self.count_vouchers = defaultdict(int)
|
||||
self.count_waitinglist = defaultdict(int)
|
||||
self.count_cart = defaultdict(int)
|
||||
|
||||
self.sizes = {}
|
||||
|
||||
def queue(self, *quota):
|
||||
self._queue += quota
|
||||
|
||||
def compute(self, now_dt=None):
|
||||
now_dt = now_dt or now()
|
||||
quotas = list(self._queue)
|
||||
quotas_original = list(self._queue)
|
||||
self._queue.clear()
|
||||
if not quotas:
|
||||
return
|
||||
|
||||
self._compute(quotas, now_dt)
|
||||
|
||||
for q in quotas_original:
|
||||
for recv, resp in quota_availability.send(sender=q.event, quota=q, result=self.results[q],
|
||||
count_waitinglist=self.count_waitinglist):
|
||||
self.results[q] = resp
|
||||
|
||||
self._close(quotas)
|
||||
self._write_cache(quotas, now_dt)
|
||||
|
||||
def _write_cache(self, quotas, now_dt):
|
||||
events = {q.event for q in quotas}
|
||||
update = []
|
||||
for e in events:
|
||||
e.cache.delete('item_quota_cache')
|
||||
for q in quotas:
|
||||
rewrite_cache = self._count_waitinglist and (
|
||||
not q.cache_is_hot(now_dt) or self.results[q][0] > q.cached_availability_state
|
||||
or q.cached_availability_paid_orders is None
|
||||
)
|
||||
if rewrite_cache:
|
||||
q.cached_availability_state = self.results[q][0]
|
||||
q.cached_availability_number = self.results[q][1]
|
||||
q.cached_availability_time = now_dt
|
||||
if q in self.count_paid_orders:
|
||||
q.cached_availability_paid_orders = self.count_paid_orders[q]
|
||||
update.append(q)
|
||||
if update:
|
||||
Quota.objects.using('default').bulk_update(update, [
|
||||
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
|
||||
'cached_availability_paid_orders'
|
||||
], batch_size=50)
|
||||
|
||||
def _close(self, quotas):
|
||||
for q in quotas:
|
||||
if self.results[q][0] <= Quota.AVAILABILITY_ORDERED and q.close_when_sold_out and not q.closed:
|
||||
q.closed = True
|
||||
q.save(update_fields=['closed'])
|
||||
q.log_action('pretix.event.quota.closed')
|
||||
|
||||
def _compute(self, quotas, now_dt):
|
||||
# Quotas we want to look at now
|
||||
self.sizes.update({q: q.size for q in quotas})
|
||||
|
||||
# Some helpful caches
|
||||
self._quota_objects.update({q.pk: q for q in quotas})
|
||||
|
||||
# Compute result for closed or unlimited
|
||||
self._compute_early_outs(quotas)
|
||||
|
||||
if self._early_out:
|
||||
if not self._full_results:
|
||||
quotas = [q for q in quotas if q not in self.results]
|
||||
if not quotas:
|
||||
return
|
||||
|
||||
size_left = Counter({q: (sys.maxsize if s is None else s) for q, s in self.sizes.items()})
|
||||
for q in quotas:
|
||||
self.count_paid_orders[q] = 0
|
||||
self.count_pending_orders[q] = 0
|
||||
self.count_cart[q] = 0
|
||||
self.count_vouchers[q] = 0
|
||||
self.count_waitinglist[q] = 0
|
||||
|
||||
# Fetch which quotas belong to which items and variations
|
||||
q_items = Quota.items.through.objects.filter(
|
||||
quota_id__in=[q.pk for q in quotas]
|
||||
).values('quota_id', 'item_id')
|
||||
for m in q_items:
|
||||
self._item_to_quotas[m['item_id']].append(self._quota_objects[m['quota_id']])
|
||||
|
||||
q_vars = Quota.variations.through.objects.filter(
|
||||
quota_id__in=[q.pk for q in quotas]
|
||||
).values('quota_id', 'itemvariation_id')
|
||||
for m in q_vars:
|
||||
self._var_to_quotas[m['itemvariation_id']].append(self._quota_objects[m['quota_id']])
|
||||
|
||||
self._compute_orders(quotas, q_items, q_vars, size_left)
|
||||
|
||||
if not self._full_results:
|
||||
quotas = [q for q in quotas if q not in self.results]
|
||||
if not quotas:
|
||||
return
|
||||
|
||||
self._compute_vouchers(quotas, q_items, q_vars, size_left, now_dt)
|
||||
|
||||
if not self._full_results:
|
||||
quotas = [q for q in quotas if q not in self.results]
|
||||
if not quotas:
|
||||
return
|
||||
|
||||
self._compute_carts(quotas, q_items, q_vars, size_left, now_dt)
|
||||
|
||||
if self._count_waitinglist:
|
||||
if not self._full_results:
|
||||
quotas = [q for q in quotas if q not in self.results]
|
||||
if not quotas:
|
||||
return
|
||||
|
||||
self._compute_waitinglist(quotas, q_items, q_vars, size_left)
|
||||
|
||||
for q in quotas:
|
||||
if q not in self.results:
|
||||
if size_left[q] > 0:
|
||||
self.results[q] = Quota.AVAILABILITY_OK, size_left[q]
|
||||
else:
|
||||
raise ValueError("inconclusive quota")
|
||||
|
||||
def _compute_orders(self, quotas, q_items, q_vars, size_left):
|
||||
events = {q.event_id for q in quotas}
|
||||
subevents = {q.subevent_id for q in quotas}
|
||||
seq = Q(subevent_id__in=subevents)
|
||||
if None in subevents:
|
||||
seq |= Q(subevent__isnull=True)
|
||||
op_lookup = OrderPosition.objects.filter(
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
|
||||
order__event_id__in=events,
|
||||
).filter(seq).filter(
|
||||
Q(
|
||||
Q(variation_id__isnull=True) &
|
||||
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
|
||||
) | Q(
|
||||
variation_id__in={i['itemvariation_id'] for i in q_vars if self._quota_objects[i['quota_id']] in quotas})
|
||||
).order_by().values('order__status', 'item_id', 'subevent_id', 'variation_id').annotate(c=Count('*'))
|
||||
for line in sorted(op_lookup, key=lambda li: li['order__status'], reverse=True): # p before n
|
||||
if line['variation_id']:
|
||||
qs = self._var_to_quotas[line['variation_id']]
|
||||
else:
|
||||
qs = self._item_to_quotas[line['item_id']]
|
||||
for q in qs:
|
||||
if q.subevent_id == line['subevent_id']:
|
||||
size_left[q] -= line['c']
|
||||
if line['order__status'] == Order.STATUS_PAID:
|
||||
self.count_paid_orders[q] += line['c']
|
||||
q.cached_availability_paid_orders = line['c']
|
||||
elif line['order__status'] == Order.STATUS_PENDING:
|
||||
self.count_pending_orders[q] += line['c']
|
||||
if size_left[q] <= 0 and q not in self.results:
|
||||
if line['order__status'] == Order.STATUS_PAID:
|
||||
self.results[q] = Quota.AVAILABILITY_GONE, 0
|
||||
else:
|
||||
self.results[q] = Quota.AVAILABILITY_ORDERED, 0
|
||||
|
||||
def _compute_vouchers(self, quotas, q_items, q_vars, size_left, now_dt):
|
||||
events = {q.event_id for q in quotas}
|
||||
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
|
||||
func = 'MAX'
|
||||
else: # NOQA
|
||||
func = 'GREATEST'
|
||||
|
||||
subevents = {q.subevent_id for q in quotas}
|
||||
seq = Q(subevent_id__in=subevents)
|
||||
if None in subevents:
|
||||
seq |= Q(subevent__isnull=True)
|
||||
v_lookup = Voucher.objects.filter(
|
||||
Q(event_id__in=events) &
|
||||
seq &
|
||||
Q(block_quota=True) &
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
|
||||
Q(
|
||||
Q(
|
||||
Q(variation_id__isnull=True) &
|
||||
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
|
||||
) | Q(
|
||||
variation_id__in={i['itemvariation_id'] for i in q_vars if
|
||||
self._quota_objects[i['quota_id']] in quotas}
|
||||
) | Q(
|
||||
quota_id__in=[q.pk for q in quotas]
|
||||
)
|
||||
)
|
||||
).order_by().values('subevent_id', 'item_id', 'quota_id', 'variation_id').annotate(
|
||||
free=Sum(Func(F('max_usages') - F('redeemed'), 0, function=func))
|
||||
)
|
||||
for line in v_lookup:
|
||||
if line['variation_id']:
|
||||
qs = self._var_to_quotas[line['variation_id']]
|
||||
elif line['item_id']:
|
||||
qs = self._item_to_quotas[line['item_id']]
|
||||
else:
|
||||
qs = [self._quota_objects[line['quota_id']]]
|
||||
for q in qs:
|
||||
if q.subevent_id == line['subevent_id']:
|
||||
size_left[q] -= line['free']
|
||||
self.count_vouchers[q] += line['free']
|
||||
if q not in self.results and size_left[q] <= 0:
|
||||
self.results[q] = Quota.AVAILABILITY_ORDERED, 0
|
||||
|
||||
def _compute_carts(self, quotas, q_items, q_vars, size_left, now_dt):
|
||||
events = {q.event_id for q in quotas}
|
||||
subevents = {q.subevent_id for q in quotas}
|
||||
seq = Q(subevent_id__in=subevents)
|
||||
if None in subevents:
|
||||
seq |= Q(subevent__isnull=True)
|
||||
cart_lookup = CartPosition.objects.filter(
|
||||
Q(event_id__in=events) &
|
||||
seq &
|
||||
Q(expires__gte=now_dt) &
|
||||
Q(
|
||||
Q(voucher__isnull=True)
|
||||
| Q(voucher__block_quota=False)
|
||||
| Q(voucher__valid_until__lt=now_dt)
|
||||
) &
|
||||
Q(
|
||||
Q(
|
||||
Q(variation_id__isnull=True) &
|
||||
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
|
||||
) | Q(
|
||||
variation_id__in={i['itemvariation_id'] for i in q_vars if self._quota_objects[i['quota_id']] in quotas}
|
||||
)
|
||||
)
|
||||
).order_by().values('item_id', 'subevent_id', 'variation_id').annotate(c=Count('*'))
|
||||
for line in cart_lookup:
|
||||
if line['variation_id']:
|
||||
qs = self._var_to_quotas[line['variation_id']]
|
||||
else:
|
||||
qs = self._item_to_quotas[line['item_id']]
|
||||
for q in qs:
|
||||
if q.subevent_id == line['subevent_id']:
|
||||
size_left[q] -= line['c']
|
||||
self.count_cart[q] += line['c']
|
||||
if q not in self.results and size_left[q] <= 0:
|
||||
self.results[q] = Quota.AVAILABILITY_RESERVED, 0
|
||||
|
||||
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
|
||||
events = {q.event_id for q in quotas}
|
||||
subevents = {q.subevent_id for q in quotas}
|
||||
seq = Q(subevent_id__in=subevents)
|
||||
if None in subevents:
|
||||
seq |= Q(subevent__isnull=True)
|
||||
w_lookup = WaitingListEntry.objects.filter(
|
||||
Q(event_id__in=events) &
|
||||
Q(voucher__isnull=True) &
|
||||
seq &
|
||||
Q(
|
||||
Q(
|
||||
Q(variation_id__isnull=True) &
|
||||
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
|
||||
) | Q(variation_id__in={i['itemvariation_id'] for i in q_vars if
|
||||
self._quota_objects[i['quota_id']] in quotas})
|
||||
)
|
||||
).order_by().values('item_id', 'subevent_id', 'variation_id').annotate(c=Count('*'))
|
||||
for line in w_lookup:
|
||||
if line['variation_id']:
|
||||
qs = self._var_to_quotas[line['variation_id']]
|
||||
else:
|
||||
qs = self._item_to_quotas[line['item_id']]
|
||||
for q in qs:
|
||||
if q.subevent_id == line['subevent_id']:
|
||||
size_left[q] -= line['c']
|
||||
self.count_waitinglist[q] += line['c']
|
||||
if q not in self.results and size_left[q] <= 0:
|
||||
self.results[q] = Quota.AVAILABILITY_ORDERED, 0
|
||||
|
||||
def _compute_early_outs(self, quotas):
|
||||
for q in quotas:
|
||||
if q.closed and not self._ignore_closed:
|
||||
self.results[q] = Quota.AVAILABILITY_ORDERED, 0
|
||||
elif q.size is None:
|
||||
self.results[q] = Quota.AVAILABILITY_OK, None
|
||||
elif q.size == 0:
|
||||
self.results[q] = Quota.AVAILABILITY_GONE, 0
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
|
||||
Reference in New Issue
Block a user