diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 874f002a30..94f45020e3 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -726,6 +726,12 @@ class Quota(LoggedModel): if self.event and clear_cache: self.event.get_cache().clear() + def rebuild_cache(self, now_dt=None): + self.cached_availability_time = None + self.cached_availability_number = None + self.cached_availability_state = None + self.availability(now_dt=now_dt) + 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 diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index e052c6890b..a997ba8a19 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -197,7 +197,8 @@ class OrderSearchFilterForm(OrderFilterForm): class SubEventFilterForm(FilterForm): orders = { 'date_from': 'date_from', - 'active': 'active' + 'active': 'active', + 'sum_quota_available': 'sum_quota_available' } status = forms.ChoiceField( label=_('Status'), @@ -258,7 +259,8 @@ class EventFilterForm(FilterForm): 'organizer': 'organizer__name', 'date_from': 'order_from', 'date_to': 'order_to', - 'live': 'live' + 'live': 'live', + 'sum_quota_available': 'sum_quota_available' } status = forms.ChoiceField( label=_('Status'), diff --git a/src/pretix/control/templates/pretixcontrol/events/index.html b/src/pretix/control/templates/pretixcontrol/events/index.html index afb2ad3021..0b1923c3e1 100644 --- a/src/pretix/control/templates/pretixcontrol/events/index.html +++ b/src/pretix/control/templates/pretixcontrol/events/index.html @@ -67,6 +67,11 @@ + + {% trans "Quota available" %} + + + {% trans "Status" %} @@ -79,9 +84,6 @@ {{ e.name }} - {% if e.has_subevents %} - {% trans "Series" %} - {% endif %}
{{ e.slug }} {% if not hide_orga %}{{ e.organizer }}{% endif %} @@ -91,6 +93,9 @@ {% else %} {{ e.get_short_date_from_display }} {% endif %} + {% if e.has_subevents %} + {% trans "Series" %} + {% endif %} {% if e.settings.show_date_to and e.date_to %}
– {% if e.has_subevents %} @@ -100,6 +105,18 @@ {% endif %} {% endif %} + + {% for q in e.first_quotas|slice:":3" %} + {% include "pretixcontrol/fragment_quota_box.html" with quota=q %} + {% endfor %} + {% if e.first_quotas|length > 3 %} + + ··· + + {% endif %} + {% if not e.live %} {% trans "Shop disabled" %} diff --git a/src/pretix/control/templates/pretixcontrol/fragment_quota_box.html b/src/pretix/control/templates/pretixcontrol/fragment_quota_box.html new file mode 100644 index 0000000000..8779015fa9 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/fragment_quota_box.html @@ -0,0 +1,18 @@ +{% load i18n %} +
{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}"> + {% if q.size|default_if_none:"NONE" == "NONE" %} +
+
+
+
+ {% else %} +
+
+
+
+ {% endif %} +
+ {{ q.cached_avail.1|default_if_none:"∞" }} / {{ q.size|default_if_none:"∞" }} +
+
diff --git a/src/pretix/control/templates/pretixcontrol/subevents/index.html b/src/pretix/control/templates/pretixcontrol/subevents/index.html index a7f8f4e825..f99b1f1c0c 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/index.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/index.html @@ -51,6 +51,11 @@ + + {% trans "Quota available" %} + + + {% trans "Status" %} @@ -67,6 +72,18 @@ {{ s.name }} {{ s.get_date_from_display }} + + {% for q in s.first_quotas|slice:":3" %} + {% include "pretixcontrol/fragment_quota_box.html" with quota=q %} + {% endfor %} + {% if s.first_quotas|length > 3 %} + + ··· + + {% endif %} + {% if not s.active %} {% trans "Disabled" %} diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 9f44154994..7baab61c8c 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -698,6 +698,7 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView): 'id': form.instance.pk } ) + form.instance.rebuild_cache() return super().form_valid(form) def get_success_url(self) -> str: diff --git a/src/pretix/control/views/main.py b/src/pretix/control/views/main.py index 953c4b3878..3665cb0343 100644 --- a/src/pretix/control/views/main.py +++ b/src/pretix/control/views/main.py @@ -2,7 +2,9 @@ from django.conf import settings from django.contrib import messages from django.core.urlresolvers import reverse from django.db import transaction -from django.db.models import Max, Min +from django.db.models import ( + F, IntegerField, Max, Min, OuterRef, Prefetch, Subquery, Sum, +) from django.db.models.functions import Coalesce, Greatest from django.http import JsonResponse from django.shortcuts import redirect @@ -14,7 +16,7 @@ from django.views.generic import ListView from formtools.wizard.views import SessionWizardView from i18nfield.strings import LazyI18nString -from pretix.base.models import Event, Organizer, Team +from pretix.base.models import Event, Organizer, Quota, Team from pretix.control.forms.event import ( EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm, ) @@ -43,6 +45,22 @@ class EventList(ListView): order_to=Coalesce('max_fromto', 'max_to', 'max_from', 'date_to'), ) + sum_quota_available = Quota.objects.filter( + event=OuterRef('pk'), subevent__isnull=True + ).order_by().values('event').annotate( + s=Sum('cached_availability_number') + ).values( + 's' + ) + + qs = qs.annotate( + sum_quota_available=Subquery(sum_quota_available, output_field=IntegerField()) + ).prefetch_related( + Prefetch('quotas', + queryset=Quota.objects.filter(subevent__isnull=True).annotate(s=Coalesce(F('size'), 0)).order_by('-s'), + to_attr='first_quotas') + ) + if self.filter_form.is_valid(): qs = self.filter_form.filter_qs(qs) return qs @@ -54,6 +72,18 @@ class EventList(ListView): pk__in=self.request.user.teams.values_list('organizer', flat=True) ).count() ctx['hide_orga'] = orga_c <= 1 + + for s in ctx['events']: + s.first_quotas = s.first_quotas[:4] + for q in s.first_quotas: + q.cached_avail = ( + (q.cached_availability_state, q.cached_availability_number) + if q.cached_availability_time is not None + else q.availability(allow_cache=True) + ) + if q.cached_avail[1] is not None: + q.percent = round(q.cached_avail[1] / q.size * 100) if q.size > 0 else 0 + q.inv_percent = 100 - q.percent return ctx @cached_property diff --git a/src/pretix/control/views/subevents.py b/src/pretix/control/views/subevents.py index 98be1a37d6..60652240f6 100644 --- a/src/pretix/control/views/subevents.py +++ b/src/pretix/control/views/subevents.py @@ -3,6 +3,8 @@ import copy from django.contrib import messages from django.core.urlresolvers import reverse from django.db import transaction +from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum +from django.db.models.functions import Coalesce from django.forms import inlineformset_factory from django.http import Http404, HttpResponseRedirect from django.utils.functional import cached_property @@ -29,7 +31,21 @@ class SubEventList(EventPermissionRequiredMixin, ListView): permission = 'can_change_settings' def get_queryset(self): - qs = self.request.event.subevents.all() + sum_quota_available = Quota.objects.filter( + subevent=OuterRef('pk') + ).order_by().values('subevent').annotate( + s=Sum('cached_availability_number') + ).values( + 's' + ) + + qs = self.request.event.subevents.annotate( + sum_quota_available=Subquery(sum_quota_available, output_field=IntegerField()) + ).prefetch_related( + Prefetch('quotas', + queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'), + to_attr='first_quotas') + ) if self.filter_form.is_valid(): qs = self.filter_form.filter_qs(qs) return qs @@ -37,6 +53,17 @@ class SubEventList(EventPermissionRequiredMixin, ListView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['filter_form'] = self.filter_form + for s in ctx['subevents']: + s.first_quotas = s.first_quotas[:4] + for q in s.first_quotas: + q.cached_avail = ( + (q.cached_availability_state, q.cached_availability_number) + if q.cached_availability_time is not None + else q.availability(allow_cache=True) + ) + if q.cached_avail[1] is not None: + q.percent = round(q.cached_avail[1] / q.size * 100) if q.size > 0 else 0 + q.inv_percent = 100 - q.percent return ctx @cached_property diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index b9621952ee..75ef827edc 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -88,6 +88,9 @@ $(function () { }); $('[data-toggle="tooltip"]').tooltip(); + $('[data-toggle="tooltip_html"]').tooltip({ + 'html': true + }); var url = document.location.toString(); if (url.match('#')) { diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss index f4e7f3deec..d60ac4798a 100644 --- a/src/pretix/static/pretixcontrol/scss/main.scss +++ b/src/pretix/static/pretixcontrol/scss/main.scss @@ -403,3 +403,32 @@ body.loading #wrapper { .event-name-col { width: 30%; } + +.quotabox { + display: inline-block; + vertical-align: top; + width: 50px; + .progress { + height: 7px; + margin-bottom: 2px; + } + .numbers { + font-size: 10px; + color: $text-muted; + display: block; + text-align: center; + } + .progress-bar-success { + background: lighten($brand-success, 20%); + } +} +.quotabox-more { + font-weight: bold; + display: inline-block; + vertical-align: top; + line-height: 20px; + margin-top: -6px; + &:hover { + text-decoration: none; + } +}