mirror of
https://github.com/pretix/pretix.git
synced 2026-04-28 00:02:37 +00:00
* Data model draft * Refactor query and assignment usages of old permissions * Backend UI * API serializer * Big string replace * Docs, tests and fixes for teams api * Update docs for device auth * Eliminate old names * Make tests pass * Use new permissions, remove inconsistencies * Add test for translations * Show plugin permissions * Add permission for seating plans * Fix plugin activation * Fix failing test * Refactor to permission groups * Update doc/api/resources/devices.rst Co-authored-by: luelista <weller@rami.io> * Update doc/api/resources/events.rst Co-authored-by: luelista <weller@rami.io> * Update src/pretix/api/serializers/organizer.py Co-authored-by: luelista <weller@rami.io> * Fix typo * Fix python version compat * Replacement after rebase * Add proper permission handling for exports * Docs for exporters * Runtime linting of permission names * Fix typos * Show export page even without orders permission * More legacy compat * Do not strongly validate before plugins are loaded * Rebase migration * Add permission for outgoing mails * Review notes * Update doc/api/resources/teams.rst Co-authored-by: Richard Schreiber <schreiber@pretix.eu> * Clean up logic around exporters * Review and failures * Fix migration leading to forbidden combination * Handle permissions on event copying * Remove print-statements * Make test clearer * Review feedback * Add AnyPermissionOf * migration safety --------- Co-authored-by: luelista <weller@rami.io> Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
701 lines
28 KiB
Python
701 lines
28 KiB
Python
#
|
||
# This file is part of pretix (Community Edition).
|
||
#
|
||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||
# Copyright (C) 2020-today pretix 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.signals import (
|
||
event_dashboard_widgets, 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
|
||
|
||
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):
|
||
from pretix.control.forms.event import CommentForm
|
||
|
||
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_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
|
||
'event.settings.general:write', 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, 'event.orders:read',
|
||
request=request)
|
||
can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
|
||
'event.settings.general:write', request=request)
|
||
can_view_vouchers = request.user.has_event_permission(request.organizer, request.event, 'event.vouchers:read',
|
||
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, 'event.items:write', 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(
|
||
TeamQuerySet.event_permission_q("event.orders:read"),
|
||
all_events=True,
|
||
).values_list('organizer', flat=True))
|
||
| Q(id__in=user.teams.filter(
|
||
TeamQuerySet.event_permission_q("event.orders:read"),
|
||
).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.with_organizer_permission("organizer.events:create").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)
|