Better dashboard layout

This commit is contained in:
Raphael Michel
2017-07-27 12:27:35 +02:00
parent 3415bf5cd3
commit 23ecd43885
6 changed files with 252 additions and 43 deletions

View File

@@ -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" %}"

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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 {