diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py
index c9ec4ec964..2d4158b87e 100644
--- a/src/pretix/base/signals.py
+++ b/src/pretix/base/signals.py
@@ -5,6 +5,8 @@ from typing import Any, Callable, List, Tuple
from .models import Event
+CORE_MODULES = {("pretix", "base"), ("pretix", "presale"), ("pretix", "control")}
+
class EventPluginSignal(django.dispatch.Signal):
"""
@@ -40,7 +42,7 @@ class EventPluginSignal(django.dispatch.Signal):
searchpath, mod = searchpath.rsplit(".", 1)
# Only fire receivers from active plugins and core modules
- if (searchpath, mod) == ("pretix", "base") or (app and app.name in sender.get_plugins()):
+ if (searchpath, mod) in CORE_MODULES or (app and app.name in sender.get_plugins()):
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
diff --git a/src/pretix/control/__init__.py b/src/pretix/control/__init__.py
index d53dcf208e..c8139479c1 100644
--- a/src/pretix/control/__init__.py
+++ b/src/pretix/control/__init__.py
@@ -5,4 +5,7 @@ class PretixControlConfig(AppConfig):
name = 'pretix.control'
label = 'pretixcontrol'
+ def ready(self):
+ from .views import event_dashboard # noqa
+
default_app_config = 'pretix.control.PretixControlConfig'
diff --git a/src/pretix/control/signals.py b/src/pretix/control/signals.py
index a322ac24f3..c0aefd88ba 100644
--- a/src/pretix/control/signals.py
+++ b/src/pretix/control/signals.py
@@ -21,3 +21,16 @@ This signal is sent out to include navigation items in the event admin
nav_event = EventPluginSignal(
providing_args=["request"]
)
+
+"""
+This signal is sent out to include widgets to the event dashboard. Receivers
+should return a list of dictionaries, where each dictionary can have the keys:
+* content (str, containing HTML)
+* minimal width (int, widget width in 1/12ths of the page, default ist 3, can be
+ ignored on small displays)
+* priority (int, used for ordering, higher comes first, default is 1)
+* link (str, optional, if the full widget should be a link)
+"""
+event_dashboard_widgets = EventPluginSignal(
+ providing_args=[]
+)
diff --git a/src/pretix/control/templates/pretixcontrol/event/index.html b/src/pretix/control/templates/pretixcontrol/event/index.html
index b1e40e8e06..64bd1059fe 100644
--- a/src/pretix/control/templates/pretixcontrol/event/index.html
+++ b/src/pretix/control/templates/pretixcontrol/event/index.html
@@ -2,95 +2,20 @@
{% load i18n %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
-
{{ request.event.name }}
-
-
-
-
-
-
-
-
-
-
{{ tickets_sold }}
-
{% trans "Tickets sold" %}
-
+
{{ request.event.name }}
+
+ {% for w in widgets %}
+
-
-
-
+ {% endif %}
-
-
-
-
-
-
-
-
-
-
{{ tickets_total }}
-
{% trans "Total items ordered" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ tickets_revenue }}
-
{% trans "Total Revenue" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ products_active }}
-
{% trans "Active Products" %}
-
-
-
-
-
-
-
-
+ {% endfor %}
{% endblock %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index dfb2e49191..40c3e61809 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -1,7 +1,8 @@
from django.conf.urls import include, url
from pretix.control.views import (
- auth, event, item, main, orders, organizer, user, vouchers,
+ auth, event, event_dashboard, item, main, orders, organizer, user,
+ vouchers,
)
urlpatterns = [
@@ -19,7 +20,7 @@ urlpatterns = [
url(r'^events/add$', main.EventCreateStart.as_view(), name='events.add'),
url(r'^event/(?P
[^/]+)/add', main.EventCreate.as_view(), name='events.create'),
url(r'^event/(?P[^/]+)/(?P[^/]+)/', include([
- url(r'^$', event.index, name='event.index'),
+ url(r'^$', event_dashboard.index, name='event.index'),
url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'),
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'),
url(r'^settings/permissions$', event.EventPermissions.as_view(), name='event.settings.permissions'),
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py
index d305b4a2f0..caa124f03b 100644
--- a/src/pretix/control/views/event.py
+++ b/src/pretix/control/views/event.py
@@ -4,9 +4,8 @@ from django import forms
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db import transaction
-from django.db.models import Sum
from django.forms import modelformset_factory
-from django.shortcuts import redirect, render
+from django.shortcuts import redirect
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView
@@ -14,9 +13,7 @@ from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin
from pretix.base.forms import I18nModelForm
-from pretix.base.models import (
- Event, EventPermission, Item, Order, OrderPosition, User,
-)
+from pretix.base.models import Event, EventPermission, User
from pretix.base.signals import (
register_payment_providers, register_ticket_outputs,
)
@@ -334,29 +331,6 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
return providers
-def index(request, organizer, event):
- ctx = {
- 'products_active': Item.objects.filter(
- event=request.event,
- active=True,
- ).count(),
- 'tickets_total': OrderPosition.objects.filter(
- order__event=request.event,
- item__admission=True
- ).count(),
- 'tickets_revenue': Order.objects.filter(
- event=request.event,
- status=Order.STATUS_PAID,
- ).aggregate(sum=Sum('total'))['sum'],
- 'tickets_sold': OrderPosition.objects.filter(
- order__event=request.event,
- order__status=Order.STATUS_PAID,
- item__admission=True
- ).count()
- }
- return render(request, 'pretixcontrol/event/index.html', ctx)
-
-
class EventPermissionForm(I18nModelForm):
class Meta:
model = EventPermission
diff --git a/src/pretix/control/views/event_dashboard.py b/src/pretix/control/views/event_dashboard.py
new file mode 100644
index 0000000000..29f01bed87
--- /dev/null
+++ b/src/pretix/control/views/event_dashboard.py
@@ -0,0 +1,107 @@
+from decimal import Decimal
+
+from django.core.urlresolvers import reverse
+from django.db.models import Sum
+from django.dispatch import receiver
+from django.shortcuts import render
+from django.utils import formats
+from django.utils.translation import ugettext_lazy as _
+
+from pretix.base.models import Item, Order, OrderPosition
+from pretix.control.signals import event_dashboard_widgets
+
+NUM_WIDGET = '{num}{text}
'
+
+
+@receiver(signal=event_dashboard_widgets)
+def base_widgets(sender, **kwargs):
+ prodc = Item.objects.filter(
+ event=sender, active=True,
+ ).count()
+
+ tickc = OrderPosition.objects.filter(
+ order__event=sender, item__admission=True
+ ).count()
+
+ paidc = OrderPosition.objects.filter(
+ order__event=sender, item__admission=True,
+ order__status=Order.STATUS_PAID,
+ ).count()
+
+ rev = Order.objects.filter(
+ event=sender,
+ status=Order.STATUS_PAID
+ ).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')
+
+ return [
+ {
+ 'content': NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')),
+ 'width': 3,
+ 'priority': 100,
+ 'url': reverse('control:event.orders', kwargs={
+ 'event': sender.slug,
+ 'organizer': sender.organizer.slug
+ })
+ },
+ {
+ 'content': NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
+ 'width': 3,
+ 'priority': 100,
+ 'url': reverse('control:event.orders.overview', kwargs={
+ 'event': sender.slug,
+ 'organizer': sender.organizer.slug
+ })
+ },
+ {
+ 'content': NUM_WIDGET.format(
+ num=formats.localize(rev), text=_('Total revenue ({currency})').format(currency=sender.currency)),
+ 'width': 3,
+ 'priority': 100,
+ 'url': reverse('control:event.orders.overview', kwargs={
+ 'event': sender.slug,
+ 'organizer': sender.organizer.slug
+ })
+ },
+ {
+ 'content': NUM_WIDGET.format(num=prodc, text=_('Active products')),
+ 'width': 3,
+ 'priority': 100,
+ 'url': reverse('control:event.items', kwargs={
+ 'event': sender.slug,
+ 'organizer': sender.organizer.slug
+ })
+ },
+ ]
+
+
+def index(request, organizer, event):
+ widgets = []
+ for r, result in event_dashboard_widgets.send(sender=request.event):
+ widgets.extend(result)
+ ctx = {
+ 'widgets': rearrange(widgets),
+ }
+ return render(request, 'pretixcontrol/event/index.html', ctx)
+
+
+def rearrange(widgets: list):
+ """
+ Small and stupid algorithm to arrange widget boxes without too many gaps while respecting
+ priority. Doing this siginificantly better might be *really* hard.
+ """
+ oldlist = sorted(widgets, key=lambda w: -1 * w.get('priority', 1))
+ newlist = []
+ cpos = 0
+ while len(oldlist) > 0:
+ max_prio = max([w.get('priority', 1) for w in oldlist])
+ try:
+ best = max([w for w in oldlist if w.get('priority', 1) == max_prio and cpos + w.get('width', 3) <= 12],
+ key=lambda w: w.get('width', 3))
+ cpos = (cpos + best.get('width', 3)) % 12
+ except ValueError: # max() arg is an empty sequence
+ best = [w for w in oldlist if w.get('priority', 1) == max_prio][0]
+ cpos = best.get('width', 3)
+ oldlist.remove(best)
+ newlist.append(best)
+
+ return newlist
diff --git a/src/static/pretixcontrol/less/main.less b/src/static/pretixcontrol/less/main.less
index edc6d5506f..0de809bff5 100644
--- a/src/static/pretixcontrol/less/main.less
+++ b/src/static/pretixcontrol/less/main.less
@@ -85,3 +85,30 @@ nav.navbar {
margin-left: 0;
}
}
+
+.dashboard > div {
+ padding: 5px;
+}
+.dashboard .widget {
+ min-height: 160px;
+ background: #F8F8F8;
+ display: block;
+ position: relative;
+}
+.dashboard .widget:hover,.dashboard .widget:focus {
+ background: #EEEEEE;
+ text-decoration: none;
+}
+.dashboard .numwidget {
+ .num {
+ display: block;
+ padding: 28px 0 10px;
+ text-align: center;
+ font-size: 40px;
+ }
+ .text {
+ display: block;
+ text-align: center;
+ font-size: 20px;
+ }
+}