Use Select2 for subevent and other long selections (#763)

* Use Select2 for subevent and other long selections

* Minor correction
This commit is contained in:
Raphael Michel
2018-01-26 16:47:33 +01:00
committed by GitHub
parent 1ee6e31538
commit e12caf186c
82 changed files with 8401 additions and 729 deletions

View File

@@ -3,6 +3,7 @@ from importlib import import_module
from django.conf import settings
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
from django.utils.translation import get_language
from pretix.base.settings import GlobalSettingsObject
@@ -57,6 +58,10 @@ def contextprocessor(request):
ctx['new_session'] = child_sess
request.session['event_access'] = True
if 'subevent' in request.GET:
# Do not use .get() for lazy evaluation
ctx['selected_subevents'] = request.event.subevents.filter(pk=request.GET.get('subevent'))
ctx['nav_event'] = _nav_event
ctx['js_payment_weekdays_disabled'] = _js_payment_weekdays_disabled
@@ -77,6 +82,7 @@ def contextprocessor(request):
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
ctx['js_time_format'] = get_javascript_format('TIME_INPUT_FORMATS')
ctx['js_locale'] = get_moment_locale()
ctx['select2locale'] = get_language()[:2]
if settings.DEBUG and 'runserver' not in sys.argv:
ctx['debug_warning'] = True

View File

@@ -1,6 +1,9 @@
from django import forms
from django.urls import reverse
from django.utils.translation import pgettext_lazy
from pretix.base.models.checkin import CheckinList
from pretix.control.forms.widgets import Select2
class CheckinListForm(forms.ModelForm):
@@ -11,6 +14,17 @@ class CheckinListForm(forms.ModelForm):
self.fields['limit_products'].queryset = self.event.items.all()
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
self.fields['subevent'].required = True
else:
del self.fields['subevent']

View File

@@ -2,6 +2,7 @@ from django import forms
from django.apps import apps
from django.db.models import Exists, F, OuterRef, Q
from django.db.models.functions import Coalesce
from django.urls import reverse, reverse_lazy
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -9,6 +10,7 @@ from pretix.base.models import (
Event, Invoice, Item, Order, OrderPosition, Organizer, SubEvent,
)
from pretix.base.signals import register_payment_providers
from pretix.control.forms.widgets import Select2
from pretix.helpers.database import FixedOrderBy, rolledback_transaction
from pretix.helpers.i18n import i18ncomp
@@ -182,6 +184,17 @@ class EventOrderFilterForm(OrderFilterForm):
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'All dates')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
elif 'subevent':
del self.fields['subevent']
@@ -207,7 +220,14 @@ class OrderSearchFilterForm(OrderFilterForm):
label=_('Organizer'),
queryset=Organizer.objects.none(),
required=False,
empty_label=_('All organizers')
empty_label=_('All organizers'),
widget=Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse_lazy('control:organizers.select2'),
'data-placeholder': _('All organizers')
}
)
)
def __init__(self, *args, **kwargs):
@@ -351,7 +371,14 @@ class EventFilterForm(FilterForm):
label=_('Organizer'),
queryset=Organizer.objects.none(),
required=False,
empty_label=_('All organizers')
empty_label=_('All organizers'),
widget=Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse_lazy('control:organizers.select2'),
'data-placeholder': _('All organizers')
}
)
)
query = forms.CharField(
label=_('Event name'),

View File

@@ -4,7 +4,10 @@ from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Max
from django.forms.formsets import DELETION_FIELD_NAME
from django.utils.translation import ugettext as __, ugettext_lazy as _
from django.urls import reverse
from django.utils.translation import (
pgettext_lazy, ugettext as __, ugettext_lazy as _,
)
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.forms import I18nFormSet, I18nModelForm
@@ -13,6 +16,7 @@ from pretix.base.models import (
)
from pretix.base.models.items import ItemAddOn
from pretix.control.forms import SplitDateTimePickerWidget
from pretix.control.forms.widgets import Select2
class CategoryForm(I18nModelForm):
@@ -95,6 +99,18 @@ class QuotaForm(I18nModelForm):
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
self.fields['subevent'].required = True
else:
del self.fields['subevent']

View File

@@ -0,0 +1,38 @@
from django import forms
class Select2Mixin:
template_name = 'pretixcontrol/select2_widget.html'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def options(self, name, value, attrs=None):
if value and value[0]:
for i, selected in enumerate(self.choices.queryset.filter(pk__in=value)):
yield self.create_option(
None,
self.choices.field.prepare_value(selected),
self.choices.field.label_from_instance(selected),
True,
i,
subindex=None,
attrs=attrs
)
return
def optgroups(self, name, value, attrs=None):
if value:
return [
(None, [c], i)
for i, c in enumerate(self.options(name, value, attrs))
]
return
class Select2(Select2Mixin, forms.Select):
pass
class Select2Multiple(Select2Mixin, forms.SelectMultiple):
pass

View File

@@ -23,6 +23,9 @@
<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.js" %}"></script>
<script type="text/javascript" src="{% static "moment/moment-with-locales.js" %}"></script>
<script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
<script type="text/javascript" src="{% static "select2/select2.js" %}"></script>
<script type="text/javascript" src="{% static "select2/i18n/de.js" %}"></script>
<script type="text/javascript" src="{% static "select2/i18n/en.js" %}"></script>
<script type="text/javascript" src="{% static "charts/raphael-min.js" %}"></script>
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
@@ -47,7 +50,7 @@
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %}
</head>
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}">
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}" data-select2-locale="{{ select2locale }}">
<div id="wrapper">
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="navbar-header">

