mirror of
https://github.com/pretix/pretix.git
synced 2025-12-13 12:42:26 +00:00
Compare commits
12 Commits
widget-coo
...
event-dash
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75fd41d6f6 | ||
|
|
20b3f43891 | ||
|
|
0b41aeb7f1 | ||
|
|
8e71102ab2 | ||
|
|
5ebaa63f6f | ||
|
|
e68cea8542 | ||
|
|
db0af4e46f | ||
|
|
b701854127 | ||
|
|
dd5d614da2 | ||
|
|
1c1652e76b | ||
|
|
64838c53f1 | ||
|
|
b47f57cf8e |
393
src/pretix/control/templates/pretixcontrol/event/new_index.html
Normal file
393
src/pretix/control/templates/pretixcontrol/event/new_index.html
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load eventurl %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load static %}
|
||||||
|
{% load escapejson %}
|
||||||
|
{% load eventsignal %}
|
||||||
|
{% block title %}{{ request.event.name }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div id="header-area" class="col-lg-8">
|
||||||
|
<h1>
|
||||||
|
{{ request.event.name }}
|
||||||
|
<small>
|
||||||
|
{% if request.event.has_subevents %}
|
||||||
|
{% trans "Event series" %}
|
||||||
|
{% else %}
|
||||||
|
{{ request.event.get_date_range_display }}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="helper-space-below">
|
||||||
|
{% trans "Shop URL:" %}
|
||||||
|
<span id="shop_url" class="text-muted">{% abseventurl request.event "presale:event.index" %}</span>
|
||||||
|
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#shop_url">
|
||||||
|
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
|
||||||
|
</button>
|
||||||
|
<div class="btn-group helper-display-inline-block">
|
||||||
|
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<i class="fa fa-qrcode" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% include "pretixcontrol/event/fragment_qr_dropdown.html" with url=0 %}
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<span id="shop_state" class="shop_state">
|
||||||
|
{{ shop_state.content|safe }}
|
||||||
|
<a href="{{ shop_state.url }}">
|
||||||
|
<button type="button" class="btn btn-default btn-xs btn-wrench js-only" href="{{ shop_state.url }}">
|
||||||
|
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{% trans "Change shop state" %}</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Comments ----------------------------------------------------------------------------------------------------->
|
||||||
|
<div class="event-comment col-lg-4">
|
||||||
|
{% trans "Internal comment" %}
|
||||||
|
<form class="form" method="post"
|
||||||
|
action="{% url "control:event.comment" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
{% bootstrap_field comment_form.comment layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
|
||||||
|
</div>
|
||||||
|
{% if not comment_form.readonly %}
|
||||||
|
<p class="text-right flip">
|
||||||
|
<button class="btn btn-default">
|
||||||
|
{% trans "Update comment" %}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Comments ----------------------------------------------------------------------------------------------------->
|
||||||
|
|
||||||
|
<!-- Warnings ----------------------------------------------------------------------------------------------------->
|
||||||
|
<div class="row-form-errors">
|
||||||
|
{% if has_overpaid_orders %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This event contains <strong>overpaid orders</strong>, for example due to duplicate payment attempts.
|
||||||
|
You should review the cases and consider refunding the overpaid amount to the user.
|
||||||
|
{% endblocktrans %}
|
||||||
|
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=overpaid"
|
||||||
|
class="btn btn-primary">{% trans "Show overpaid orders" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_pending_refunds %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This event contains <strong>pending refunds</strong> that you should take care of.
|
||||||
|
{% endblocktrans %}
|
||||||
|
<a href="{% url "control:event.orders.refunds" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||||
|
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_cancellation_requests %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This event contains <strong>requested cancellations</strong> that you should take care of.
|
||||||
|
{% endblocktrans %}
|
||||||
|
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=rc"
|
||||||
|
class="btn btn-primary">{% trans "Show orders requesting cancellation" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_pending_approvals %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This event contains <strong>pending approvals</strong> that you should take care of.
|
||||||
|
{% endblocktrans %}
|
||||||
|
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pa"
|
||||||
|
class="btn btn-primary">{% trans "Show orders pending approval" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_pending_orders_with_full_payment %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
This event contains <strong>fully paid orders</strong> that are not marked as paid, probably
|
||||||
|
because no quota was left at the time their payment arrived. You should review the cases and consider
|
||||||
|
either refunding the customer or creating more space.
|
||||||
|
{% endblocktrans %}
|
||||||
|
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pendingpaid"
|
||||||
|
class="btn btn-primary">{% trans "Show affected orders" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<!-- Warnings ----------------------------------------------------------------------------------------------------->
|
||||||
|
|
||||||
|
{% eventsignal request.event "pretix.control.signals.event_dashboard_top" request=request %}
|
||||||
|
<!-- Timeline ----------------------------------------------------------------------------------------------------->
|
||||||
|
{% if request.event.has_subevents %}
|
||||||
|
<form class="form-inline helper-display-inline" action="" method="get">
|
||||||
|
{% 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 %}
|
||||||
|
<!-- Timeline ----------------------------------------------------------------------------------------------------->
|
||||||
|
<!-- Big numbers ---------------------------------------------------------------------------------------------->
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="widget-container widget-small event-dashboard">
|
||||||
|
<a href="{{ attendees_paid_ordered.url }}" class="widget">
|
||||||
|
{{ attendees_paid_ordered.content|safe }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="widget-container widget-small event-dashboard">
|
||||||
|
<a href="{{ total_revenue.url }}" class="widget">
|
||||||
|
{{ total_revenue.content|safe }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Big numbers -------------------------------------------------------------------------------------------------->
|
||||||
|
<!-- Diagram ------------------------------------------------------------------------------------------------------>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">{% trans "Revenue over time" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div id="rev_chart" class="chart"></div>
|
||||||
|
{% if request.GET.subevent %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed context "subevent" %}
|
||||||
|
If you select a single date, payment method fees will not be listed here as it might not be clear which
|
||||||
|
date they belong to.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<p class="help-block">
|
||||||
|
<small>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Only fully paid orders are counted.
|
||||||
|
Orders paid in multiple payments are shown with the date of their last payment.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Diagram ------------------------------------------------------------------------------------------------------>
|
||||||
|
<!-- Check-Ins ---------------------------------------------------------------------------------------------------->
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">{% trans "Check-In" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-quotas">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% trans "Name" %}
|
||||||
|
</th>
|
||||||
|
<th>{% trans "Checked in" %}</th>
|
||||||
|
{% if request.event.has_subevents %}
|
||||||
|
<th>
|
||||||
|
{% trans "Date" context "subevent" %}
|
||||||
|
</th>
|
||||||
|
{% endif %}
|
||||||
|
<th class="iconcol">{% trans "Automated check-in" %}</th>
|
||||||
|
<th>{% trans "Products" %}</th>
|
||||||
|
<th class="action-col-2"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for cl in checkinlists %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><a
|
||||||
|
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="quotabox availability">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="numbers">
|
||||||
|
{{ cl.checkin_count|default_if_none:"0" }} /
|
||||||
|
{{ cl.position_count|default_if_none:"0" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% if request.event.has_subevents %}
|
||||||
|
{% if cl.subevent %}
|
||||||
|
<td>
|
||||||
|
{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }}
|
||||||
|
{{ cl.subevent.date_from|date:"TIME_FORMAT" }}
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td>
|
||||||
|
<em>{% trans "All" %}</em>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
{% for channel in cl.auto_checkin_sales_channels %}
|
||||||
|
<span class="fa fa-{{ channel.icon }} text-muted"
|
||||||
|
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if cl.all_products %}
|
||||||
|
<em>{% trans "All" %}</em>
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for item in cl.limit_products.all %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-right flip">
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
|
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
|
||||||
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
|
||||||
|
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||||
|
<span class="fa fa-copy"></span>
|
||||||
|
</a>
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
|
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
|
||||||
|
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
|
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||||
|
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||||
|
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Check-Ins ---------------------------------------------------------------------------------------------------->
|
||||||
|
<!-- Quotas ------------------------------------------------------------------------------------------------------->
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">{% trans "Quotas" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive table-quotas">
|
||||||
|
<table class="table table-hover table-quotas">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% trans "Name" %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Begin" %}
|
||||||
|
<a href="? url_replace request 'filter-ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
|
<a href="? url_replace request 'filter-ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Paid tickets per quota" %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Capacity left" %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if request.event.has_subevents and not subevent %}
|
||||||
|
{% for s in subevents %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}">
|
||||||
|
{{ s.name }}</a></strong><br>
|
||||||
|
<small class="text-muted">
|
||||||
|
#{{ s.pk }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ s.get_date_from_display }}<br>
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ s.date_from|date:"l" }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for q in s.first_quotas|slice:":3" %}
|
||||||
|
{% include "pretixcontrol/fragment_quota_box_paid.html" with quota=q %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if s.first_quotas|length > 3 %}
|
||||||
|
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}"
|
||||||
|
class="quotabox-more" data-toggle="tooltip" title="{% trans "More quotas" %}"
|
||||||
|
data-placement="top">
|
||||||
|
···
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for q in s.first_quotas %}
|
||||||
|
{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% if subevent %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=subevent.id %}?returnto={{ request.GET.urlencode|urlencode }}">
|
||||||
|
{{ subevent.name }}</a></strong><br>
|
||||||
|
<small class="text-muted">
|
||||||
|
#{{ subevent.pk }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ subevent.get_date_from_display }}<br>
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ subevent.date_from|date:"l" }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for q in subevent.first_quotas|slice:":3" %}
|
||||||
|
{% include "pretixcontrol/fragment_quota_box_paid.html" with quota=q %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if subevent.first_quotas|length > 3 %}
|
||||||
|
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ subevent.id }}"
|
||||||
|
class="quotabox-more" data-toggle="tooltip" title="{% trans "More quotas" %}"
|
||||||
|
data-placement="top">
|
||||||
|
···
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for q in subevent.first_quotas %}
|
||||||
|
{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Quotas ------------------------------------------------------------------------------------------------------->
|
||||||
|
<!-- Logs --------------------------------------------------------------------------------------------------------->
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
{% trans "Event logs" %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<ul class="list-group" id="logs_target">
|
||||||
|
<div class="logs-lazy-loading">
|
||||||
|
<span class="fa fa-cog fa-4x"></span>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
<div class="panel-footer">
|
||||||
|
<a href="{% url "control:event.log" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||||
|
{% trans "Show more logs" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Logs --------------------------------------------------------------------------------------------------------->
|
||||||
|
<script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script>
|
||||||
|
<script type="application/text" id="currency">{{ request.event.currency }}</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -38,8 +38,8 @@ from django.views.generic.base import RedirectView
|
|||||||
|
|
||||||
from pretix.control.views import (
|
from pretix.control.views import (
|
||||||
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
|
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
|
||||||
main, oauth, orderimport, orders, organizer, pdf, search, shredder,
|
main, new_dashboard, oauth, orderimport, orders, organizer, pdf, search,
|
||||||
subevents, typeahead, user, users, vouchers, waitinglist,
|
shredder, subevents, typeahead, user, users, vouchers, waitinglist,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -239,7 +239,7 @@ urlpatterns = [
|
|||||||
re_path(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'),
|
re_path(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'),
|
||||||
re_path(r'^search/payments/$', search.PaymentSearch.as_view(), name='search.payments'),
|
re_path(r'^search/payments/$', search.PaymentSearch.as_view(), name='search.payments'),
|
||||||
re_path(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
|
re_path(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
|
||||||
re_path(r'^$', dashboards.event_index, name='event.index'),
|
re_path(r'^$', new_dashboard.IndexView.as_view(), name='event.index'),
|
||||||
re_path(r'^qrcode.(?P<filetype>(png|jpeg|gif|svg))$', event.EventQRCode.as_view(), name='event.qrcode'),
|
re_path(r'^qrcode.(?P<filetype>(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'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
|
||||||
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
|
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
|
||||||
|
|||||||
266
src/pretix/control/views/new_dashboard.py
Normal file
266
src/pretix/control/views/new_dashboard.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import dateutil
|
||||||
|
from django.db.models import (
|
||||||
|
DateTimeField, F, Max, OuterRef, Prefetch, Q, Subquery, Sum,
|
||||||
|
)
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import formats, timezone
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
from pretix.base.channels import get_all_sales_channels
|
||||||
|
from pretix.base.decimal import round_decimal
|
||||||
|
from pretix.base.models import SubEvent
|
||||||
|
from pretix.base.models.orders import (
|
||||||
|
CancellationRequest, Order, OrderPayment, OrderPosition, OrderRefund,
|
||||||
|
)
|
||||||
|
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.permissions import EventPermissionRequiredMixin
|
||||||
|
from pretix.control.views import ChartContainingView
|
||||||
|
|
||||||
|
NUM_WIDGET = str('<div class="numwidget">'
|
||||||
|
'<span class="num">{num}</span>'
|
||||||
|
'<span class="text"><span class="label-primary">{text}</span></span>'
|
||||||
|
'<span class="text-add">{text_add}</span></div>')
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView):
|
||||||
|
template_name = 'pretixcontrol/event/new_index.html'
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
subevent = None
|
||||||
|
if self.request.GET.get("subevent", "") != "" and self.request.event.has_subevents:
|
||||||
|
i = self.request.GET.get("subevent", "")
|
||||||
|
try:
|
||||||
|
subevent = self.request.event.subevents.get(pk=i)
|
||||||
|
except SubEvent.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
can_view_orders = self.request.user.has_event_permission(self.request.organizer, self.request.event,
|
||||||
|
'can_view_orders',
|
||||||
|
request=self.request)
|
||||||
|
# can_change_event_settings = self.request.user.has_event_permission(self.request.organizer, self.request.event,
|
||||||
|
# 'can_change_event_settings',
|
||||||
|
# request=self.request)
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
'subevent': subevent,
|
||||||
|
'comment_form': CommentForm(initial={'comment': self.request.event.comment},
|
||||||
|
readonly=True), # not can_change_event_settings),
|
||||||
|
}
|
||||||
|
|
||||||
|
if subevent:
|
||||||
|
opqs = OrderPosition.objects.filter(subevent=subevent)
|
||||||
|
else:
|
||||||
|
opqs = OrderPosition.objects
|
||||||
|
|
||||||
|
ctx['shop_state'] = {
|
||||||
|
'display_size': 'small',
|
||||||
|
'priority': 1000,
|
||||||
|
'content': '<span class="{cls}">{t1} {state} <span class="fa {icon}"></span></span>'.format(
|
||||||
|
t1=_('Your ticket shop is'),
|
||||||
|
state=_('live') if self.request.event.live and not self.request.event.testmode else (
|
||||||
|
_('live and in test mode') if self.request.event.live else (
|
||||||
|
_('not yet public') if not self.request.event.testmode else (
|
||||||
|
_('in private test mode')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
icon='fa-check-circle' if self.request.event.live and not self.request.event.testmode else (
|
||||||
|
'fa-warning' if self.request.event.live else (
|
||||||
|
'fa-times-circle' if not self.request.event.testmode else (
|
||||||
|
'fa-lock'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
cls='live' if self.request.event.live else 'off'
|
||||||
|
),
|
||||||
|
'url': reverse('control:event.live', kwargs={
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
'organizer': self.request.event.organizer.slug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
qs = self.request.event.checkin_lists.filter(subevent=subevent)
|
||||||
|
sales_channels = get_all_sales_channels()
|
||||||
|
for cl in qs:
|
||||||
|
if cl.subevent:
|
||||||
|
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
|
||||||
|
cl.auto_checkin_sales_channels = [sales_channels[channel] for channel in cl.auto_checkin_sales_channels]
|
||||||
|
ctx['checkinlists'] = qs
|
||||||
|
|
||||||
|
qs = self.request.event.subevents
|
||||||
|
if list:
|
||||||
|
qs = qs.prefetch_related(
|
||||||
|
Prefetch('quotas',
|
||||||
|
queryset=self.request.event.quotas.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
|
||||||
|
to_attr='first_quotas')
|
||||||
|
)
|
||||||
|
ctx['subevents'] = qs
|
||||||
|
quotas = []
|
||||||
|
for s in ctx['subevents']:
|
||||||
|
s.first_quotas = s.first_quotas[:4]
|
||||||
|
quotas += list(s.first_quotas)
|
||||||
|
|
||||||
|
qa = QuotaAvailability(early_out=False)
|
||||||
|
for q in quotas:
|
||||||
|
qa.queue(q)
|
||||||
|
qa.compute()
|
||||||
|
|
||||||
|
for q in quotas:
|
||||||
|
q.cached_avail = qa.results[q]
|
||||||
|
q.cached_availability_paid_orders = qa.count_paid_orders.get(q, 0)
|
||||||
|
if q.size is not None:
|
||||||
|
q.percent_paid = min(
|
||||||
|
100,
|
||||||
|
round(q.cached_availability_paid_orders / q.size * 100) if q.size > 0 else 100
|
||||||
|
)
|
||||||
|
|
||||||
|
tickc = opqs.filter(
|
||||||
|
order__event=self.request.event, item__admission=True,
|
||||||
|
order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
|
||||||
|
).count()
|
||||||
|
paidc = opqs.filter(
|
||||||
|
order__event=self.request.event, item__admission=True,
|
||||||
|
order__status=Order.STATUS_PAID,
|
||||||
|
).count()
|
||||||
|
ctx['attendees_paid_ordered'] = {
|
||||||
|
'content': NUM_WIDGET.format(
|
||||||
|
num=f'{tickc}',
|
||||||
|
text=_('<span class="fa fa-user icon"></span> Attendees'),
|
||||||
|
text_add=f'{paidc} paid, {tickc - paidc} pending'),
|
||||||
|
'priority': 100,
|
||||||
|
'url': reverse('control:event.orders.overview', kwargs={
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
'organizer': self.request.event.organizer.slug
|
||||||
|
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if subevent:
|
||||||
|
rev = opqs.filter(
|
||||||
|
order__event=self.request.event, order__status=Order.STATUS_PAID
|
||||||
|
).aggregate(
|
||||||
|
sum=Sum('price')
|
||||||
|
)['sum'] or Decimal('0.00')
|
||||||
|
else:
|
||||||
|
rev = Order.objects.filter(
|
||||||
|
event=self.request.event,
|
||||||
|
status=Order.STATUS_PAID
|
||||||
|
).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')
|
||||||
|
|
||||||
|
ctx['total_revenue'] = {
|
||||||
|
'content': NUM_WIDGET.format(
|
||||||
|
num='<span class="icon">{currency}</span> {amount}'.format(
|
||||||
|
currency=self.request.event.currency,
|
||||||
|
amount=formats.localize(round_decimal(rev, self.request.event.currency))
|
||||||
|
),
|
||||||
|
text=_('Total revenue'),
|
||||||
|
text_add=''
|
||||||
|
),
|
||||||
|
'priority': 100,
|
||||||
|
'url': reverse('control:event.orders.overview', kwargs={
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
'organizer': self.request.event.organizer.slug
|
||||||
|
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = self.request.event.cache
|
||||||
|
ckey = str(subevent.pk) if subevent else 'all'
|
||||||
|
tz = timezone.get_current_timezone()
|
||||||
|
op_date = OrderPayment.objects.filter(
|
||||||
|
order=OuterRef('order'),
|
||||||
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||||
|
payment_date__isnull=False
|
||||||
|
).values('order').annotate(
|
||||||
|
m=Max('payment_date')
|
||||||
|
).values(
|
||||||
|
'm'
|
||||||
|
).order_by()
|
||||||
|
p_date = OrderPayment.objects.filter(
|
||||||
|
order=OuterRef('pk'),
|
||||||
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||||
|
payment_date__isnull=False
|
||||||
|
).values('order').annotate(
|
||||||
|
m=Max('payment_date')
|
||||||
|
).values(
|
||||||
|
'm'
|
||||||
|
).order_by()
|
||||||
|
ctx['rev_data'] = cache.get('statistics_rev_data' + ckey)
|
||||||
|
if not ctx['rev_data']:
|
||||||
|
rev_by_day = {}
|
||||||
|
if subevent:
|
||||||
|
for o in OrderPosition.objects.annotate(
|
||||||
|
payment_date=Subquery(op_date, output_field=DateTimeField())
|
||||||
|
).filter(order__event=self.request.event,
|
||||||
|
subevent=subevent,
|
||||||
|
order__status=Order.STATUS_PAID,
|
||||||
|
payment_date__isnull=False).values('payment_date', 'price'):
|
||||||
|
day = o['payment_date'].astimezone(tz).date()
|
||||||
|
rev_by_day[day] = rev_by_day.get(day, 0) + o['price']
|
||||||
|
else:
|
||||||
|
for o in Order.objects.annotate(
|
||||||
|
payment_date=Subquery(p_date, output_field=DateTimeField())
|
||||||
|
).filter(event=self.request.event,
|
||||||
|
status=Order.STATUS_PAID,
|
||||||
|
payment_date__isnull=False).values('payment_date', 'total'):
|
||||||
|
day = o['payment_date'].astimezone(tz).date()
|
||||||
|
rev_by_day[day] = rev_by_day.get(day, 0) + o['total']
|
||||||
|
|
||||||
|
data = []
|
||||||
|
total = 0
|
||||||
|
for d in dateutil.rrule.rrule(
|
||||||
|
dateutil.rrule.DAILY,
|
||||||
|
dtstart=min(rev_by_day.keys() if rev_by_day else [datetime.date.today()]),
|
||||||
|
until=max(rev_by_day.keys() if rev_by_day else [datetime.date.today()])):
|
||||||
|
d = d.date()
|
||||||
|
rev = float(rev_by_day.get(d, 0))
|
||||||
|
if True: # rev != 0:
|
||||||
|
total += rev
|
||||||
|
data.append({
|
||||||
|
'date': d.strftime('%Y-%m-%d'),
|
||||||
|
'revenue': round(total, 2),
|
||||||
|
})
|
||||||
|
ctx['rev_data'] = json.dumps(data)
|
||||||
|
cache.set('statistics_rev_data' + ckey, ctx['rev_data'])
|
||||||
|
|
||||||
|
ctx['has_overpaid_orders'] = can_view_orders and Order.annotate_overpayments(self.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(
|
||||||
|
self.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=self.request.event,
|
||||||
|
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_EXTERNAL)
|
||||||
|
).exists()
|
||||||
|
ctx['has_pending_approvals'] = can_view_orders and self.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=self.request.event
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
ctx['timeline'] = [
|
||||||
|
{
|
||||||
|
'date': t.datetime.astimezone(self.request.event.timezone).date(),
|
||||||
|
'entry': t,
|
||||||
|
'time': t.datetime.astimezone(self.request.event.timezone)
|
||||||
|
}
|
||||||
|
for t in timeline_for_event(self.request.event, subevent)
|
||||||
|
]
|
||||||
|
ctx['today'] = now().astimezone(self.request.event.timezone).date()
|
||||||
|
ctx['nearly_now'] = now().astimezone(self.request.event.timezone) - datetime.timedelta(seconds=20)
|
||||||
|
# resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
|
||||||
|
return ctx
|
||||||
@@ -20,3 +20,26 @@ $(function () {
|
|||||||
add_log_expand_handlers($("#logs_target"))
|
add_log_expand_handlers($("#logs_target"))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function gettext(msgid) {
|
||||||
|
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
|
||||||
|
return django.gettext(msgid);
|
||||||
|
}
|
||||||
|
return msgid;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
$(".chart").css("height", "250px");
|
||||||
|
new Morris.Area({
|
||||||
|
element: 'rev_chart',
|
||||||
|
data: JSON.parse($("#rev-data").html()),
|
||||||
|
xkey: 'date',
|
||||||
|
ykeys: ['revenue'],
|
||||||
|
labels: [gettext('Total revenue')],
|
||||||
|
smooth: false,
|
||||||
|
resize: true,
|
||||||
|
lineColors: ['#3b1c4a'],
|
||||||
|
fillOpacity: 0.3,
|
||||||
|
preUnits: $.trim($("#currency").html()) + ' '
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,18 +3,34 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-left: -5px;
|
margin-left: -5px;
|
||||||
|
margin-top: -5px;
|
||||||
margin-right: -5px;
|
margin-right: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard.dashboard-panels {
|
||||||
|
margin-left: -15px;
|
||||||
|
margin-right: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard .widget-container {
|
.dashboard .widget-container {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
padding: 15px 5px;
|
padding: 15px 5px;
|
||||||
border: 5px solid white;
|
border: 5px solid white;
|
||||||
min-height: 160px;
|
min-height: 140px;
|
||||||
background: #F8F8F8;
|
background: #F8F8F8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard .widget-container.event-dashboard {
|
||||||
|
background: white;
|
||||||
|
padding: 0 5px 15px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .widget-container.event-dashboard:hover {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard .widget-container.widget-full {
|
.dashboard .widget-container.widget-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -25,6 +41,17 @@
|
|||||||
|
|
||||||
.dashboard .widget-container.widget-small {
|
.dashboard .widget-container.widget-small {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: lighten($brand-primary, 25%);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text .label-primary {
|
||||||
|
color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard .widget-container.widget-lazy-loading {
|
.dashboard .widget-container.widget-lazy-loading {
|
||||||
@@ -42,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard .widget-container:hover, .dashboard .widget-container:focus {
|
.dashboard .widget-container:hover, .dashboard .widget-container:focus {
|
||||||
background: #EEEEEE;
|
background: $brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard .widget:hover, .dashboard .widget:focus, .dashboard a:hover {
|
.dashboard .widget:hover, .dashboard .widget:focus, .dashboard a:hover {
|
||||||
@@ -52,32 +79,51 @@
|
|||||||
.dashboard .numwidget {
|
.dashboard .numwidget {
|
||||||
.num {
|
.num {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px 0 10px;
|
font-size: 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 20px;
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-add {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard .shopstate {
|
.event-comment {
|
||||||
text-align: center;
|
textarea.form-control {
|
||||||
padding: 18px 0;
|
height: 0;
|
||||||
|
min-height: 95px;
|
||||||
span.live, span.off {
|
|
||||||
display: block;
|
|
||||||
font-size: 20px;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop_state {
|
||||||
|
span.live, span.off {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
span.live {
|
span.live {
|
||||||
color: $brand-success;
|
color: $brand-success;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.off {
|
span.off {
|
||||||
color: $brand-danger;
|
color: $brand-danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-quotas {
|
||||||
|
max-height: 250px;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-container.widget-container-event {
|
.widget-container.widget-container-event {
|
||||||
|
|||||||
Reference in New Issue
Block a user