Event dashboard with a flat design and plugin hooks

This commit is contained in:
Raphael Michel
2016-02-22 16:14:01 +01:00
parent 0fd519df4d
commit 4f35a16787
8 changed files with 171 additions and 119 deletions

View File

@@ -5,6 +5,8 @@ from typing import Any, Callable, List, Tuple
from .models import Event from .models import Event
CORE_MODULES = {("pretix", "base"), ("pretix", "presale"), ("pretix", "control")}
class EventPluginSignal(django.dispatch.Signal): class EventPluginSignal(django.dispatch.Signal):
""" """
@@ -40,7 +42,7 @@ class EventPluginSignal(django.dispatch.Signal):
searchpath, mod = searchpath.rsplit(".", 1) searchpath, mod = searchpath.rsplit(".", 1)
# Only fire receivers from active plugins and core modules # 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: if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
response = receiver(signal=self, sender=sender, **named) response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response)) responses.append((receiver, response))

View File

@@ -5,4 +5,7 @@ class PretixControlConfig(AppConfig):
name = 'pretix.control' name = 'pretix.control'
label = 'pretixcontrol' label = 'pretixcontrol'
def ready(self):
from .views import event_dashboard # noqa
default_app_config = 'pretix.control.PretixControlConfig' default_app_config = 'pretix.control.PretixControlConfig'

View File

@@ -21,3 +21,16 @@ This signal is sent out to include navigation items in the event admin
nav_event = EventPluginSignal( nav_event = EventPluginSignal(
providing_args=["request"] 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=[]
)

View File

@@ -2,95 +2,20 @@
{% load i18n %} {% load i18n %}
{% block title %}{{ request.event.name }}{% endblock %} {% block title %}{{ request.event.name }}{% endblock %}
{% block content %} {% block content %}
<h1>{{ request.event.name }}</h1> <h1>{{ request.event.name }}</h1>
<div class="row dashboard-panels"> <div class="row dashboard">
<div class="col-lg-3 col-md-6"> {% for w in widgets %}
<div class="panel panel-green"> <div class="col-xs-12 col-sm-{% if w.width > 6 %}12{% else %}6{% endif %} col-md-{{ w.width }}">
<div class="panel-heading"> {% if w.url %}
<div class="row"> <a href="{{ w.url }}" class="widget">
<div class="col-xs-3"> {{ w.content|safe }}
<i class="fa fa-users fa-5x"></i> </a>
</div> {% else %}
<div class="col-xs-9 text-right"> <div class="widget">
<div class="huge">{{ tickets_sold }}</div> {{ w.content|safe }}
<div>{% trans "Tickets sold" %}</div>
</div>
</div> </div>
</div> {% endif %}
<a href="{% url "control:event.orders.overview" organizer=request.organizer.slug event=request.event.slug %}">
<div class="panel-footer">
<span class="pull-left">{% trans "Orders overview" %}</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div> </div>
</div> {% endfor %}
<div class="col-lg-3 col-md-6">
<div class="panel panel-primary">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-shopping-cart fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge">{{ tickets_total }}</div>
<div>{% trans "Total items ordered" %}</div>
</div>
</div>
</div>
<a href="{% url "control:event.orders" organizer=request.organizer.slug event=request.event.slug %}">
<div class="panel-footer">
<span class="pull-left">{% trans "View all orders" %}</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="panel panel-green">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-money fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge">{{ tickets_revenue }}</div>
<div>{% trans "Total Revenue" %}</div>
</div>
</div>
</div>
<a href="{% url "control:event.orders.overview" organizer=request.organizer.slug event=request.event.slug %}">
<div class="panel-footer">
<span class="pull-left">{% trans "Orders overview" %}</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="panel panel-primary">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-folder fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge">{{ products_active }}</div>
<div>{% trans "Active Products" %}</div>
</div>
</div>
</div>
<a href="{% url "control:event.items" organizer=request.organizer.slug event=request.event.slug %}">
<div class="panel-footer">
<span class="pull-left">{% trans "View details" %}</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,7 +1,8 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from pretix.control.views import ( from pretix.control.views import (
auth, event, item, main, orders, organizer, user, vouchers, auth, event, event_dashboard, item, main, orders, organizer, user,
vouchers,
) )
urlpatterns = [ urlpatterns = [
@@ -19,7 +20,7 @@ urlpatterns = [
url(r'^events/add$', main.EventCreateStart.as_view(), name='events.add'), url(r'^events/add$', main.EventCreateStart.as_view(), name='events.add'),
url(r'^event/(?P<organizer>[^/]+)/add', main.EventCreate.as_view(), name='events.create'), url(r'^event/(?P<organizer>[^/]+)/add', main.EventCreate.as_view(), name='events.create'),
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([ url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', 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/$', event.EventUpdate.as_view(), name='event.settings'),
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'), url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'),
url(r'^settings/permissions$', event.EventPermissions.as_view(), name='event.settings.permissions'), url(r'^settings/permissions$', event.EventPermissions.as_view(), name='event.settings.permissions'),

View File

@@ -4,9 +4,8 @@ from django import forms
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.db.models import Sum
from django.forms import modelformset_factory 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.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView 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 django.views.generic.detail import SingleObjectMixin
from pretix.base.forms import I18nModelForm from pretix.base.forms import I18nModelForm
from pretix.base.models import ( from pretix.base.models import Event, EventPermission, User
Event, EventPermission, Item, Order, OrderPosition, User,
)
from pretix.base.signals import ( from pretix.base.signals import (
register_payment_providers, register_ticket_outputs, register_payment_providers, register_ticket_outputs,
) )
@@ -334,29 +331,6 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
return providers 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 EventPermissionForm(I18nModelForm):
class Meta: class Meta:
model = EventPermission model = EventPermission

View File

@@ -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 = '<div class="numwidget"><span class="num">{num}</span><span class="text">{text}</span></div>'
@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

View File

@@ -85,3 +85,30 @@ nav.navbar {
margin-left: 0; 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;
}
}