View File

@@ -21,20 +21,9 @@
</p>
{% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get">
<p>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %}
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
<form class="form-inline helper-display-inline" action="" method="get">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
</form>
</form>
{% endif %}
{% if checkinlists|length == 0 %}

View File

@@ -1,13 +1,12 @@
{% load i18n %}
<p>
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
<select name="subevent" class="form-control simple-subevent-choice" data-model-select2="event"
data-select2-url="{% url "control:event.subevents.select2" organizer=request.event.organizer.slug event=request.event.slug %}"
data-placeholder="{% trans "All dates" context "subevent" %}">
{% for se in selected_subevents %}
<option value="{{ se.pk }}" selected>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Show" %}</button>
</p>

View File

@@ -14,20 +14,7 @@
</p>
{% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get">
<p>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %}
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
</form>
{% endif %}
{% if quotas|length == 0 %}

View File

@@ -44,17 +44,20 @@
<div class="col-md-1 col-xs-6">
{% bootstrap_field filter_form.item layout='inline' %}
</div>
<div class="col-md-1 col-xs-6">
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.subevent layout='inline' %}
</div>
<div class="col-md-1 col-xs-6">
{% bootstrap_field filter_form.provider layout='inline' %}
</div>
{% else %}
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.item layout='inline' %}
</div>
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.provider layout='inline' %}
</div>
{% endif %}
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.provider layout='inline' %}
</div>
<div class="col-md-2 col-xs-6">
{% bootstrap_field filter_form.query layout='inline' %}
</div>

View File

@@ -0,0 +1,11 @@
{% load i18n %}
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}
</select>
<noscript>
<div class="alert alert-danger">
{% trans "Please enable JavaScript in your browser." %}
</div>
</noscript>

View File

@@ -17,6 +17,7 @@ urlpatterns = [
url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
url(r'^users/select2$', typeahead.users_select2, name='users.select2'),
url(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
url(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
url(r'^settings/notifications/$', user.UserNotificationsEditView.as_view(), name='user.settings.notifications'),
@@ -36,6 +37,7 @@ urlpatterns = [
name='user.settings.2fa.delete'),
url(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
url(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
@@ -80,6 +82,7 @@ urlpatterns = [
url(r'^settings/tax/(?P<rule>\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'),
url(r'^settings/widget$', event.WidgetSettings.as_view(), name='event.settings.widget'),
url(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
url(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
url(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'),
url(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
name='event.subevent.delete'),

View File

@@ -1,16 +1,23 @@
import pytz
from django.core.exceptions import PermissionDenied
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.base.models import Organizer, User
from pretix.control.permissions import event_permission_required
from pretix.helpers.daterange import daterange
from pretix.helpers.i18n import i18ncomp
def event_list(request):
query = request.GET.get('query', '')
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
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)
@@ -33,9 +40,11 @@ def event_list(request):
(e.max_fromto or e.max_to or e.max_from).astimezone(tz)
)
return {
'id': e.pk,
'slug': e.slug,
'organizer': str(e.organizer.name),
'name': str(e.name),
'text': str(e.name),
'date_range': dr,
'url': reverse('control:event.index', kwargs={
'event': e.slug,
@@ -43,9 +52,110 @@ def event_list(request):
})
}
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
doc = {
'results': [
serialize(e) for e in qs.select_related('organizer')[:10]
]
serialize(e) for e in qs.select_related('organizer')[offset:offset + pagesize]
],
'pagination': {
"more": total >= (offset + pagesize)
}
}
return JsonResponse(doc)
@event_permission_required(None)
def subevent_select2(request, **kwargs):
query = request.GET.get('query', '')
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
qs = request.event.subevents.filter(
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
).order_by('-date_from')
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
doc = {
'results': [
{
'id': e.pk,
'name': str(e.name),
'date_range': e.get_date_range_display(),
'text': '{} {}'.format(e.name, e.get_date_range_display()),
}
for e in qs[offset:offset + pagesize]
],
'pagination': {
"more": total >= (offset + pagesize)
}
}
return JsonResponse(doc)
def organizer_select2(request):
term = request.GET.get('query', '')
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
qs = Organizer.objects.all()
if term:
qs = qs.filter(Q(name__icontains=term) | Q(slug__icontains=term))
if not request.user.is_superuser:
qs = qs.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
doc = {
"results": [
{
'id': o.pk,
'text': str(o.name)
} for o in qs[offset:offset + pagesize]
],
"pagination": {
"more": total >= (offset + pagesize)
}
}
return JsonResponse(doc)
def users_select2(request):
if not request.user.is_superuser:
raise PermissionDenied()
term = request.GET.get('query', '')
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
qs = User.objects.all()
if term:
qs = qs.filter(Q(email__icontains=term) | Q(fullname__icontains=term))
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
doc = {
"results": [
{
'id': o.pk,
'text': str(o.email)
} for o in qs[offset:offset + pagesize]
],
"pagination": {
"more": total >= (offset + pagesize)
}
}
return JsonResponse(doc)