forked from CGM_Public/pretix_original
Event dashboard with a flat design and plugin hooks
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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=[]
|
||||||
|
)
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
107
src/pretix/control/views/event_dashboard.py
Normal file
107
src/pretix/control/views/event_dashboard.py
Normal 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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user