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 %} +
+ {% if w.url %} + + {{ w.content|safe }} + + {% else %} +
+ {{ w.content|safe }}
-
- - - + {% 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; + } +}