diff --git a/src/pretix/base/__init__.py b/src/pretix/base/__init__.py index 2b6a18c8d6..47f341563a 100644 --- a/src/pretix/base/__init__.py +++ b/src/pretix/base/__init__.py @@ -11,7 +11,7 @@ class PretixBaseConfig(AppConfig): from . import payment # NOQA from . import exporters # NOQA from . import invoice # NOQA - from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check # NOQA + from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas # NOQA try: from .celery_app import app as celery_app # NOQA diff --git a/src/pretix/base/migrations/0078_auto_20171003_1650.py b/src/pretix/base/migrations/0078_auto_20171003_1650.py new file mode 100644 index 0000000000..2ec82f9103 --- /dev/null +++ b/src/pretix/base/migrations/0078_auto_20171003_1650.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2017-10-03 16:50 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0077_auto_20170829_1126'), + ] + + operations = [ + migrations.AddField( + model_name='quota', + name='cached_availability_number', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='quota', + name='cached_availability_state', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='quota', + name='cached_availability_time', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='eventmetaproperty', + name='default', + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name='taxrule', + name='eu_reverse_charge', + field=models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be qualified for reverse charge since the place of taxation is the location of the event. This option disables charging VAT for all customers outside the EU and for business customers in different EU countries who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax calculation. USE AT YOUR OWN RISK.', verbose_name='Use EU reverse charge taxation rules'), + ), + ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 9cf51036d4..874f002a30 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -704,6 +704,9 @@ class Quota(LoggedModel): blank=True, verbose_name=_("Variations") ) + cached_availability_state = models.PositiveIntegerField(null=True, blank=True) + cached_availability_number = models.PositiveIntegerField(null=True, blank=True) + cached_availability_time = models.DateTimeField(null=True, blank=True) class Meta: verbose_name = _("Quota") @@ -718,11 +721,18 @@ class Quota(LoggedModel): self.event.get_cache().clear() def save(self, *args, **kwargs): + clear_cache = kwargs.pop('clear_cache', True) super().save(*args, **kwargs) - if self.event: + if self.event and clear_cache: self.event.get_cache().clear() - def availability(self, now_dt: datetime=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]: + def cache_is_hot(self, now_dt=None): + now_dt = now_dt or now() + return self.cached_availability_time and (now_dt - self.cached_availability_time).total_seconds() < 120 + + def availability( + self, now_dt: datetime=None, count_waitinglist=True, _cache=None, allow_cache=False + ) -> Tuple[int, int]: """ This method is used to determine whether Items or ItemVariations belonging to this quota should currently be available for sale. @@ -730,12 +740,26 @@ 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. """ + if allow_cache and self.cache_is_hot() and count_waitinglist: + return self.cached_availability_state, self.cached_availability_number + if _cache and count_waitinglist is not _cache.get('_count_waitinglist', True): _cache.clear() 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) + + if count_waitinglist and not self.cache_is_hot(now_dt): + self.cached_availability_state = res[0] + self.cached_availability_number = res[1] + self.cached_availability_time = now_dt + self.save( + update_fields=['cached_availability_state', 'cached_availability_number', 'cached_availability_time'], + clear_cache=False + ) + if _cache is not None: _cache[self.pk] = res _cache['_count_waitinglist'] = count_waitinglist diff --git a/src/pretix/base/services/quotas.py b/src/pretix/base/services/quotas.py new file mode 100644 index 0000000000..cec48fd012 --- /dev/null +++ b/src/pretix/base/services/quotas.py @@ -0,0 +1,34 @@ +from datetime import timedelta + +from django.db import models +from django.db.models import F, Max, OuterRef, Q, Subquery +from django.dispatch import receiver + +from pretix.base.models import LogEntry, Quota +from pretix.celery_app import app + +from ..signals import periodic_task + + +@receiver(signal=periodic_task) +def build_all_quota_caches(sender, **kwargs): + refresh_quota_cashes.apply_async() + + +@app.task +def refresh_quota_cashes(): + last_activity = LogEntry.objects.filter( + event=OuterRef('event_id'), + ).order_by().values('event').annotate( + m=Max('datetime') + ).values( + 'm' + ) + quotas = Quota.objects.annotate( + last_activity=Subquery(last_activity, output_field=models.DateTimeField()) + ).filter( + Q(cached_availability_time__isnull=True) | + Q(cached_availability_time__lt=F('last_activity') - timedelta(hours=1)) + ) + for q in quotas: + q.availability() diff --git a/src/pretix/control/views/dashboards.py b/src/pretix/control/views/dashboards.py index dfb9dbecb8..675d2f3b02 100644 --- a/src/pretix/control/views/dashboards.py +++ b/src/pretix/control/views/dashboards.py @@ -152,7 +152,7 @@ def quota_widgets(sender, subevent=None, **kwargs): widgets = [] for q in sender.quotas.filter(subevent=subevent): - status, left = q.availability() + status, left = q.availability(allow_cache=True) widgets.append({ 'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e', text=_('{quota} left').format(quota=escape(q.name))),