From bb10d255617efd8c11941fcc71e7547ab7d29762 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 3 Apr 2018 12:10:34 +0200 Subject: [PATCH] Fix #782 -- Select2 widget for item selection for vouchers --- src/pretix/control/forms/vouchers.py | 76 ++++++++++-------- src/pretix/control/forms/widgets.py | 20 +++++ src/pretix/control/urls.py | 1 + src/pretix/control/views/typeahead.py | 79 +++++++++++++++++++ src/pretix/static/pretixcontrol/js/ui/main.js | 18 ++++- .../static/pretixcontrol/scss/main.scss | 5 +- 6 files changed, 163 insertions(+), 36 deletions(-) diff --git a/src/pretix/control/forms/vouchers.py b/src/pretix/control/forms/vouchers.py index 37120f7238..ddb847daba 100644 --- a/src/pretix/control/forms/vouchers.py +++ b/src/pretix/control/forms/vouchers.py @@ -1,20 +1,25 @@ import copy from django import forms -from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db.models.functions import Lower from django.urls import reverse from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from pretix.base.forms import I18nModelForm -from pretix.base.models import Item, ItemVariation, Quota, Voucher +from pretix.base.models import Item, Voucher from pretix.control.forms import SplitDateTimePickerWidget -from pretix.control.forms.widgets import Select2 +from pretix.control.forms.widgets import Select2, Select2ItemVarQuota from pretix.control.signals import voucher_form_validation +class FakeChoiceField(forms.ChoiceField): + def valid_value(self, value): + return True + + class VoucherForm(I18nModelForm): - itemvar = forms.ChoiceField( + itemvar = FakeChoiceField( label=_("Product"), help_text=_( "This product is added to the user's cart if the voucher is redeemed." @@ -72,43 +77,48 @@ class VoucherForm(I18nModelForm): del self.fields['subevent'] choices = [] - for i in self.instance.event.items.prefetch_related('variations').all(): - variations = list(i.variations.all()) - if variations: - choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name))) - for v in variations: - choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value))) - else: - choices.append((str(i.pk), i.name)) - for q in self.instance.event.quotas.all(): - choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q))) self.fields['itemvar'].choices = choices + self.fields['itemvar'].widget = Select2ItemVarQuota( + attrs={ + 'data-model-select2': 'generic', + 'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={ + 'event': instance.event.slug, + 'organizer': instance.event.organizer.slug, + }), + 'data-placeholder': '' + } + ) + self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices + self.fields['itemvar'].required = True def clean(self): data = super().clean() if not self._errors: - itemid = quotaid = None - iv = self.data.get('itemvar', '') - if iv.startswith('q-'): - quotaid = iv[2:] - elif '-' in iv: - itemid, varid = iv.split('-') - else: - itemid, varid = iv, None - - if itemid: - self.instance.item = Item.objects.get(pk=itemid, event=self.instance.event) - if varid: - self.instance.variation = ItemVariation.objects.get(pk=varid, item=self.instance.item) + try: + itemid = quotaid = None + iv = self.data.get('itemvar', '') + if iv.startswith('q-'): + quotaid = iv[2:] + elif '-' in iv: + itemid, varid = iv.split('-') else: - self.instance.variation = None - self.instance.quota = None + itemid, varid = iv, None - else: - self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event) - self.instance.item = None - self.instance.variation = None + if itemid: + self.instance.item = self.instance.event.items.get(pk=itemid) + if varid: + self.instance.variation = self.instance.item.variations.get(pk=varid) + else: + self.instance.variation = None + self.instance.quota = None + + else: + self.instance.quota = self.instance.event.quotas.get(pk=quotaid) + self.instance.item = None + self.instance.variation = None + except ObjectDoesNotExist: + raise ValidationError(_("Invalid product selected.")) if 'codes' in data: data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a] diff --git a/src/pretix/control/forms/widgets.py b/src/pretix/control/forms/widgets.py index 6f01ec91bc..924c261e04 100644 --- a/src/pretix/control/forms/widgets.py +++ b/src/pretix/control/forms/widgets.py @@ -36,3 +36,23 @@ class Select2(Select2Mixin, forms.Select): class Select2Multiple(Select2Mixin, forms.SelectMultiple): pass + + +class Select2ItemVarQuotaMixin(Select2Mixin): + + def options(self, name, value, attrs=None): + if value and value[0]: + yield self.create_option( + None, + value[0], + value[0], + True, + 0, + subindex=None, + attrs=attrs + ) + return + + +class Select2ItemVarQuota(Select2ItemVarQuotaMixin, forms.Select): + pass diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index aaee886c9f..55ca7c3f2e 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -142,6 +142,7 @@ urlpatterns = [ url(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'), url(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'), url(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'), + url(r'^vouchers/item_select$', typeahead.itemvarquota_select2, name='event.vouchers.itemselect2'), url(r'^vouchers/(?P\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'), url(r'^vouchers/(?P\d+)/delete$', vouchers.VoucherDelete.as_view(), name='event.voucher.delete'), diff --git a/src/pretix/control/views/typeahead.py b/src/pretix/control/views/typeahead.py index 069816863d..4f61c36a7c 100644 --- a/src/pretix/control/views/typeahead.py +++ b/src/pretix/control/views/typeahead.py @@ -158,6 +158,85 @@ def checkinlist_select2(request, **kwargs): return JsonResponse(doc) +@event_permission_required(None) +def itemvarquota_select2(request, **kwargs): + query = request.GET.get('query', '') + try: + page = int(request.GET.get('page', '1')) + except ValueError: + page = 1 + + choices = [] + + if not request.event.has_subevents: + # We are very unlikely to need pagination + itemqs = request.event.items.prefetch_related('variations').filter(name__icontains=i18ncomp(query)) + quotaqs = request.event.quotas.filter(name__icontains=query) + more = False + else: + # We can't do proper pagination on a UNION-like query, so we hack it. + if query: + # Don't paginate + quotaf = Q(name__icontains=query) + try: + dt = parse(query) + except ValueError: + pass + else: + tz = request.event.timezone + if dt and request.event.has_subevents: + dt_start = make_aware(datetime.combine(dt.date(), time(hour=0, minute=0, second=0)), tz) + dt_end = make_aware(datetime.combine(dt.date(), time(hour=23, minute=59, second=59)), tz) + quotaf |= Q(subevent__date_from__gte=dt_start) & Q(subevent__date_from__lte=dt_end) + + itemqs = request.event.items.prefetch_related('variations').filter(name__icontains=i18ncomp(query)) + quotaqs = request.event.quotas.filter(quotaf).select_related('subevent') + more = False + else: + if page == 1: + itemqs = request.event.items.prefetch_related('variations').filter(name__icontains=i18ncomp(query)) + else: + itemqs = request.event.items.none() + quotaqs = request.event.quotas.filter(name__icontains=query).select_related('subevent') + total = quotaqs.count() + pagesize = 20 + offset = (page - 1) * pagesize + quotaqs = quotaqs[offset:offset + pagesize] + more = total >= (offset + pagesize) + + for i in itemqs: + variations = list(i.variations.all()) + if variations: + choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name), '')) + for v in variations: + choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value), '')) + else: + choices.append((str(i.pk), i.name, '')) + for q in quotaqs: + if request.event.has_subevents: + choices.append(('q-%d' % q.pk, + _('Any product in quota "{quota}"').format( + quota=q + ), str(q.subevent))) + else: + choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q), '')) + + doc = { + 'results': [ + { + 'id': k, + 'text': str(v), + 'event': str(t), + } + for k, v, t in choices + ], + 'pagination': { + "more": more + } + } + return JsonResponse(doc) + + def organizer_select2(request): term = request.GET.get('query', '') try: diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index 9e52446348..fb84be5fa2 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -281,7 +281,23 @@ var form_handlers = function (el) { page: params.page || 1 } } - } + }, + templateResult: function (res) { + if (!res.id) { + return res.text; + } + var $ret = $("").append( + $("").addClass("primary").append($("
").text(res.text).html()) + ); + if (res.event) { + $ret.append( + $("").addClass("secondary").append( + $("").addClass("fa fa-calendar fa-fw") + ).append(" ").append($("
").text(res.event).html()) + ); + } + return $ret; + }, }).on("select2:select", function () { // Allow continuing to select if ($s.hasAttribute("multiple")) { diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss index 76c9aba944..548451d477 100644 --- a/src/pretix/static/pretixcontrol/scss/main.scss +++ b/src/pretix/static/pretixcontrol/scss/main.scss @@ -322,16 +322,17 @@ body.loading #wrapper { width: 300px; } .event-dropdown, .mobile-event-dropdown, .select2-results { - .event-name-full { + .event-name-full, .primary { display: block; } - .event-daterange, .event-organizer { + .event-daterange, .event-organizer, .secondary { display: block; font-size: $font-size-small; color: $text-muted; } .active .event-daterange, .active .event-organizer, .active a, .select2-results__option--highlighted .event-daterange, + .select2-results__option--highlighted .secondary, .select2-results__option--highlighted .event-organizer { color: white; }