mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Display a timeline on the dashboard (#1290)
* Timeline data model * Display timeline * … * More events * Plugin support * Fix docs typo
This commit is contained in:
@@ -515,3 +515,12 @@ dictionaries as values that contain keys like in the following example::
|
||||
The evaluate member will be called with the order position, order and event as arguments. The event might
|
||||
also be a subevent, if applicable.
|
||||
"""
|
||||
|
||||
|
||||
timeline_events = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to collect events for the time line shown on event dashboards. You are passed
|
||||
a ``subevent`` argument which might be none and you are expected to return a list of instances of
|
||||
``pretix.base.timeline.TimelineEvent``, which is a ``namedtuple`` with the fields ``event``, ``subevent``,
|
||||
``datetime``, ``description`` and ``edit_url``.
|
||||
"""
|
||||
|
||||
189
src/pretix/base/timeline.py
Normal file
189
src/pretix/base/timeline.py
Normal file
@@ -0,0 +1,189 @@
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import pgettext_lazy
|
||||
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.signals import timeline_events
|
||||
|
||||
TimelineEvent = namedtuple('TimelineEvent', ('event', 'subevent', 'datetime', 'description', 'edit_url'))
|
||||
|
||||
|
||||
def timeline_for_event(event, subevent=None):
|
||||
tl = []
|
||||
ev = subevent or event
|
||||
if subevent:
|
||||
ev_edit_url = reverse(
|
||||
'control:event.subevent', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk
|
||||
}
|
||||
)
|
||||
else:
|
||||
ev_edit_url = reverse(
|
||||
'control:event.settings', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
}
|
||||
)
|
||||
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.date_from,
|
||||
description=pgettext_lazy('timeline', 'Your event starts'),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
if ev.date_to:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.date_to,
|
||||
description=pgettext_lazy('timeline', 'Your event ends'),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
if ev.date_admission:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.date_admission,
|
||||
description=pgettext_lazy('timeline', 'Admissions for your event start'),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
if ev.presale_start:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.presale_start,
|
||||
description=pgettext_lazy('timeline', 'Start of ticket sales'),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
if ev.presale_end:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.presale_end,
|
||||
description=pgettext_lazy('timeline', 'End of ticket sales'),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||
if rd:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=rd.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'Customers can no longer modify their orders'),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
rd = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
if rd:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=rd.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'No more payments can be completed'),
|
||||
edit_url=reverse('control:event.settings.payment', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
))
|
||||
|
||||
rd = event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
|
||||
if rd and event.settings.ticket_download:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=rd.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'Tickets can be downloaded'),
|
||||
edit_url=reverse('control:event.settings.tickets', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
))
|
||||
|
||||
rd = event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper)
|
||||
if rd and event.settings.cancel_allow_user:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=rd.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'Customers can no longer cancel free or unpaid orders'),
|
||||
edit_url=reverse('control:event.settings.tickets', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
))
|
||||
|
||||
rd = event.settings.get('cancel_allow_user_paid_until', as_type=RelativeDateWrapper)
|
||||
if rd and event.settings.cancel_allow_user_paid:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=rd.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'Customers can no longer cancel paid orders'),
|
||||
edit_url=reverse('control:event.settings.tickets', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
))
|
||||
|
||||
if not event.has_subevents:
|
||||
days = event.settings.get('mail_days_download_reminder', as_type=int)
|
||||
if days is not None:
|
||||
reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=reminder_date,
|
||||
description=pgettext_lazy('timeline', 'Download reminders are being sent out'),
|
||||
edit_url=reverse('control:event.settings.mail', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
))
|
||||
|
||||
for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
|
||||
if p.available_from:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=p.available_from,
|
||||
description=pgettext_lazy('timeline', 'Product "{name}" becomes available').format(name=str(p)),
|
||||
edit_url=reverse('control:event.item', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'item': p.pk,
|
||||
})
|
||||
))
|
||||
if p.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=p.available_until,
|
||||
description=pgettext_lazy('timeline', 'Product "{name}" becomes unavailable').format(name=str(p)),
|
||||
edit_url=reverse('control:event.item', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'item': p.pk,
|
||||
})
|
||||
))
|
||||
|
||||
pprovs = event.get_payment_providers()
|
||||
# This is a special case, depending on payment providers not overriding BasePaymentProvider by too much, but it's
|
||||
# preferrable to having all plugins implement this spearately.
|
||||
for pprov in pprovs.values():
|
||||
availability_date = pprov.settings.get('_availability_date', as_type=RelativeDateWrapper)
|
||||
if availability_date:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=availability_date.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'Payment provider "{name}" can no longer be selected').format(
|
||||
name=str(pprov.verbose_name)
|
||||
),
|
||||
edit_url=reverse('control:event.settings.payment.provider', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'provider': pprov.identifier,
|
||||
})
|
||||
))
|
||||
|
||||
for recv, resp in timeline_events.send(sender=event, subevent=subevent):
|
||||
tl += resp
|
||||
|
||||
return sorted(tl, key=lambda e: e.datetime)
|
||||
@@ -0,0 +1,36 @@
|
||||
{% load i18n %}
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Your timeline" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body timeline">
|
||||
{% regroup timeline by date as tl_list %}
|
||||
{% for day in tl_list %}
|
||||
<div class="row {% if day.grouper < today %}text-muted{% endif %}">
|
||||
<div class="col-date">
|
||||
<strong>{{ day.grouper|date:"SHORT_DATE_FORMAT" }}</strong>
|
||||
</div>
|
||||
<div class="col-event">
|
||||
{% for e in day.list %}
|
||||
<strong class="">{{ e.time|date:"TIME_FORMAT" }}</strong>
|
||||
|
||||
<span class="{% if e.time < nearly_now %}text-muted{% endif %}">
|
||||
{{ e.entry.description }}
|
||||
</span>
|
||||
{% if e.entry.edit_url %}
|
||||
|
||||
<a href="{{ e.entry.edit_url }}" class="text-muted">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if forloop.revcounter > 1 %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,6 +90,9 @@
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not request.event.has_subevents or subevent %}
|
||||
{% include "pretixcontrol/event/fragment_timeline.html" %}
|
||||
{% endif %}
|
||||
<div class="dashboard">
|
||||
{% for w in widgets %}
|
||||
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytz
|
||||
@@ -22,6 +23,7 @@ from pretix.base.models import (
|
||||
WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.timeline import timeline_for_event
|
||||
from pretix.control.forms.event import CommentForm
|
||||
from pretix.control.signals import (
|
||||
event_dashboard_widgets, user_dashboard_widgets,
|
||||
@@ -279,6 +281,7 @@ def event_index(request, organizer, event):
|
||||
ctx = {
|
||||
'widgets': rearrange(widgets),
|
||||
'logs': qs[:5],
|
||||
'subevent': subevent,
|
||||
'actions': a_qs[:5] if can_change_orders else [],
|
||||
'comment_form': CommentForm(initial={'comment': request.event.comment})
|
||||
}
|
||||
@@ -302,7 +305,19 @@ def event_index(request, organizer, event):
|
||||
for a in ctx['actions']:
|
||||
a.display = a.display(request)
|
||||
|
||||
return render(request, 'pretixcontrol/event/index.html', ctx)
|
||||
ctx['timeline'] = [
|
||||
{
|
||||
'date': t.datetime.astimezone(request.event.timezone).date(),
|
||||
'entry': t,
|
||||
'time': t.datetime.astimezone(request.event.timezone)
|
||||
}
|
||||
for t in timeline_for_event(request.event, subevent)
|
||||
]
|
||||
ctx['today'] = now().astimezone(request.event.timezone).date()
|
||||
ctx['nearly_now'] = now().astimezone(request.event.timezone) - timedelta(seconds=20)
|
||||
resp = render(request, 'pretixcontrol/event/index.html', ctx)
|
||||
# resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
|
||||
return resp
|
||||
|
||||
|
||||
def annotated_event_query(request):
|
||||
|
||||
@@ -618,3 +618,30 @@ h1 .label {
|
||||
border-radius: $border-radius-small;
|
||||
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
|
||||
}
|
||||
|
||||
.timeline {
|
||||
.col-date {
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
padding-left: 15px;
|
||||
width: 120px;
|
||||
float: left;
|
||||
}
|
||||
.col-event {
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
padding-right: 15px;
|
||||
margin-left: 120px;
|
||||
}
|
||||
}
|
||||
@media(max-width: $screen-sm-max) {
|
||||
.timeline {
|
||||
.col-date {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
.col-event {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user