diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst
index 75bef4474f..568b618194 100644
--- a/doc/development/api/general.rst
+++ b/doc/development/api/general.rst
@@ -83,7 +83,7 @@ Dashboards
.. automodule:: pretix.control.signals
:no-index:
- :members: event_dashboard_widgets, event_dashboard_widgets_override, user_dashboard_widgets, event_dashboard_top
+ :members: event_dashboard_statistics, user_dashboard_widgets, event_dashboard_top
Ticket designs
""""""""""""""
diff --git a/src/pretix/control/signals.py b/src/pretix/control/signals.py
index 183542675b..870a9239e8 100644
--- a/src/pretix/control/signals.py
+++ b/src/pretix/control/signals.py
@@ -133,47 +133,14 @@ This signal is sent out to include custom HTML in the top part of the the event
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
-An additional keyword argument ``subevent`` *can* contain a sub-event.
"""
-event_dashboard_widgets = EventPluginSignal()
+event_dashboard_statistics = EventPluginSignal()
"""
-This signal is sent out to include widgets in the event dashboard. Receivers
-should return a list of dictionaries, where each dictionary can have the keys:
-
-* content (SafeString, containing HTML)
-* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
- (quarter of a row). May be ignored on small displays, default is "small")
-* priority (int, used for ordering, higher comes first, default is 1)
-* url (str, optional, if the full widget should be a link)
+This signal is sent out to include statistical content on the event dashboard.
+Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
-An additional keyword argument ``subevent`` *can* contain a sub-event.
-
-The keyword argument ``widgets_override_active`` is set to ``True`` when a plugin
-has returned content from :py:data:`event_dashboard_widgets_override` and is taking
-over the KPI section of the dashboard. Receivers that produce widgets which would
-be redundant with such a replacement (e.g. the built-in attendees/revenue/products
-KPI tiles) should return an empty list in that case. Receivers that provide widgets
-unrelated to the KPI tiles (waitinglist, quotas, check-in lists, shop state, etc.)
-should ignore the flag and continue to return their widgets — those will render
-below the override block.
-"""
-
-event_dashboard_widgets_override = EventPluginSignal()
-"""
-This signal allows a plugin to take over the KPI section of the event dashboard
-with custom HTML. Receivers should return an HTML string (or an empty string /
-``None`` to opt out). If any receiver returns non-empty HTML, that HTML is
-rendered at the top of the dashboard, and receivers of
-:py:data:`event_dashboard_widgets` are informed via the
-``widgets_override_active=True`` keyword argument so they can suppress widgets
-that the override replaces. The remaining widgets (those that don't opt out)
-still render below the override block. Multiple receivers' return values are
-concatenated.
-
-As with all event plugin signals, the ``sender`` keyword argument will contain
-the event. An additional keyword argument ``subevent`` *can* contain a sub-event.
"""
user_dashboard_widgets = GlobalSignal()
diff --git a/src/pretix/control/templates/pretixcontrol/event/dashboard_partial_waiting.html b/src/pretix/control/templates/pretixcontrol/event/dashboard_partial_waiting.html
new file mode 100644
index 0000000000..93d7b0bb0a
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/event/dashboard_partial_waiting.html
@@ -0,0 +1,11 @@
+{% load i18n %}
+{% load humanize %}
+{% if count %}
+
+ {% blocktrans trimmed with number=count|intcomma count count=count %}
+ {{ number }} person waiting
+ {% plural %}
+ {{ number }} persons waiting
+ {% endblocktrans %}
+
+{% endif %}
\ No newline at end of file
diff --git a/src/pretix/control/templates/pretixcontrol/event/index.html b/src/pretix/control/templates/pretixcontrol/event/index.html
index 649467bafd..dd0cc12b11 100644
--- a/src/pretix/control/templates/pretixcontrol/event/index.html
+++ b/src/pretix/control/templates/pretixcontrol/event/index.html
@@ -71,16 +71,25 @@
hx-indicator="#warnings_indicator"
hx-trigger="load">
+ {% if stats %}
+ {{ stats }}
+ {% endif %}
+
+
{% if not request.event.items.exists %}
{% include "pretixcontrol/event/dashboard_partial_welcome.html" %}
- {% endif %}
-
- {% if not request.event.has_subevents %}
+ {% elif not request.event.has_subevents %}
{% include "pretixcontrol/event/fragment_timeline.html" %}
{% endif %}
- {% if widgets_override and 0 %}
- {{ widgets_override|safe }}
- {% endif %}
- {% if widgets %}
-
- {% for w in widgets %}
-
- {% endfor %}
-
- {% endif %}
-
{% if "event.orders:read" in request.eventpermset or "event.orders:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset or "event.items:write" in request.eventpermset %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index d41e11809b..79e2235aea 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -272,10 +272,10 @@ urlpatterns = [
re_path(r'^event/(?P
[^/]+)/(?P[^/]+)/', include([
re_path(r'^$', dashboards.event_index, name='event.index'),
re_path(r'^qrcode.(?P(png|jpeg|gif|svg))$', event.EventQRCode.as_view(), name='event.qrcode'),
- re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
re_path(r'^dashboard/partials/logs$', dashboards.event_index_log_lazy, name='event.index.logs'),
re_path(r'^dashboard/partials/warnings$', dashboards.event_index_warnings_lazy, name='event.index.warnings'),
re_path(r'^dashboard/partials/quotas$', dashboards.event_index_quotas_lazy, name='event.index.quotas'),
+ re_path(r'^dashboard/partials/waiting$', dashboards.event_index_waiting_lazy, name='event.index.waiting'),
re_path(r'^dashboard/partials/checkin$', dashboards.event_index_checkin_lazy, name='event.index.checkin'),
re_path(r'^dashboard/partials/comment$', event.EventComment.as_view(), name='event.index.comment'),
re_path(r'^live/$', event.EventLive.as_view(), name='event.live'),
diff --git a/src/pretix/control/views/dashboards.py b/src/pretix/control/views/dashboards.py
index f3b9796327..d8411ea79d 100644
--- a/src/pretix/control/views/dashboards.py
+++ b/src/pretix/control/views/dashboards.py
@@ -33,192 +33,48 @@
# License for the specific language governing permissions and limitations under the License.
from datetime import timedelta
-from decimal import Decimal
from zoneinfo import ZoneInfo
-from django.conf import settings
from django.contrib.contenttypes.models import ContentType
-from django.contrib.humanize.templatetags.humanize import intcomma
from django.db.models import (
- Count, IntegerField, Max, Min, OuterRef, Prefetch, Q, Subquery, Sum,
+ Count, IntegerField, Max, Min, OuterRef, Q, Subquery,
)
from django.db.models.functions import Coalesce, Greatest
-from django.dispatch import receiver
from django.http import Http404, JsonResponse
from django.shortcuts import render
from django.urls import reverse
from django.utils.formats import date_format
-from django.utils.html import conditional_escape, escape, format_html
+from django.utils.html import (
+ conditional_escape, escape, format_html, format_html_join,
+)
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext, pgettext
-from pretix.base.decimal import round_decimal
from pretix.base.models import (
- Item, ItemCategory, ItemVariation, Order, OrderPosition, OrderRefund,
- Question, Quota, SubEvent, Voucher, WaitingListEntry,
+ Item, ItemCategory, Order, OrderRefund, Question, Quota, Voucher,
+ WaitingListEntry,
)
from pretix.base.timeline import timeline_for_event
from pretix.control.signals import (
- event_dashboard_widgets, event_dashboard_widgets_override,
- user_dashboard_widgets,
+ event_dashboard_statistics, user_dashboard_widgets,
)
from pretix.helpers.daterange import daterange
from ...base.models.orders import CancellationRequest
from ...base.models.organizer import TeamQuerySet
-from ...base.templatetags.money import money_filter
from ..logdisplay import OVERVIEW_BANLIST
from .utils import prepare_quotas_for_boxes
-NUM_WIDGET = '{num}{text}
'
-
-@receiver(signal=event_dashboard_widgets)
-def base_widgets(sender, subevent=None, lazy=False, widgets_override_active=False, **kwargs):
- if widgets_override_active:
- return []
- if not lazy:
- if subevent:
- opqs = OrderPosition.objects.filter(subevent=subevent)
- else:
- opqs = OrderPosition.objects
-
- tickc = opqs.filter(
- order__event=sender, item__admission=True,
- order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
- ).count()
-
- paidc = opqs.filter(
- order__event=sender, item__admission=True,
- order__status=Order.STATUS_PAID,
- ).count()
-
- if subevent:
- rev = opqs.filter(
- order__event=sender, order__status=Order.STATUS_PAID
- ).aggregate(
- sum=Sum('price')
- )['sum'] or Decimal('0.00')
- else:
- rev = Order.objects.filter(
- event=sender,
- status=Order.STATUS_PAID
- ).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')
-
- return [
+def event_index_waiting_lazy(request, organizer, event):
+ wles = WaitingListEntry.objects.filter(event=request.event, voucher__isnull=True)
+ return render(
+ request,
+ 'pretixcontrol/event/dashboard_partial_waiting.html',
{
- 'content': None if lazy else format_html(NUM_WIDGET, num=intcomma(tickc), text=_('Attendees (ordered)')),
- 'lazy': 'attendees-ordered',
- 'display_size': 'small',
- 'priority': 100,
- 'url': reverse('control:event.orders', kwargs={
- 'event': sender.slug,
- 'organizer': sender.organizer.slug
- }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
- },
- {
- 'content': None if lazy else format_html(NUM_WIDGET, num=intcomma(paidc), text=_('Attendees (paid)')),
- 'lazy': 'attendees-paid',
- 'display_size': 'small',
- 'priority': 100,
- 'url': reverse('control:event.orders.overview', kwargs={
- 'event': sender.slug,
- 'organizer': sender.organizer.slug
- }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
- },
- {
- 'content': None if lazy else format_html(
- NUM_WIDGET,
- num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True),
- text=_('Total revenue ({currency})').format(currency=sender.currency)
- ),
- 'lazy': 'total-revenue',
- 'display_size': 'small',
- 'priority': 100,
- 'url': reverse('control:event.orders.overview', kwargs={
- 'event': sender.slug,
- 'organizer': sender.organizer.slug
- }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
- },
- ]
-
-
-@receiver(signal=event_dashboard_widgets)
-def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
- widgets = []
-
- wles = WaitingListEntry.objects.filter(event=sender, subevent=subevent, voucher__isnull=True)
- if wles.exists():
- if not lazy:
- quota_cache = {}
- happy = 0
- tuples = wles.values('item', 'variation').order_by().annotate(cnt=Count('id'))
-
- items = {
- i.pk: i for i in sender.items.filter(id__in=[t['item'] for t in tuples]).prefetch_related(
- Prefetch('quotas',
- to_attr='_subevent_quotas',
- queryset=sender.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent)),
- )
- }
- vars = {
- i.pk: i for i in ItemVariation.objects.filter(
- item__event=sender, id__in=[t['variation'] for t in tuples if t['variation']]
- ).prefetch_related(
- Prefetch('quotas',
- to_attr='_subevent_quotas',
- queryset=sender.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent)),
- )
- }
-
- for wlt in tuples:
- item = items.get(wlt['item'])
- variation = vars.get(wlt['variation'])
- if not item:
- continue
- quotas = (
- variation._get_quotas(subevent=subevent)
- if variation
- else item._get_quotas(subevent=subevent)
- )
- row = (
- variation.check_quotas(subevent=subevent, count_waitinglist=False, _cache=quota_cache)
- if variation
- else item.check_quotas(subevent=subevent, count_waitinglist=False, _cache=quota_cache)
- )
- if row[1] is None:
- happy += wlt['cnt']
- elif row[1] > 0:
- happy += min(wlt['cnt'], row[1])
- for q in quotas:
- if q.size is not None:
- quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - min(wlt['cnt'], row[1]))
-
- widgets.append({
- 'content': None if lazy else format_html(
- NUM_WIDGET, num=intcomma(happy), text=_('available to give to people on waiting list')
- ),
- 'lazy': 'waitinglist-avail',
- 'priority': 50,
- 'url': reverse('control:event.orders.waitinglist', kwargs={
- 'event': sender.slug,
- 'organizer': sender.organizer.slug,
- })
- })
- widgets.append({
- 'content': None if lazy else format_html(
- NUM_WIDGET, num=intcomma(wles.count()), text=_('total waiting list length')
- ),
- 'lazy': 'waitinglist-length',
- 'display_size': 'small',
- 'priority': 50,
- 'url': reverse('control:event.orders.waitinglist', kwargs={
- 'event': sender.slug,
- 'organizer': sender.organizer.slug,
- })
- })
-
- return widgets
+ 'count': wles.count,
+ }
+ )
def build_json_response(widgets):
@@ -228,33 +84,20 @@ def build_json_response(widgets):
def event_index(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
+ can_view_orders = request.user.has_event_permission(
+ request.organizer,
+ request.event,
+ 'event.orders:read',
+ request=request
+ )
- can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'event.orders:read',
- request=request)
-
- widgets = []
- widgets_override = ''
+ stats = []
if can_view_orders:
- for r, result in event_dashboard_widgets_override.send(sender=request.event, subevent=subevent, request=request):
- if result:
- widgets_override += result
- for r, result in event_dashboard_widgets.send(
- sender=request.event, subevent=subevent, lazy=True,
- widgets_override_active=bool(widgets_override),
- ):
- widgets.extend(result)
+ for r, result in event_dashboard_statistics.send(sender=request.event, request=request):
+ stats.append(result)
ctx = {
- 'widgets': rearrange(widgets),
- 'widgets_override': widgets_override,
- 'subevent': subevent,
+ 'stats': format_html_join("", "{}", [(s,) for s in stats]),
}
if not request.event.has_subevents:
@@ -273,22 +116,6 @@ def event_index(request, organizer, event):
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 build_json_response(widgets)
-
-
def event_index_warnings_lazy(request, organizer, event):
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'event.orders:read',
request=request)
diff --git a/src/pretix/control/views/subevents.py b/src/pretix/control/views/subevents.py
index 12022c78cb..95e9ca06c9 100644
--- a/src/pretix/control/views/subevents.py
+++ b/src/pretix/control/views/subevents.py
@@ -564,6 +564,8 @@ class SubEventDetail(EventPermissionRequiredMixin, DetailView):
}
for t in timeline_for_event(self.request.event, self.object)
]
+ ctx['today'] = now().astimezone(self.request.event.timezone).date()
+ ctx['nearly_now'] = now().astimezone(self.request.event.timezone) - timedelta(seconds=20)
return super().get_context_data(
**kwargs,
diff --git a/src/pretix/plugins/statistics/signals.py b/src/pretix/plugins/statistics/signals.py
index 8c77d9ff63..80e778ecc9 100644
--- a/src/pretix/plugins/statistics/signals.py
+++ b/src/pretix/plugins/statistics/signals.py
@@ -19,12 +19,21 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
+from decimal import Decimal
+
+from django.contrib.humanize.templatetags.humanize import intcomma
+from django.db.models import Sum
from django.dispatch import receiver
+from django.template.loader import get_template
from django.urls import resolve, reverse
+from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
+from pretix.base.decimal import round_decimal
+from pretix.base.models import Order, OrderPosition
from pretix.base.signals import order_paid, order_placed
-from pretix.control.signals import nav_event
+from pretix.base.templatetags.money import money_filter
+from pretix.control.signals import event_dashboard_statistics, nav_event
@receiver(nav_event, dispatch_uid="statistics_nav")
@@ -58,3 +67,62 @@ def clear_cache(sender, *args, **kwargs):
order_placed.connect(clear_cache)
order_paid.connect(clear_cache)
+
+
+@receiver(event_dashboard_statistics, dispatch_uid="statistics_event_dashboard_state")
+def event_dashboard_stats_receivers(sender, request, **kwargs):
+ if len(event_dashboard_statistics.receivers) > 1:
+ # Only render this as a fallback if no other plugins exist
+ return ""
+
+ opqs = OrderPosition.objects
+ tickc = opqs.filter(
+ order__event=sender, item__admission=True,
+ order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
+ ).count()
+ paidc = opqs.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')
+ num_widget = '{num}{text}
'
+
+ widgets = [
+ {
+ 'content': format_html(num_widget, num=intcomma(tickc), text=_('Attendees (ordered)')),
+ 'display_size': 'small',
+ 'priority': 100,
+ 'url': reverse('control:event.orders', kwargs={
+ 'event': sender.slug,
+ 'organizer': sender.organizer.slug
+ })
+ },
+ {
+ 'content': format_html(num_widget, num=intcomma(paidc), text=_('Attendees (paid)')),
+ 'display_size': 'small',
+ 'priority': 100,
+ 'url': reverse('control:event.orders.overview', kwargs={
+ 'event': sender.slug,
+ 'organizer': sender.organizer.slug
+ })
+ },
+ {
+ 'content': format_html(
+ num_widget,
+ num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True),
+ text=_('Total revenue ({currency})').format(currency=sender.currency)
+ ),
+ 'display_size': 'small',
+ 'priority': 100,
+ 'url': reverse('control:event.orders.overview', kwargs={
+ 'event': sender.slug,
+ 'organizer': sender.organizer.slug
+ })
+ },
+ ]
+ template = get_template('pretixplugins/statistics/dashboard.html')
+ ctx = {'request': request, 'widgets': widgets}
+ return template.render(ctx)
diff --git a/src/pretix/plugins/statistics/templates/pretixplugins/statistics/dashboard.html b/src/pretix/plugins/statistics/templates/pretixplugins/statistics/dashboard.html
new file mode 100644
index 0000000000..9015d88694
--- /dev/null
+++ b/src/pretix/plugins/statistics/templates/pretixplugins/statistics/dashboard.html
@@ -0,0 +1,15 @@
+
+ {% for w in widgets %}
+
+ {% endfor %}
+
\ No newline at end of file
diff --git a/src/pretix/static/pretixcontrol/js/ui/dashboard.js b/src/pretix/static/pretixcontrol/js/ui/dashboard.js
index 993d833d79..ef1244dc7f 100644
--- a/src/pretix/static/pretixcontrol/js/ui/dashboard.js
+++ b/src/pretix/static/pretixcontrol/js/ui/dashboard.js
@@ -1,16 +1,5 @@
/* global $,gettext */
-$(function () {
- if ($('div[data-lazy-id]').length == 0) {
- return
- }
- $.getJSON('widgets.json' + ($('select[name=\'subevent\']').val() ? '?subevent=' + $('select[name=\'subevent\']').val() : ''), 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)
- })
- })
-})
$(function () {
$('.timeline').each(function () {
let $tl = $(this)
diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js
index 7ebefa0bbd..44e95f9a89 100644
--- a/src/pretix/static/pretixcontrol/js/ui/main.js
+++ b/src/pretix/static/pretixcontrol/js/ui/main.js
@@ -1,4 +1,4 @@
-/*global $, gettext, ngettext, interpolate */
+/*global $, gettext, ngettext, interpolate, htmx */
function formatPrice(price, currency, locale) {
if (!window.Intl || !Intl.NumberFormat) return price;
@@ -1120,3 +1120,9 @@ $(function () {
return $(this).find("button:not([type=button]), input[type=submit]").length > 0;
}).areYouSure( {'message': gettext('You have unsaved changes!')});
});
+
+htmx.on('htmx:afterSettle', (e) => {
+ console.log("afterSwap", e)
+ setup_basics($(e.detail.elt))
+ form_handlers($(e.detail.elt))
+})