From 2e89fc0a947af0ecf5acfbe2203c5d8d1a5fa97a Mon Sep 17 00:00:00 2001 From: Lukas Bockstaller Date: Mon, 15 Dec 2025 12:46:06 +0100 Subject: [PATCH] Questions: filter answers by dateFrame (Z#23216406) (#5706) * replace manual form with QuestionFilterForm * move form to form/item.py * filter using a dateFrameField * rename QuestionFilterForm to QuestionAnswerFilterForm Co-authored-by: Richard Schreiber * pass existing `opqs` into `filter_qs` Co-authored-by: Richard Schreiber * clean up filters * fix view errors * add labels * display validation failures on field/label * fix linting issues * adjust datetime comparisons from lte to lt & gte to gt * Change filter-form layout similar to order-filter-form * improve label texts Co-authored-by: Richard Schreiber * use order constants Co-authored-by: Richard Schreiber * use Order Constants in Form where possible * Change phrasing from Subevent to Date Co-authored-by: Richard Schreiber * include product variations in products filter * repair time zone comparisons * fix linting * move filter form to form/filter.py * remove references to timezone.utc Co-authored-by: Richard Schreiber * remove manual class statements Co-authored-by: Richard Schreiber * removes unnecessary check Co-authored-by: Richard Schreiber * fix datetime comparison * Add full stop to error message to match style * unify var-names and code-indent --------- Co-authored-by: Richard Schreiber Co-authored-by: Richard Schreiber --- src/pretix/control/forms/filter.py | 127 ++++++++++++++++++ .../pretixcontrol/items/question.html | 39 ++---- src/pretix/control/views/item.py | 43 ++---- 3 files changed, 152 insertions(+), 57 deletions(-) diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index 6e330bb8fc..0f3b2a1e44 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -61,6 +61,10 @@ from pretix.base.models import ( SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher, ) from pretix.base.signals import register_payment_providers +from pretix.base.timeframes import ( + DateFrameField, + resolve_timeframe_to_datetime_start_inclusive_end_exclusive, +) from pretix.control.forms import SplitDateTimeField from pretix.control.forms.widgets import Select2, Select2ItemVarQuota from pretix.control.signals import order_search_filter_q @@ -1219,6 +1223,129 @@ class OrderPaymentSearchFilterForm(forms.Form): return qs +class QuestionAnswerFilterForm(forms.Form): + STATUS_VARIANTS = [ + ("", _("All orders")), + (Order.STATUS_PAID, _("Paid")), + (Order.STATUS_PAID + 'v', _("Paid or confirmed")), + (Order.STATUS_PENDING, _("Pending")), + (Order.STATUS_PENDING + Order.STATUS_PAID, _("Pending or paid")), + ("o", _("Pending (overdue)")), + (Order.STATUS_EXPIRED, _("Expired")), + (Order.STATUS_PENDING + Order.STATUS_EXPIRED, _("Pending or expired")), + (Order.STATUS_CANCELED, _("Canceled")) + ] + + status = forms.ChoiceField( + choices=STATUS_VARIANTS, + required=False, + label=_("Order status"), + ) + item = forms.ChoiceField( + choices=[], + required=False, + label=_("Products"), + ) + subevent = forms.ModelChoiceField( + queryset=SubEvent.objects.none(), + required=False, + empty_label=pgettext_lazy('subevent', 'All dates'), + label=pgettext_lazy("subevent", "Date"), + ) + date_range = DateFrameField( + required=False, + include_future_frames=True, + label=_('Event date'), + ) + + def __init__(self, *args, **kwargs): + self.event = kwargs.pop('event') + super().__init__(*args, **kwargs) + self.initial['status'] = Order.STATUS_PENDING + Order.STATUS_PAID + + choices = [('', _('All products'))] + for i in self.event.items.prefetch_related('variations').all(): + variations = list(i.variations.all()) + if variations: + choices.append((str(i.pk), _('{product} – Any variation').format(product=str(i)))) + for v in variations: + choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value))) + else: + choices.append((str(i.pk), str(i))) + self.fields['item'].choices = choices + + 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 + else: + del self.fields['subevent'] + + def clean(self): + cleaned_data = super().clean() + subevent = cleaned_data.get('subevent') + date_range = cleaned_data.get('date_range') + + if subevent is not None and date_range is not None: + d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone) + if ( + (d_start and not (d_start <= subevent.date_from)) or + (d_end and not (subevent.date_from < d_end)) + ): + self.add_error('subevent', pgettext_lazy('subevent', "Date doesn't start in selected date range.")) + return cleaned_data + + def filter_qs(self, opqs): + fdata = self.cleaned_data + + subevent = fdata.get('subevent', None) + date_range = fdata.get('date_range', None) + + if subevent is not None: + opqs = opqs.filter(subevent=subevent) + + if date_range is not None: + d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone) + opqs = opqs.filter( + subevent__date_from__gte=d_start, + subevent__date_from__lt=d_end + ) + + s = fdata.get("status", Order.STATUS_PENDING + Order.STATUS_PAID) + if s != "": + if s == Order.STATUS_PENDING: + opqs = opqs.filter(order__status=Order.STATUS_PENDING, + order__expires__lt=now().replace(hour=0, minute=0, second=0)) + elif s == Order.STATUS_PENDING + Order.STATUS_PAID: + opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]) + elif s == Order.STATUS_PAID + 'v': + opqs = opqs.filter( + Q(order__status=Order.STATUS_PAID) | + Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True) + ) + elif s == Order.STATUS_PENDING + Order.STATUS_EXPIRED: + opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED]) + else: + opqs = opqs.filter(order__status=s) + + if s not in (Order.STATUS_CANCELED, ""): + opqs = opqs.filter(canceled=False) + if fdata.get("item", "") != "": + i = fdata.get("item", "") + opqs = opqs.filter(item_id__in=(i,)) + + return opqs + + class SubEventFilterForm(FilterForm): orders = { 'date_from': 'date_from', diff --git a/src/pretix/control/templates/pretixcontrol/items/question.html b/src/pretix/control/templates/pretixcontrol/items/question.html index 168c597449..3be5f25a69 100644 --- a/src/pretix/control/templates/pretixcontrol/items/question.html +++ b/src/pretix/control/templates/pretixcontrol/items/question.html @@ -20,35 +20,20 @@
-
- +
+ {% bootstrap_field form.status %} +
+
+ {% bootstrap_field form.item %} +
+ {% if has_subevents %} +
+ {% bootstrap_field form.subevent %}
-
- +
+ {% bootstrap_field form.date_range %}
- {% if request.event.has_subevents %} -
- {% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %} -
- {% endif %} + {% endif %}