Lazy-load dashboard widgets

This commit is contained in:
Raphael Michel
2019-08-12 12:19:02 +02:00
parent 9bdb715874
commit 5363f4206e
6 changed files with 139 additions and 67 deletions

View File

@@ -47,6 +47,7 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/orderchange.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/orderchange.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/dashboard.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/tabs.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/tabs.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>

View File

@@ -95,18 +95,30 @@
{% endif %} {% endif %}
<div class="dashboard"> <div class="dashboard">
{% for w in widgets %} {% for w in widgets %}
<div class="widget-container widget-{{ w.display_size|default:"small" }}"> <div class="widget-container widget-{{ w.display_size|default:"small" }} {% if w.lazy %}widget-lazy-loading{% endif %}" data-lazy-id="{{ w.lazy }}">
{% if w.url %}{# backwards compatibility #} {% if w.url %}{# backwards compatibility #}
<a href="{{ w.url }}" class="widget"> <a href="{{ w.url }}" class="widget">
{{ w.content|safe }} {% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</a> </a>
{% elif w.link %} {% elif w.link %}
<a href="{{ w.link }}" class="widget"> <a href="{{ w.link }}" class="widget">
{{ w.content|safe }} {% if w.lazy %}
<span class="fa fa-cog fa-4x´"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</a> </a>
{% else %} {% else %}
<div class="widget"> <div class="widget">
{{ w.content|safe }} {% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -106,6 +106,7 @@ urlpatterns = [
url(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'), url(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'),
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([ url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
url(r'^$', dashboards.event_index, name='event.index'), url(r'^$', dashboards.event_index, name='event.index'),
url(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
url(r'^live/$', event.EventLive.as_view(), name='event.live'), url(r'^live/$', event.EventLive.as_view(), name='event.live'),
url(r'^logs/$', event.EventLog.as_view(), name='event.log'), url(r'^logs/$', event.EventLog.as_view(), name='event.log'),
url(r'^delete/$', event.EventDelete.as_view(), name='event.delete'), url(r'^delete/$', event.EventDelete.as_view(), name='event.delete'),

View File

@@ -8,6 +8,7 @@ from django.db.models import (
) )
from django.db.models.functions import Coalesce, Greatest from django.db.models.functions import Coalesce, Greatest
from django.dispatch import receiver from django.dispatch import receiver
from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse from django.urls import reverse
@@ -36,44 +37,46 @@ NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="
@receiver(signal=event_dashboard_widgets) @receiver(signal=event_dashboard_widgets)
def base_widgets(sender, subevent=None, **kwargs): def base_widgets(sender, subevent=None, lazy=False, **kwargs):
prodc = Item.objects.filter( if not lazy:
event=sender, active=True, prodc = Item.objects.filter(
).filter( event=sender, active=True,
(Q(available_until__isnull=True) | Q(available_until__gte=now())) & ).filter(
(Q(available_from__isnull=True) | Q(available_from__lte=now())) (Q(available_until__isnull=True) | Q(available_until__gte=now())) &
).count() (Q(available_from__isnull=True) | Q(available_from__lte=now()))
).count()
if subevent: if subevent:
opqs = OrderPosition.objects.filter(subevent=subevent) opqs = OrderPosition.objects.filter(subevent=subevent)
else: else:
opqs = OrderPosition.objects opqs = OrderPosition.objects
tickc = opqs.filter( tickc = opqs.filter(
order__event=sender, item__admission=True, order__event=sender, item__admission=True,
order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING), order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
).count() ).count()
paidc = opqs.filter( paidc = opqs.filter(
order__event=sender, item__admission=True, order__event=sender, item__admission=True,
order__status=Order.STATUS_PAID, order__status=Order.STATUS_PAID,
).count() ).count()
if subevent: if subevent:
rev = opqs.filter( rev = opqs.filter(
order__event=sender, order__status=Order.STATUS_PAID order__event=sender, order__status=Order.STATUS_PAID
).aggregate( ).aggregate(
sum=Sum('price') sum=Sum('price')
)['sum'] or Decimal('0.00') )['sum'] or Decimal('0.00')
else: else:
rev = Order.objects.filter( rev = Order.objects.filter(
event=sender, event=sender,
status=Order.STATUS_PAID status=Order.STATUS_PAID
).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00') ).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')
return [ return [
{ {
'content': NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')), 'content': None if lazy else NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')),
'lazy': 'attendees-ordered',
'display_size': 'small', 'display_size': 'small',
'priority': 100, 'priority': 100,
'url': reverse('control:event.orders', kwargs={ 'url': reverse('control:event.orders', kwargs={
@@ -82,7 +85,8 @@ def base_widgets(sender, subevent=None, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '') }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
}, },
{ {
'content': NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')), 'content': None if lazy else NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
'lazy': 'attendees-paid',
'display_size': 'small', 'display_size': 'small',
'priority': 100, 'priority': 100,
'url': reverse('control:event.orders.overview', kwargs={ 'url': reverse('control:event.orders.overview', kwargs={
@@ -91,8 +95,9 @@ def base_widgets(sender, subevent=None, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '') }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
}, },
{ {
'content': NUM_WIDGET.format( 'content': None if lazy else NUM_WIDGET.format(
num=formats.localize(round_decimal(rev, sender.currency)), text=_('Total revenue ({currency})').format(currency=sender.currency)), num=formats.localize(round_decimal(rev, sender.currency)), text=_('Total revenue ({currency})').format(currency=sender.currency)),
'lazy': 'total-revenue',
'display_size': 'small', 'display_size': 'small',
'priority': 100, 'priority': 100,
'url': reverse('control:event.orders.overview', kwargs={ 'url': reverse('control:event.orders.overview', kwargs={
@@ -101,7 +106,8 @@ def base_widgets(sender, subevent=None, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '') }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
}, },
{ {
'content': NUM_WIDGET.format(num=prodc, text=_('Active products')), 'content': None if lazy else NUM_WIDGET.format(num=prodc, text=_('Active products')),
'lazy': 'active-products',
'display_size': 'small', 'display_size': 'small',
'priority': 100, 'priority': 100,
'url': reverse('control:event.items', kwargs={ 'url': reverse('control:event.items', kwargs={
@@ -113,32 +119,36 @@ def base_widgets(sender, subevent=None, **kwargs):
@receiver(signal=event_dashboard_widgets) @receiver(signal=event_dashboard_widgets)
def waitinglist_widgets(sender, subevent=None, **kwargs): def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
widgets = [] widgets = []
wles = WaitingListEntry.objects.filter(event=sender, subevent=subevent, voucher__isnull=True) wles = WaitingListEntry.objects.filter(event=sender, subevent=subevent, voucher__isnull=True)
if wles.count(): if wles.count():
quota_cache = {} if not lazy:
itemvar_cache = {} quota_cache = {}
happy = 0 itemvar_cache = {}
happy = 0
for wle in wles: for wle in wles:
if (wle.item, wle.variation) not in itemvar_cache: if (wle.item, wle.variation) not in itemvar_cache:
itemvar_cache[(wle.item, wle.variation)] = ( itemvar_cache[(wle.item, wle.variation)] = (
wle.variation.check_quotas(subevent=wle.subevent, count_waitinglist=False, _cache=quota_cache) wle.variation.check_quotas(subevent=wle.subevent, count_waitinglist=False, _cache=quota_cache)
if wle.variation if wle.variation
else wle.item.check_quotas(subevent=wle.subevent, count_waitinglist=False, _cache=quota_cache) else wle.item.check_quotas(subevent=wle.subevent, count_waitinglist=False, _cache=quota_cache)
) )
row = itemvar_cache.get((wle.item, wle.variation)) row = itemvar_cache.get((wle.item, wle.variation))
if row[1] is None: if row[1] is None:
itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1]) itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1])
happy += 1 happy += 1
elif row[1] > 0: elif row[1] > 0:
itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1] - 1) itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1] - 1)
happy += 1 happy += 1
widgets.append({ widgets.append({
'content': NUM_WIDGET.format(num=str(happy), text=_('available to give to people on waiting list')), 'content': None if lazy else NUM_WIDGET.format(
num=str(happy), text=_('available to give to people on waiting list')
),
'lazy': 'waitinglist',
'priority': 50, 'priority': 50,
'url': reverse('control:event.orders.waitinglist', kwargs={ 'url': reverse('control:event.orders.waitinglist', kwargs={
'event': sender.slug, 'event': sender.slug,
@@ -146,7 +156,8 @@ def waitinglist_widgets(sender, subevent=None, **kwargs):
}) })
}) })
widgets.append({ widgets.append({
'content': NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')), 'content': None if lazy else NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')),
'lazy': lazy,
'display_size': 'small', 'display_size': 'small',
'priority': 50, 'priority': 50,
'url': reverse('control:event.orders.waitinglist', kwargs={ 'url': reverse('control:event.orders.waitinglist', kwargs={
@@ -159,14 +170,18 @@ def waitinglist_widgets(sender, subevent=None, **kwargs):
@receiver(signal=event_dashboard_widgets) @receiver(signal=event_dashboard_widgets)
def quota_widgets(sender, subevent=None, **kwargs): def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
widgets = [] widgets = []
for q in sender.quotas.filter(subevent=subevent): for q in sender.quotas.filter(subevent=subevent):
status, left = q.availability(allow_cache=True) if not lazy:
status, left = q.availability(allow_cache=True)
widgets.append({ widgets.append({
'content': NUM_WIDGET.format(num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e', 'content': None if lazy else NUM_WIDGET.format(
text=_('{quota} left').format(quota=escape(q.name))), num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
text=_('{quota} left').format(quota=escape(q.name))
),
'lazy': 'quota-{}'.format(q.pk),
'display_size': 'small', 'display_size': 'small',
'priority': 50, 'priority': 50,
'url': reverse('control:event.items.quotas.show', kwargs={ 'url': reverse('control:event.items.quotas.show', kwargs={
@@ -209,14 +224,18 @@ def shop_state_widget(sender, **kwargs):
@receiver(signal=event_dashboard_widgets) @receiver(signal=event_dashboard_widgets)
def checkin_widget(sender, subevent=None, **kwargs): def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
widgets = [] widgets = []
qs = sender.checkin_lists.filter(subevent=subevent) qs = sender.checkin_lists.filter(subevent=subevent)
qs = CheckinList.annotate_with_numbers(qs, sender) if not lazy:
qs = CheckinList.annotate_with_numbers(qs, sender)
for cl in qs: for cl in qs:
widgets.append({ widgets.append({
'content': NUM_WIDGET.format(num='{}/{}'.format(cl.checkin_count, cl.position_count), 'content': None if lazy else NUM_WIDGET.format(
text=_('Checked in {list}').format(list=escape(cl.name))), num='{}/{}'.format(cl.checkin_count, cl.position_count),
text=_('Checked in {list}').format(list=escape(cl.name))
),
'lazy': 'checkin-{}'.format(cl.pk),
'display_size': 'small', 'display_size': 'small',
'priority': 50, 'priority': 50,
'url': reverse('control:event.orders.checkinlists.show', kwargs={ 'url': reverse('control:event.orders.checkinlists.show', kwargs={
@@ -263,7 +282,7 @@ def event_index(request, organizer, event):
pass pass
widgets = [] widgets = []
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent): for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=True):
widgets.extend(result) widgets.extend(result)
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders', can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
@@ -320,6 +339,22 @@ def event_index(request, organizer, event):
return resp return resp
def event_index_widgets_lazy(request, organizer, event):
subevent = None
if request.GET.get("subevent", "") != "" and request.event.has_subevents:
i = request.GET.get("subevent", "")
try:
subevent = request.event.subevents.get(pk=i)
except SubEvent.DoesNotExist:
pass
widgets = []
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
widgets.extend(result)
return JsonResponse({'widgets': widgets})
def annotated_event_query(request): def annotated_event_query(request):
active_orders = Order.objects.filter( active_orders = Order.objects.filter(
event=OuterRef('pk'), event=OuterRef('pk'),

View File

@@ -0,0 +1,13 @@
/*global $,gettext*/
$(function () {
if ($("div[data-lazy-id]").length == 0) {
return;
}
$.getJSON("widgets.json", function (data) {
$.each(data.widgets, function (k, v) {
$("[data-lazy-id=" + v.lazy + "]").removeClass("widget-lazy-loading");
$("[data-lazy-id=" + v.lazy + "] .widget").html(v.content);
});
});
});

View File

@@ -27,6 +27,16 @@
width: 25%; width: 25%;
} }
.dashboard .widget-container.widget-lazy-loading {
text-align: center;
.fa-cog {
color: #ccc;
margin-top: 30px;
-webkit-animation: fa-spin 4s infinite linear;
animation: fa-spin 4s infinite linear;
}
}
.dashboard-panels .panel-heading .fa { .dashboard-panels .panel-heading .fa {
opacity: 0.5; opacity: 0.5;
} }