mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
Better dashboard layout
This commit is contained in:
@@ -89,7 +89,7 @@
|
||||
<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu event-dropdown" role="menu" data-event-typeahead
|
||||
data-source="{% url "control:events.typeahead" %}">
|
||||
<li>
|
||||
<li class="query-holder">
|
||||
<div class="form-box">
|
||||
<input type="text" class="form-control"
|
||||
placeholder="{% trans "Search for events" %}"
|
||||
|
||||
@@ -3,9 +3,18 @@
|
||||
{% block title %}{% trans "Dashboard" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Dashboard" %}</h1>
|
||||
<div class="row dashboard">
|
||||
|
||||
<div class="dropdown-container">
|
||||
<input type="text" class="form-control" id="dashboard_query"
|
||||
placeholder="{% trans "Go to event" %}"
|
||||
data-typeahead-query autofocus>
|
||||
<ul data-event-typeahead data-source="{% url "control:events.typeahead" %}" data-typeahead-field="#dashboard_query"
|
||||
class="event-dropdown dropdown-menu">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dashboard">
|
||||
{% for w in widgets %}
|
||||
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
|
||||
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }}">
|
||||
{% if w.url %}
|
||||
<a href="{{ w.url }}" class="widget">
|
||||
{{ w.content|safe }}
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import pytz
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Sum
|
||||
from django.db.models import (
|
||||
Count, Exists, IntegerField, Max, Min, OuterRef, Q, Subquery, Sum,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.dispatch import receiver
|
||||
from django.shortcuts import render
|
||||
from django.template.loader import get_template
|
||||
from django.utils import formats
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext_lazy as _, ungettext
|
||||
|
||||
from pretix.base.models import (
|
||||
Item, Order, OrderPosition, Voucher, WaitingListEntry,
|
||||
Event, Item, Order, OrderPosition, RequiredAction, Voucher,
|
||||
WaitingListEntry,
|
||||
)
|
||||
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 ..logdisplay import OVERVIEW_BLACKLIST
|
||||
|
||||
@@ -255,21 +261,118 @@ def user_event_widgets(**kwargs):
|
||||
user = kwargs.pop('user')
|
||||
widgets = []
|
||||
|
||||
events = user.get_events_with_any_permission().order_by('-date_from', 'name').select_related('organizer')[:100]
|
||||
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>
|
||||
"""
|
||||
|
||||
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'
|
||||
)
|
||||
|
||||
required_actions = RequiredAction.objects.filter(
|
||||
event=OuterRef('pk'),
|
||||
done=False
|
||||
)
|
||||
|
||||
# Get set of events where we have the permission to show the # of orders
|
||||
events_with_orders = set(Event.objects.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))
|
||||
|
||||
events = user.get_events_with_any_permission().annotate(
|
||||
order_count=Subquery(active_orders, output_field=IntegerField()),
|
||||
has_ra=Exists(required_actions)
|
||||
).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_from=Coalesce('min_from', 'date_from'),
|
||||
order_to=Coalesce('max_fromto', 'max_to', 'max_from', 'date_to'),
|
||||
).order_by(
|
||||
'-order_from', 'name'
|
||||
).prefetch_related(
|
||||
'_settings_objects', 'organizer___settings_objects'
|
||||
).select_related('organizer')[:100]
|
||||
for event in events:
|
||||
dr = event.get_date_range_display()
|
||||
if event.has_subevents:
|
||||
tz = pytz.timezone(event.settings.timezone)
|
||||
dr = daterange(
|
||||
(event.min_from).astimezone(tz),
|
||||
(event.max_fromto or event.max_to or event.max_from).astimezone(tz)
|
||||
)
|
||||
|
||||
if event.has_ra:
|
||||
status = ('danger', _('Action required'))
|
||||
elif 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': '<div class="event">{event}<span class="from">{df}</span><span class="to">{dt}</span></div>'.format(
|
||||
'content': tpl.format(
|
||||
event=escape(event.name),
|
||||
df=date_format(event.date_from, 'SHORT_DATE_FORMAT') if event.date_from else '',
|
||||
dt=date_format(event.date_to, 'SHORT_DATE_FORMAT') if event.date_to else ''
|
||||
times=_('Event series') if event.has_subevents else (
|
||||
((date_format(event.date_admission, 'TIME_FORMAT') + ' / ')
|
||||
if event.date_admission and event.date_admission != event.date_from else '')
|
||||
+ (date_format(event.date_from, 'TIME_FORMAT') if event.date_from 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=ungettext('{num} order', '{num} orders', event.order_count or 0).format(
|
||||
num=event.order_count or 0
|
||||
)
|
||||
) if user.is_superuser or event.pk in events_with_orders else ''
|
||||
),
|
||||
daterange=dr,
|
||||
status=status[1],
|
||||
statusclass=status[0],
|
||||
),
|
||||
'display_size': 'small',
|
||||
'priority': 100,
|
||||
'url': reverse('control:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
'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
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from django.db.models import Q
|
||||
import pytz
|
||||
from django.db.models import Max, Min, Q
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.http import JsonResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.control.utils.i18n import i18ncomp
|
||||
from pretix.helpers.daterange import daterange
|
||||
|
||||
|
||||
def event_list(request):
|
||||
@@ -10,19 +14,38 @@ def event_list(request):
|
||||
qs = request.user.get_events_with_any_permission().filter(
|
||||
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) |
|
||||
Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
|
||||
).order_by('-date_from')
|
||||
).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_from=Coalesce('min_from', 'date_from'),
|
||||
).order_by('-order_from')
|
||||
|
||||
def serialize(e):
|
||||
|
||||
dr = e.get_date_range_display()
|
||||
if e.has_subevents:
|
||||
tz = pytz.timezone(e.settings.timezone)
|
||||
dr = _('Series:') + ' ' + daterange(
|
||||
e.min_from.astimezone(tz),
|
||||
(e.max_fromto or e.max_to or e.max_from).astimezone(tz)
|
||||
)
|
||||
return {
|
||||
'slug': e.slug,
|
||||
'organizer': str(e.organizer.name),
|
||||
'name': str(e.name),
|
||||
'date_range': dr,
|
||||
'url': reverse('control:event.index', kwargs={
|
||||
'event': e.slug,
|
||||
'organizer': e.organizer.slug
|
||||
})
|
||||
}
|
||||
|
||||
doc = {
|
||||
'results': [
|
||||
{
|
||||
'slug': e.slug,
|
||||
'organizer': str(e.organizer.name),
|
||||
'name': str(e.name),
|
||||
'date_range': e.get_date_range_display(),
|
||||
'url': reverse('control:event.index', kwargs={
|
||||
'event': e.slug,
|
||||
'organizer': e.organizer.slug
|
||||
})
|
||||
} for e in qs.select_related('organizer')[:10]
|
||||
serialize(e) for e in qs.select_related('organizer')[:10]
|
||||
]
|
||||
}
|
||||
return JsonResponse(doc)
|
||||
|
||||
@@ -9,14 +9,14 @@ $(function () {
|
||||
|
||||
$("[data-event-typeahead]").each(function () {
|
||||
var $container = $(this);
|
||||
var $query = $(this).find('[data-typeahead-query]');
|
||||
$query.closest("li").nextAll().remove();
|
||||
var $query = $(this).find('[data-typeahead-query]').length ? $(this).find('[data-typeahead-query]') : $($(this).attr("data-typeahead-field"));
|
||||
$container.find("li:not(.query-holder)").remove();
|
||||
|
||||
$query.on("change", function () {
|
||||
$.getJSON(
|
||||
$container.attr("data-source") + "?query=" + encodeURIComponent($query.val()),
|
||||
function (data) {
|
||||
$query.closest("li").nextAll().remove();
|
||||
$container.find("li:not(.query-holder)").remove();
|
||||
$.each(data.results, function (i, res) {
|
||||
$container.append(
|
||||
$("<li>").append(
|
||||
@@ -36,6 +36,7 @@ $(function () {
|
||||
)
|
||||
);
|
||||
});
|
||||
$container.toggleClass('focused', $query.is(":focus") && $container.children().length > 0);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -50,9 +51,12 @@ $(function () {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
$query.on("blur", function (event) {
|
||||
$container.removeClass('focused');
|
||||
});
|
||||
$query.on("keyup", function (event) {
|
||||
var $first = $query.closest("li").next();
|
||||
var $last = $query.closest("li").nextAll().last();
|
||||
var $first = $container.find("li:not(.query-holder)").first();
|
||||
var $last = $container.find("li:not(.query-holder)").last();
|
||||
var $selected = $container.find(".active");
|
||||
|
||||
if (event.which === 13) { // enter
|
||||
@@ -60,10 +64,12 @@ $(function () {
|
||||
event.stopPropagation();
|
||||
return true;
|
||||
} else if (event.which === 40) { // down
|
||||
var $next;
|
||||
if ($selected.length === 0) {
|
||||
$selected = $query.closest("li");
|
||||
$next = $first;
|
||||
} else {
|
||||
$next = $selected.next();
|
||||
}
|
||||
var $next = $selected.next();
|
||||
if ($next.length === 0) {
|
||||
$next = $first;
|
||||
}
|
||||
@@ -74,7 +80,7 @@ $(function () {
|
||||
return true;
|
||||
} else if (event.which === 38) { // up
|
||||
if ($selected.length === 0) {
|
||||
$selected = $container.first();
|
||||
$selected = $first;
|
||||
}
|
||||
var $prev = $selected.prev();
|
||||
if ($prev.length === 0 || $prev.find("input").length > 0) {
|
||||
|
||||
@@ -2,33 +2,43 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.dashboard .widget-container {
|
||||
flex:1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
align-self: stretch;
|
||||
padding: 15px 5px;
|
||||
border: 5px solid white;
|
||||
min-height: 160px;
|
||||
background: #F8F8F8;
|
||||
}
|
||||
|
||||
.dashboard .widget-container.widget-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard .widget-container.widget-big {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.dashboard .widget-container.widget-small {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.dashboard-panels .panel-heading .fa {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.dashboard .widget-container:hover,.dashboard .widget-container:focus {
|
||||
|
||||
.dashboard .widget-container:hover, .dashboard .widget-container:focus {
|
||||
background: #EEEEEE;
|
||||
}
|
||||
.dashboard .widget:hover,.dashboard .widget:focus {
|
||||
|
||||
.dashboard .widget:hover, .dashboard .widget:focus, .dashboard a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dashboard .numwidget {
|
||||
.num {
|
||||
display: block;
|
||||
@@ -42,6 +52,7 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard .shopstate {
|
||||
text-align: center;
|
||||
padding: 18px 0;
|
||||
@@ -58,16 +69,61 @@
|
||||
color: $brand-danger;
|
||||
}
|
||||
}
|
||||
.dashboard .event {
|
||||
text-align: center;
|
||||
padding: 15px 30px;
|
||||
font-size: 20px;
|
||||
|
||||
span.from, span.to {
|
||||
.widget-container.widget-container-event {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.widget {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.event {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
padding: 15px 30px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bottomrow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
a {
|
||||
flex-grow: 1;
|
||||
flex-basis: 50%;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
a.orders {
|
||||
background: $navbar-inverse-bg;
|
||||
}
|
||||
a.status-success {
|
||||
background: $brand-success;
|
||||
}
|
||||
a.status-warning {
|
||||
background: $brand-warning;
|
||||
}
|
||||
a.status-default {
|
||||
background: $gray-light;
|
||||
}
|
||||
a.status-danger {
|
||||
background: $brand-danger;
|
||||
}
|
||||
}
|
||||
.daterange, .times {
|
||||
display: block;
|
||||
font-size: 25px;
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard .newevent {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
@@ -79,6 +135,7 @@
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard .welcome-wizard {
|
||||
padding: 5px 15px;
|
||||
h3 {
|
||||
@@ -92,11 +149,22 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
margin: 15px 0;
|
||||
|
||||
.focused.dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
.dashboard .widget-container.widget-small {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
.dashboard .widget-container.widget-small,
|
||||
.dashboard .widget-container.widget-big {
|
||||
|
||||
Reference in New Issue
Block a user