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 @@
+
{{ 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;
+ }
+}
|