diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index ecb4a22b4d..7f8183b5fc 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -7,7 +7,8 @@ from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from pretix.base.models import ( - Checkin, Event, Invoice, Item, Order, OrderPosition, Organizer, SubEvent, + Checkin, Event, Invoice, Item, Order, OrderPosition, Organizer, Question, + QuestionAnswer, SubEvent, ) from pretix.base.signals import register_payment_providers from pretix.control.forms.widgets import Select2 @@ -99,6 +100,7 @@ class OrderFilterForm(FilterForm): ('p', _('Paid')), ('n', _('Pending')), ('o', _('Pending (overdue)')), + ('np', _('Pending or paid')), ('e', _('Expired')), ('ne', _('Pending or expired')), ('c', _('Canceled')), @@ -145,6 +147,8 @@ class OrderFilterForm(FilterForm): s = fdata.get('status') if s == 'o': qs = qs.filter(status=Order.STATUS_PENDING, expires__lt=now().replace(hour=0, minute=0, second=0)) + elif s == 'np': + qs = qs.filter(status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]) elif s == 'ne': qs = qs.filter(status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED]) else: @@ -175,11 +179,19 @@ class EventOrderFilterForm(OrderFilterForm): required=False, empty_label=pgettext_lazy('subevent', 'All dates') ) + question = forms.ModelChoiceField( + queryset=Question.objects.none(), + required=False, + ) + answer = forms.CharField( + required=False + ) def __init__(self, *args, **kwargs): self.event = kwargs.pop('event') super().__init__(*args, **kwargs) self.fields['item'].queryset = self.event.items.all() + self.fields['question'].queryset = self.event.questions.all() self.fields['provider'].choices += [(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()] @@ -209,6 +221,31 @@ class EventOrderFilterForm(OrderFilterForm): if fdata.get('subevent'): qs = qs.filter(positions__subevent=fdata.get('subevent')) + if fdata.get('question') and fdata.get('answer') is not None: + q = fdata.get('question') + + if q.type == Question.TYPE_FILE: + answers = QuestionAnswer.objects.filter( + orderposition__order_id=OuterRef('pk'), + question_id=q.pk, + file__isnull=False + ) + qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True) + elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE): + answers = QuestionAnswer.objects.filter( + question_id=q.pk, + orderposition__order_id=OuterRef('pk'), + options__pk=fdata.get('answer') + ) + qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True) + else: + answers = QuestionAnswer.objects.filter( + question_id=q.pk, + orderposition__order_id=OuterRef('pk'), + answer__iexact=fdata.get('answer') + ) + qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True) + return qs diff --git a/src/pretix/control/templates/pretixcontrol/items/question.html b/src/pretix/control/templates/pretixcontrol/items/question.html index c3095792c1..a27308f5ff 100644 --- a/src/pretix/control/templates/pretixcontrol/items/question.html +++ b/src/pretix/control/templates/pretixcontrol/items/question.html @@ -74,7 +74,9 @@ {% for stat in stats %} - {{ stat.answer }} + + {{ stat.answer }} + {{ stat.count }} diff --git a/src/pretix/control/templates/pretixcontrol/orders/index.html b/src/pretix/control/templates/pretixcontrol/orders/index.html index e8a023981c..d66334125b 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/index.html +++ b/src/pretix/control/templates/pretixcontrol/orders/index.html @@ -72,6 +72,18 @@ + {% if filter_form.is_valid and filter_form.cleaned_data.question %} +

+ + {% blocktrans trimmed with question=filter_form.cleaned_data.question.question %} + List filtered by answers to question "{{ question }}". + {% endblocktrans %} + + + {% trans "Remove filter" %} + +

+ {% endif %}
diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index fe51ae0cda..0dd4f4a171 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -416,13 +416,14 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV qs = [ { 'answer': ugettext('File uploaded'), - 'count': qs.filter(file__isnull=False).count() + 'count': qs.filter(file__isnull=False).count(), } ] elif self.object.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE): qs = qs.order_by('options').values('options', 'options__answer') \ .annotate(count=Count('id')).order_by('-count') for a in qs: + a['alink'] = a['options'] a['answer'] = str(a['options__answer']) del a['options__answer'] elif self.object.type in (Question.TYPE_TIME, Question.TYPE_DATE, Question.TYPE_DATETIME): @@ -430,12 +431,14 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV qs_model = qs qs = qs.values('answer').annotate(count=Count('id')).order_by('-count') for a, a_model in zip(qs, qs_model): + a['alink'] = a['answer'] a['answer'] = str(a_model) else: qs = qs.order_by('answer').values('answer').annotate(count=Count('id')).order_by('-count') if self.object.type == Question.TYPE_BOOLEAN: for a in qs: + a['alink'] = a['answer'] a['answer'] = ugettext('Yes') if a['answer'] == 'True' else ugettext('No') a['answer_bool'] = a['answer'] == 'True' diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py index a5cb9c8bff..b5a6694b81 100644 --- a/src/tests/control/test_orders.py +++ b/src/tests/control/test_orders.py @@ -9,8 +9,8 @@ from django_countries.fields import Country from tests.base import SoupTest from pretix.base.models import ( - Event, InvoiceAddress, Item, Order, OrderPosition, Organizer, Quota, Team, - User, + Event, InvoiceAddress, Item, Order, OrderPosition, Organizer, Question, + QuestionAnswer, Quota, Team, User, ) from pretix.base.services.invoices import ( generate_cancellation, generate_invoice, @@ -86,6 +86,25 @@ def test_order_list(client, env): response = client.get('/control/event/dummy/dummy/orders/?status=o') assert 'FOO' in response.rendered_content + q = Question.objects.create(event=env[0], question="Q", type="N", required=True) + q.items.add(env[3]) + op = env[2].positions.first() + qa = QuestionAnswer.objects.create(question=q, orderposition=op, answer="12") + response = client.get('/control/event/dummy/dummy/orders/?question=%d&answer=12' % q.pk) + assert 'FOO' in response.rendered_content + response = client.get('/control/event/dummy/dummy/orders/?question=%d&answer=13' % q.pk) + assert 'FOO' not in response.rendered_content + + q.type = "C" + q.save() + qo1 = q.options.create(answer="Foo") + qo2 = q.options.create(answer="Bar") + qa.options.add(qo1) + response = client.get('/control/event/dummy/dummy/orders/?question=%d&answer=%d' % (q.pk, qo1.pk)) + assert 'FOO' in response.rendered_content + response = client.get('/control/event/dummy/dummy/orders/?question=%d&answer=%d' % (q.pk, qo2.pk)) + assert 'FOO' not in response.rendered_content + @pytest.mark.django_db def test_order_detail(client, env):