mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
Add a registry for datasync providers and an associated sync queue, to be used by plugins that transfer data from pretix orders to external systems. Additionally, provide a generic data mapping interface to be used in settings pages of such plugins, to let users configure which information from pretix to fill into which data fields of the external system. --------- Co-authored-by: Raphael Michel <michel@rami.io>
694 lines
27 KiB
Python
694 lines
27 KiB
Python
#
|
||
# This file is part of pretix (Community Edition).
|
||
#
|
||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||
#
|
||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||
# this file, see <https://pretix.eu/about/en/license>.
|
||
#
|
||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||
# details.
|
||
#
|
||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||
# <https://www.gnu.org/licenses/>.
|
||
#
|
||
|
||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||
#
|
||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||
#
|
||
# This file contains Apache-licensed contributions copyrighted by: Sohalt, Tobias Kunze, jasonwaiting@live.hk
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||
# 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,
|
||
)
|
||
from django.db.models.functions import Coalesce, Greatest
|
||
from django.dispatch import receiver
|
||
from django.http import JsonResponse
|
||
from django.shortcuts import render
|
||
from django.template.loader import get_template
|
||
from django.urls import reverse
|
||
from django.utils.formats import date_format
|
||
from django.utils.html import escape
|
||
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,
|
||
)
|
||
from pretix.base.services.quotas import QuotaAvailability
|
||
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,
|
||
)
|
||
from pretix.helpers.daterange import daterange
|
||
|
||
from ...base.models.orders import CancellationRequest
|
||
from ...base.templatetags.money import money_filter
|
||
from ..logdisplay import OVERVIEW_BANLIST
|
||
|
||
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, subevent=None, lazy=False, **kwargs):
|
||
if not lazy:
|
||
prodc = Item.objects.filter(
|
||
event=sender, active=True,
|
||
).filter(
|
||
(Q(available_until__isnull=True) | Q(available_until__gte=now())) &
|
||
(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||
).count()
|
||
|
||
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 [
|
||
{
|
||
'content': None if lazy else NUM_WIDGET.format(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 NUM_WIDGET.format(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 NUM_WIDGET.format(
|
||
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 '')
|
||
},
|
||
{
|
||
'content': None if lazy else NUM_WIDGET.format(num=prodc, text=_('Active products')),
|
||
'lazy': 'active-products',
|
||
'display_size': 'small',
|
||
'priority': 100,
|
||
'url': reverse('control:event.items', kwargs={
|
||
'event': sender.slug,
|
||
'organizer': sender.organizer.slug
|
||
})
|
||
},
|
||
]
|
||
|
||
|
||
@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 NUM_WIDGET.format(
|
||
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 NUM_WIDGET.format(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
|
||
|
||
|
||
@receiver(signal=event_dashboard_widgets)
|
||
def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||
widgets = []
|
||
quotas = sender.quotas.filter(subevent=subevent)
|
||
|
||
qa = QuotaAvailability()
|
||
if quotas:
|
||
qa.queue(*quotas)
|
||
qa.compute(allow_cache=True)
|
||
|
||
for q in quotas:
|
||
if not lazy:
|
||
status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True)
|
||
widgets.append({
|
||
'content': None if lazy else NUM_WIDGET.format(
|
||
num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e',
|
||
text=_('{quota} left').format(quota=escape(q.name))
|
||
),
|
||
'lazy': 'quota-{}'.format(q.pk),
|
||
'display_size': 'small',
|
||
'priority': 50,
|
||
'url': reverse('control:event.items.quotas.show', kwargs={
|
||
'event': sender.slug,
|
||
'organizer': sender.organizer.slug,
|
||
'quota': q.id
|
||
})
|
||
})
|
||
return widgets
|
||
|
||
|
||
@receiver(signal=event_dashboard_widgets)
|
||
def shop_state_widget(sender, **kwargs):
|
||
return [{
|
||
'display_size': 'small',
|
||
'priority': 1000,
|
||
'content': '<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>'.format(
|
||
t1=_('Your ticket shop is'), t2=_('Click here to change'),
|
||
state=_('live') if sender.live and not sender.testmode else (
|
||
_('live and in test mode') if sender.live else (
|
||
_('not yet public') if not sender.testmode else (
|
||
_('in private test mode')
|
||
)
|
||
)
|
||
),
|
||
icon='fa-check-circle' if sender.live and not sender.testmode else (
|
||
'fa-warning' if sender.live else (
|
||
'fa-times-circle' if not sender.testmode else (
|
||
'fa-times-circle'
|
||
)
|
||
)
|
||
),
|
||
cls='live' if sender.live else 'off'
|
||
),
|
||
'url': reverse('control:event.live', kwargs={
|
||
'event': sender.slug,
|
||
'organizer': sender.organizer.slug
|
||
})
|
||
}]
|
||
|
||
|
||
@receiver(signal=event_dashboard_widgets)
|
||
def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
|
||
widgets = []
|
||
qs = sender.checkin_lists.filter(subevent=subevent)
|
||
for cl in qs:
|
||
widgets.append({
|
||
'content': None if lazy else NUM_WIDGET.format(
|
||
num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)),
|
||
text=_('Present – {list}').format(list=escape(cl.name))
|
||
),
|
||
'lazy': 'checkin-{}'.format(cl.pk),
|
||
'display_size': 'small',
|
||
'priority': 50,
|
||
'url': reverse('control:event.orders.checkinlists.show', kwargs={
|
||
'event': sender.slug,
|
||
'organizer': sender.organizer.slug,
|
||
'list': cl.pk
|
||
})
|
||
})
|
||
return widgets
|
||
|
||
|
||
@receiver(signal=event_dashboard_widgets)
|
||
def welcome_wizard_widget(sender, **kwargs):
|
||
template = get_template('pretixcontrol/event/dashboard_widget_welcome.html')
|
||
ctx = {
|
||
'title': _('Welcome to pretix!')
|
||
}
|
||
kwargs = {'event': sender.slug, 'organizer': sender.organizer.slug}
|
||
|
||
if not sender.items.exists():
|
||
ctx.update({
|
||
'subtitle': _('Get started with our setup tool'),
|
||
'text': _('To start selling tickets, you need to create products or quotas. The fastest way to create '
|
||
'this is to use our setup tool.'),
|
||
'button_text': _('Set up event'),
|
||
'button_url': reverse('control:event.quick', kwargs=kwargs)
|
||
})
|
||
else:
|
||
return []
|
||
return [{
|
||
'display_size': 'full',
|
||
'priority': 2000,
|
||
'content': template.render(ctx)
|
||
}]
|
||
|
||
|
||
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, 'can_view_orders',
|
||
request=request)
|
||
can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
|
||
'can_change_event_settings', request=request)
|
||
|
||
widgets = []
|
||
if can_view_orders:
|
||
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=True):
|
||
widgets.extend(result)
|
||
|
||
ctx = {
|
||
'widgets': rearrange(widgets),
|
||
'subevent': subevent,
|
||
'comment_form': CommentForm(initial={'comment': request.event.comment}, readonly=not can_change_event_settings),
|
||
}
|
||
|
||
ctx['has_overpaid_orders'] = can_view_orders and Order.annotate_overpayments(request.event.orders).filter(
|
||
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
|
||
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
|
||
).exists()
|
||
ctx['has_pending_orders_with_full_payment'] = can_view_orders and Order.annotate_overpayments(request.event.orders).filter(
|
||
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0) & Q(require_approval=False)
|
||
).exists()
|
||
ctx['has_pending_refunds'] = can_view_orders and OrderRefund.objects.filter(
|
||
order__event=request.event,
|
||
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_EXTERNAL)
|
||
).exists()
|
||
ctx['has_pending_approvals'] = can_view_orders and request.event.orders.filter(
|
||
status=Order.STATUS_PENDING,
|
||
require_approval=True
|
||
).exists()
|
||
ctx['has_cancellation_requests'] = can_view_orders and CancellationRequest.objects.filter(
|
||
order__event=request.event
|
||
).exists()
|
||
ctx['has_sync_problems'] = can_change_event_settings and request.event.queued_sync_jobs.filter(
|
||
Q(need_manual_retry__isnull=False)
|
||
| Q(failed_attempts__gt=0)
|
||
).exists()
|
||
|
||
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 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 JsonResponse({'widgets': widgets})
|
||
|
||
|
||
def event_index_log_lazy(request, organizer, event):
|
||
qs = request.event.logentry_set.all().select_related('user', 'content_type', 'api_token', 'oauth_application',
|
||
'device').order_by('-datetime')
|
||
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
||
|
||
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'can_view_orders',
|
||
request=request)
|
||
can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
|
||
'can_change_event_settings', request=request)
|
||
can_view_vouchers = request.user.has_event_permission(request.organizer, request.event, 'can_view_vouchers',
|
||
request=request)
|
||
|
||
if not can_view_orders:
|
||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
|
||
if not can_view_vouchers:
|
||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||
if not can_change_event_settings:
|
||
allowed_types = [
|
||
ContentType.objects.get_for_model(Voucher),
|
||
ContentType.objects.get_for_model(Order)
|
||
]
|
||
if request.user.has_event_permission(request.organizer, request.event, 'can_change_items', request=request):
|
||
allowed_types += [
|
||
ContentType.objects.get_for_model(Item),
|
||
ContentType.objects.get_for_model(ItemCategory),
|
||
ContentType.objects.get_for_model(Quota),
|
||
ContentType.objects.get_for_model(Question),
|
||
]
|
||
qs = qs.filter(content_type__in=allowed_types)
|
||
|
||
return render(
|
||
request,
|
||
'pretixcontrol/event/logs_embed.html',
|
||
{
|
||
'logs': qs[:5]
|
||
}
|
||
)
|
||
|
||
|
||
def annotated_event_query(request, lazy=False):
|
||
active_orders = Order.objects.filter(
|
||
event=OuterRef('pk'),
|
||
status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
|
||
).order_by().values('event').annotate(
|
||
c=Count('*')
|
||
).values(
|
||
'c'
|
||
)
|
||
|
||
qs = request.user.get_events_with_any_permission(request)
|
||
if not lazy:
|
||
qs = qs.annotate(
|
||
order_count=Subquery(active_orders, output_field=IntegerField()),
|
||
)
|
||
qs = qs.annotate(
|
||
min_from=Min('subevents__date_from'),
|
||
max_from=Max('subevents__date_from'),
|
||
max_to=Max('subevents__date_to'),
|
||
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from')),
|
||
).annotate(
|
||
order_to=Coalesce('max_fromto', 'max_to', 'max_from', 'date_to', 'date_from'),
|
||
)
|
||
return qs
|
||
|
||
|
||
def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
|
||
widgets = []
|
||
|
||
# Get set of events where we have the permission to show the # of orders
|
||
if not lazy:
|
||
events_with_orders = set(qs.filter(
|
||
Q(organizer_id__in=user.teams.filter(all_events=True, can_view_orders=True).values_list('organizer', flat=True))
|
||
| Q(id__in=user.teams.filter(can_view_orders=True).values_list('limit_events__id', flat=True))
|
||
).values_list('id', flat=True))
|
||
|
||
tpl = """
|
||
<a href="{url}" class="event">
|
||
<div class="name">{event}</div>
|
||
<div class="daterange">{daterange}</div>
|
||
<div class="times">{times}</div>
|
||
</a>
|
||
<div class="bottomrow">
|
||
{orders}
|
||
<a href="{url}" class="status-{statusclass}">
|
||
{status}
|
||
</a>
|
||
</div>
|
||
"""
|
||
|
||
if lazy:
|
||
events = qs[:nmax]
|
||
else:
|
||
events = qs.prefetch_related(
|
||
'_settings_objects', 'organizer___settings_objects'
|
||
).select_related('organizer')[:nmax]
|
||
for event in events:
|
||
if not lazy:
|
||
tzname = event.cache.get_or_set('timezone', lambda: event.settings.timezone)
|
||
tz = ZoneInfo(tzname)
|
||
if event.has_subevents:
|
||
if event.min_from is None:
|
||
dr = pgettext("subevent", "No dates")
|
||
else:
|
||
dr = daterange(
|
||
(event.min_from).astimezone(tz),
|
||
(event.max_fromto or event.max_to or event.max_from).astimezone(tz)
|
||
)
|
||
else:
|
||
if event.date_to:
|
||
dr = daterange(event.date_from.astimezone(tz), event.date_to.astimezone(tz))
|
||
else:
|
||
dr = date_format(event.date_from.astimezone(tz), "DATE_FORMAT")
|
||
|
||
if not event.live:
|
||
status = ('warning', _('Shop disabled'))
|
||
elif event.presale_has_ended:
|
||
status = ('default', _('Sale over'))
|
||
elif not event.presale_is_running:
|
||
status = ('default', _('Soon'))
|
||
else:
|
||
status = ('success', _('On sale'))
|
||
|
||
widgets.append({
|
||
'content': tpl.format(
|
||
event=escape(event.name),
|
||
times=_('Event series') if event.has_subevents else (
|
||
((date_format(event.date_admission.astimezone(tz), 'TIME_FORMAT') + ' / ')
|
||
if event.date_admission and event.date_admission != event.date_from else '')
|
||
+ (date_format(event.date_from.astimezone(tz), 'TIME_FORMAT') if event.date_from else '')
|
||
) + (
|
||
' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>'.format(tzname)
|
||
if tzname != request.timezone and not event.has_subevents else ''
|
||
),
|
||
url=reverse('control:event.index', kwargs={
|
||
'event': event.slug,
|
||
'organizer': event.organizer.slug
|
||
}),
|
||
orders=(
|
||
'<a href="{orders_url}" class="orders">{orders_text}</a>'.format(
|
||
orders_url=reverse('control:event.orders', kwargs={
|
||
'event': event.slug,
|
||
'organizer': event.organizer.slug
|
||
}),
|
||
orders_text=ngettext('{num} order', '{num} orders', event.order_count or 0).format(
|
||
num=event.order_count or 0
|
||
)
|
||
) if user.has_active_staff_session(request.session.session_key) or event.pk in events_with_orders else ''
|
||
),
|
||
daterange=dr,
|
||
status=status[1],
|
||
statusclass=status[0],
|
||
) if not lazy else '',
|
||
'display_size': 'small',
|
||
'lazy': 'event-{}'.format(event.pk),
|
||
'priority': 100,
|
||
'container_class': 'widget-container widget-container-event',
|
||
})
|
||
"""
|
||
{% if not e.live %}
|
||
<span class="label label-danger">{% trans "Shop disabled" %}</span>
|
||
{% elif e.presale_has_ended %}
|
||
<span class="label label-warning">{% trans "Presale over" %}</span>
|
||
{% elif not e.presale_is_running %}
|
||
<span class="label label-warning">{% trans "Presale not started" %}</span>
|
||
{% else %}
|
||
<span class="label label-success">{% trans "On sale" %}</span>
|
||
{% endif %}
|
||
"""
|
||
return widgets
|
||
|
||
|
||
def user_index_widgets_lazy(request):
|
||
widgets = []
|
||
widgets += widgets_for_event_qs(
|
||
request,
|
||
annotated_event_query(request).filter(
|
||
Q(has_subevents=False) &
|
||
Q(
|
||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||
)
|
||
).order_by('date_from', 'order_to', 'pk'),
|
||
request.user,
|
||
7
|
||
)
|
||
widgets += widgets_for_event_qs(
|
||
request,
|
||
annotated_event_query(request).filter(
|
||
Q(has_subevents=False) &
|
||
Q(
|
||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||
)
|
||
).order_by('-order_to', 'pk'),
|
||
request.user,
|
||
8
|
||
)
|
||
widgets += widgets_for_event_qs(
|
||
request,
|
||
annotated_event_query(request).filter(
|
||
has_subevents=True
|
||
).order_by('-order_to', 'pk'),
|
||
request.user,
|
||
8
|
||
)
|
||
return JsonResponse({'widgets': widgets})
|
||
|
||
|
||
def user_index(request):
|
||
widgets = []
|
||
for r, result in user_dashboard_widgets.send(request, user=request.user):
|
||
widgets.extend(result)
|
||
|
||
ctx = {
|
||
'widgets': rearrange(widgets),
|
||
'can_create_event': request.user.teams.filter(can_create_events=True).exists(),
|
||
'upcoming': widgets_for_event_qs(
|
||
request,
|
||
annotated_event_query(request, lazy=True).filter(
|
||
Q(has_subevents=False) &
|
||
Q(
|
||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||
)
|
||
).order_by('date_from', 'order_to', 'pk'),
|
||
request.user,
|
||
7,
|
||
lazy=True
|
||
),
|
||
'past': widgets_for_event_qs(
|
||
request,
|
||
annotated_event_query(request, lazy=True).filter(
|
||
Q(has_subevents=False) &
|
||
Q(
|
||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||
)
|
||
).order_by('-order_to', 'pk'),
|
||
request.user,
|
||
8,
|
||
lazy=True
|
||
),
|
||
'series': widgets_for_event_qs(
|
||
request,
|
||
annotated_event_query(request, lazy=True).filter(
|
||
has_subevents=True
|
||
).order_by('-order_to', 'pk'),
|
||
request.user,
|
||
8,
|
||
lazy=True
|
||
),
|
||
}
|
||
return render(request, 'pretixcontrol/dashboard.html', ctx)
|
||
|
||
|
||
def rearrange(widgets: list):
|
||
"""
|
||
Sort widget boxes according to priority.
|
||
"""
|
||
mapping = {
|
||
'small': 1,
|
||
'big': 2,
|
||
'full': 3,
|
||
}
|
||
|
||
def sort_key(element):
|
||
return (
|
||
element.get('priority', 1),
|
||
mapping.get(element.get('display_size', 'small'), 1),
|
||
)
|
||
|
||
return sorted(widgets, key=sort_key, reverse=True)
|