mirror of
https://github.com/pretix/pretix.git
synced 2026-05-09 15:54:03 +00:00
Add advanced search to order list
This commit is contained in:
@@ -1,16 +1,22 @@
|
|||||||
from datetime import datetime, time
|
from datetime import datetime, time
|
||||||
|
from decimal import Decimal
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import Exists, F, OuterRef, Q
|
from django.conf import settings
|
||||||
|
from django.db.models import Exists, F, Model, OuterRef, Q, QuerySet
|
||||||
from django.db.models.functions import Coalesce, ExtractWeekDay
|
from django.db.models.functions import Coalesce, ExtractWeekDay
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
|
from django.utils.formats import date_format, localize
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.forms.widgets import DatePickerWidget
|
from pretix.base.channels import get_all_sales_channels
|
||||||
|
from pretix.base.forms.widgets import (
|
||||||
|
DatePickerWidget, SplitDateTimePickerWidget,
|
||||||
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, Event, EventMetaProperty, EventMetaValue, Invoice, InvoiceAddress,
|
Checkin, Event, EventMetaProperty, EventMetaValue, Invoice, InvoiceAddress,
|
||||||
Item, Order, OrderPayment, OrderPosition, OrderRefund, Organizer, Question,
|
Item, Order, OrderPayment, OrderPosition, OrderRefund, Organizer, Question,
|
||||||
@@ -19,7 +25,9 @@ from pretix.base.models import (
|
|||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
from pretix.control.signals import order_search_filter_q
|
from pretix.control.signals import order_search_filter_q
|
||||||
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.helpers.database import FixedOrderBy, rolledback_transaction
|
from pretix.helpers.database import FixedOrderBy, rolledback_transaction
|
||||||
|
from pretix.helpers.dicts import move_to_end
|
||||||
from pretix.helpers.i18n import i18ncomp
|
from pretix.helpers.i18n import i18ncomp
|
||||||
|
|
||||||
PAYMENT_PROVIDERS = []
|
PAYMENT_PROVIDERS = []
|
||||||
@@ -83,6 +91,38 @@ class FilterForm(forms.Form):
|
|||||||
else:
|
else:
|
||||||
return self.orders[o]
|
return self.orders[o]
|
||||||
|
|
||||||
|
def filter_to_strings(self):
|
||||||
|
string = []
|
||||||
|
for k, f in self.fields.items():
|
||||||
|
v = self.cleaned_data.get(k)
|
||||||
|
if v is None or (isinstance(v, (list, str, QuerySet)) and len(v) == 0):
|
||||||
|
continue
|
||||||
|
if k == "saveas":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(v, bool):
|
||||||
|
val = _('Yes') if v else _('No')
|
||||||
|
elif isinstance(v, QuerySet):
|
||||||
|
q = ['"' + str(m) + '"' for m in v]
|
||||||
|
if not q:
|
||||||
|
continue
|
||||||
|
val = ' or '.join(q)
|
||||||
|
elif isinstance(v, Model):
|
||||||
|
val = '"' + str(v) + '"'
|
||||||
|
elif isinstance(f, forms.MultipleChoiceField):
|
||||||
|
valdict = dict(f.choices)
|
||||||
|
val = ' or '.join([str(valdict.get(m)) for m in v])
|
||||||
|
elif isinstance(f, forms.ChoiceField):
|
||||||
|
val = str(dict(f.choices).get(v))
|
||||||
|
elif isinstance(v, datetime):
|
||||||
|
val = date_format(v, 'SHORT_DATETIME_FORMAT')
|
||||||
|
elif isinstance(v, Decimal):
|
||||||
|
val = localize(v)
|
||||||
|
else:
|
||||||
|
val = v
|
||||||
|
string.append('{}: {}'.format(f.label, val))
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
class OrderFilterForm(FilterForm):
|
class OrderFilterForm(FilterForm):
|
||||||
query = forms.CharField(
|
query = forms.CharField(
|
||||||
@@ -104,21 +144,29 @@ class OrderFilterForm(FilterForm):
|
|||||||
label=_('Order status'),
|
label=_('Order status'),
|
||||||
choices=(
|
choices=(
|
||||||
('', _('All orders')),
|
('', _('All orders')),
|
||||||
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
(_('Valid orders'), (
|
||||||
(Order.STATUS_PENDING, _('Pending')),
|
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
||||||
('o', _('Pending (overdue)')),
|
(Order.STATUS_PENDING, _('Pending')),
|
||||||
(Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')),
|
(Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')),
|
||||||
(Order.STATUS_EXPIRED, _('Expired')),
|
)),
|
||||||
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
|
(_('Cancellations'), (
|
||||||
(Order.STATUS_CANCELED, _('Canceled')),
|
(Order.STATUS_CANCELED, _('Canceled')),
|
||||||
('cp', _('Canceled (or with paid fee)')),
|
('cp', _('Canceled (or with paid fee)')),
|
||||||
('na', _('Approved, payment pending')),
|
('rc', _('Cancellation requested')),
|
||||||
('pa', _('Approval pending')),
|
)),
|
||||||
('overpaid', _('Overpaid')),
|
(_('Payment process'), (
|
||||||
('underpaid', _('Underpaid')),
|
(Order.STATUS_EXPIRED, _('Expired')),
|
||||||
('pendingpaid', _('Pending (but fully paid)')),
|
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
|
||||||
|
('o', _('Pending (overdue)')),
|
||||||
|
('overpaid', _('Overpaid')),
|
||||||
|
('underpaid', _('Underpaid')),
|
||||||
|
('pendingpaid', _('Pending (but fully paid)')),
|
||||||
|
)),
|
||||||
|
(_('Approval process'), (
|
||||||
|
('na', _('Approved, payment pending')),
|
||||||
|
('pa', _('Approval pending')),
|
||||||
|
)),
|
||||||
('testmode', _('Test mode')),
|
('testmode', _('Test mode')),
|
||||||
('rc', _('Cancellation requested')),
|
|
||||||
),
|
),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
@@ -343,6 +391,237 @@ class EventOrderFilterForm(OrderFilterForm):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class FilterNullBooleanSelect(forms.NullBooleanSelect):
|
||||||
|
def __init__(self, attrs=None):
|
||||||
|
choices = (
|
||||||
|
('unknown', _('All')),
|
||||||
|
('true', _('Yes')),
|
||||||
|
('false', _('No')),
|
||||||
|
)
|
||||||
|
super(forms.NullBooleanSelect, self).__init__(attrs, choices)
|
||||||
|
|
||||||
|
|
||||||
|
class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||||
|
subevents_from = forms.SplitDateTimeField(
|
||||||
|
widget=SplitDateTimePickerWidget(attrs={
|
||||||
|
}),
|
||||||
|
label=pgettext_lazy('subevent', 'All dates starting at or after'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
subevents_to = forms.SplitDateTimeField(
|
||||||
|
widget=SplitDateTimePickerWidget(attrs={
|
||||||
|
}),
|
||||||
|
label=pgettext_lazy('subevent', 'All dates starting before'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
created_from = forms.SplitDateTimeField(
|
||||||
|
widget=SplitDateTimePickerWidget(attrs={
|
||||||
|
}),
|
||||||
|
label=_('Order placed at or after'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
created_to = forms.SplitDateTimeField(
|
||||||
|
widget=SplitDateTimePickerWidget(attrs={
|
||||||
|
}),
|
||||||
|
label=_('Order placed before'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
email = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('E-mail address')
|
||||||
|
)
|
||||||
|
comment = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('Comment')
|
||||||
|
)
|
||||||
|
locale = forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
label=_('Locale'),
|
||||||
|
choices=settings.LANGUAGES
|
||||||
|
)
|
||||||
|
email_known_to_work = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=FilterNullBooleanSelect,
|
||||||
|
label=_('E-mail address verified'),
|
||||||
|
)
|
||||||
|
total = forms.DecimalField(
|
||||||
|
localize=True,
|
||||||
|
required=False,
|
||||||
|
label=_('Total amount'),
|
||||||
|
)
|
||||||
|
sales_channel = forms.ChoiceField(
|
||||||
|
label=_('Sales channel'),
|
||||||
|
required=False,
|
||||||
|
choices=[('', '')] + [
|
||||||
|
(k, v.verbose_name) for k, v in get_all_sales_channels().items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
del self.fields['query']
|
||||||
|
del self.fields['question']
|
||||||
|
del self.fields['answer']
|
||||||
|
del self.fields['ordering']
|
||||||
|
if not self.event.has_subevents:
|
||||||
|
del self.fields['subevents_from']
|
||||||
|
del self.fields['subevents_to']
|
||||||
|
|
||||||
|
locale_names = dict(settings.LANGUAGES)
|
||||||
|
self.fields['locale'].choices = [('', '')] + [(a, locale_names[a]) for a in self.event.settings.locales]
|
||||||
|
|
||||||
|
move_to_end(self.fields, 'item')
|
||||||
|
move_to_end(self.fields, 'provider')
|
||||||
|
|
||||||
|
self.fields['invoice_address_company'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Invoice address') + ': ' + gettext('Company')
|
||||||
|
)
|
||||||
|
self.fields['invoice_address_name'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Invoice address') + ': ' + gettext('Name')
|
||||||
|
)
|
||||||
|
self.fields['invoice_address_street'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Invoice address') + ': ' + gettext('Address')
|
||||||
|
)
|
||||||
|
self.fields['invoice_address_zipcode'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Invoice address') + ': ' + gettext('ZIP code'),
|
||||||
|
help_text=_('Exact matches only')
|
||||||
|
)
|
||||||
|
self.fields['invoice_address_city'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Invoice address') + ': ' + gettext('City'),
|
||||||
|
help_text=_('Exact matches only')
|
||||||
|
)
|
||||||
|
self.fields['invoice_address_country'] = forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Invoice address') + ': ' + gettext('Country'),
|
||||||
|
choices=[('', '')] + list(CachedCountries())
|
||||||
|
)
|
||||||
|
self.fields['attendee_name'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('Attendee name')
|
||||||
|
)
|
||||||
|
self.fields['attendee_email'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('Attendee e-mail address')
|
||||||
|
)
|
||||||
|
self.fields['attendee_address_company'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Attendee address') + ': ' + gettext('Company')
|
||||||
|
)
|
||||||
|
self.fields['attendee_address_street'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Attendee address') + ': ' + gettext('Address')
|
||||||
|
)
|
||||||
|
self.fields['attendee_address_zipcode'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Attendee address') + ': ' + gettext('ZIP code'),
|
||||||
|
help_text=_('Exact matches only')
|
||||||
|
)
|
||||||
|
self.fields['attendee_address_city'] = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Attendee address') + ': ' + gettext('City'),
|
||||||
|
help_text=_('Exact matches only')
|
||||||
|
)
|
||||||
|
self.fields['attendee_address_country'] = forms.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
label=gettext('Attendee address') + ': ' + gettext('Country'),
|
||||||
|
choices=[('', '')] + list(CachedCountries())
|
||||||
|
)
|
||||||
|
self.fields['ticket_secret'] = forms.CharField(
|
||||||
|
label=_('Ticket secret'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
for q in self.event.questions.all():
|
||||||
|
self.fields['question_{}'.format(q.pk)] = forms.CharField(
|
||||||
|
label=q.question,
|
||||||
|
required=False,
|
||||||
|
help_text=_('Exact matches only')
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_qs(self, qs):
|
||||||
|
fdata = self.cleaned_data
|
||||||
|
qs = super().filter_qs(qs)
|
||||||
|
|
||||||
|
if fdata.get('subevents_from'):
|
||||||
|
qs = qs.filter(
|
||||||
|
all_positions__subevent__date_from__gte=fdata.get('subevents_from'),
|
||||||
|
all_positions__canceled=False
|
||||||
|
).distinct()
|
||||||
|
if fdata.get('subevents_to'):
|
||||||
|
qs = qs.filter(
|
||||||
|
all_positions__subevent__date_from__lt=fdata.get('subevents_to'),
|
||||||
|
all_positions__canceled=False
|
||||||
|
).distinct()
|
||||||
|
if fdata.get('email'):
|
||||||
|
qs = qs.filter(
|
||||||
|
email__icontains=fdata.get('email')
|
||||||
|
)
|
||||||
|
if fdata.get('created_from'):
|
||||||
|
qs = qs.filter(datetime__gte=fdata.get('created_from'))
|
||||||
|
if fdata.get('created_to'):
|
||||||
|
qs = qs.filter(datetime__gte=fdata.get('created_to'))
|
||||||
|
if fdata.get('comment'):
|
||||||
|
qs = qs.filter(comment__icontains=fdata.get('comment'))
|
||||||
|
if fdata.get('sales_channel'):
|
||||||
|
qs = qs.filter(sales_channel=fdata.get('sales_channel'))
|
||||||
|
if fdata.get('total'):
|
||||||
|
qs = qs.filter(total=fdata.get('total'))
|
||||||
|
if fdata.get('email_known_to_work') is not None:
|
||||||
|
qs = qs.filter(email_known_to_work=fdata.get('email_known_to_work'))
|
||||||
|
if fdata.get('locale'):
|
||||||
|
qs = qs.filter(locale=fdata.get('locale'))
|
||||||
|
if fdata.get('invoice_address_company'):
|
||||||
|
qs = qs.filter(invoice_address__company__icontains=fdata.get('invoice_address_company'))
|
||||||
|
if fdata.get('invoice_address_name'):
|
||||||
|
qs = qs.filter(invoice_address__name_cached__icontains=fdata.get('invoice_address_name'))
|
||||||
|
if fdata.get('invoice_address_street'):
|
||||||
|
qs = qs.filter(invoice_address__street__icontains=fdata.get('invoice_address_street'))
|
||||||
|
if fdata.get('invoice_address_zipcode'):
|
||||||
|
qs = qs.filter(invoice_address__zipcode__iexact=fdata.get('invoice_address_zipcode'))
|
||||||
|
if fdata.get('invoice_address_city'):
|
||||||
|
qs = qs.filter(invoice_address__city__iexact=fdata.get('invoice_address_city'))
|
||||||
|
if fdata.get('invoice_address_country'):
|
||||||
|
qs = qs.filter(invoice_address__country=fdata.get('invoice_address_country'))
|
||||||
|
if fdata.get('attendee_name'):
|
||||||
|
qs = qs.filter(
|
||||||
|
all_positions__attendee_name_cached__icontains=fdata.get('attendee_name')
|
||||||
|
)
|
||||||
|
if fdata.get('attendee_address_company'):
|
||||||
|
qs = qs.filter(
|
||||||
|
all_positions__company__icontains=fdata.get('attendee_address_company')
|
||||||
|
).distinct()
|
||||||
|
if fdata.get('attendee_address_street'):
|
||||||
|
qs = qs.filter(
|
||||||
|
all_positions__street__icontains=fdata.get('attendee_address_street')
|
||||||
|
).distinct()
|
||||||
|
if fdata.get('attendee_address_city'):
|
||||||
|
qs = qs.filter(
|
||||||
|
all_positions__city__iexact=fdata.get('attendee_address_city')
|
||||||
|
).distinct()
|
||||||
|
if fdata.get('attendee_address_country'):
|
||||||
|
qs = qs.filter(
|
||||||
|
all_positions__country=fdata.get('attendee_address_country')
|
||||||
|
).distinct()
|
||||||
|
if fdata.get('ticket_secret'):
|
||||||
|
qs = qs.filter(
|
||||||
|
all_positions__secret__icontains=fdata.get('ticket_secret')
|
||||||
|
).distinct()
|
||||||
|
for q in self.event.questions.all():
|
||||||
|
if fdata.get(f'question_{q.pk}'):
|
||||||
|
answers = QuestionAnswer.objects.filter(
|
||||||
|
question_id=q.pk,
|
||||||
|
orderposition__order_id=OuterRef('pk'),
|
||||||
|
answer__iexact=fdata.get(f'question_{q.pk}')
|
||||||
|
)
|
||||||
|
qs = qs.annotate(**{f'q_{q.pk}': Exists(answers)}).filter(**{f'q_{q.pk}': True})
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class OrderSearchFilterForm(OrderFilterForm):
|
class OrderSearchFilterForm(OrderFilterForm):
|
||||||
orders = {'code': 'code', 'email': 'email', 'total': 'total',
|
orders = {'code': 'code', 'email': 'email', 'total': 'total',
|
||||||
'datetime': 'datetime', 'status': 'status',
|
'datetime': 'datetime', 'status': 'status',
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ def get_event_navigation(request: HttpRequest):
|
|||||||
'event': request.event.slug,
|
'event': request.event.slug,
|
||||||
'organizer': request.event.organizer.slug,
|
'organizer': request.event.organizer.slug,
|
||||||
}),
|
}),
|
||||||
'active': url.url_name in ('event.orders', 'event.order') or "event.order." in url.url_name,
|
'active': url.url_name in ('event.orders', 'event.order', 'event.orders.search') or "event.order." in url.url_name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Overview'),
|
'label': _('Overview'),
|
||||||
|
|||||||
@@ -323,3 +323,19 @@ this is not an Event signal and will be called even if your plugin is not active
|
|||||||
event if the search is performed within an event, and ``None`` otherwise. The search query will be passed as
|
event if the search is performed within an event, and ``None`` otherwise. The search query will be passed as
|
||||||
``query``.
|
``query``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
order_search_forms = EventPluginSignal(
|
||||||
|
providing_args=['request']
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
This signal allows you to return additional forms that should be rendered in the advanced order search.
|
||||||
|
You are passed ``request`` argument and are expected to return an instance of a form class that you bind
|
||||||
|
yourself when appropriate. Your form will be executed as part of the standard validation and rendering
|
||||||
|
cycle and rendered using default bootstrap styles.
|
||||||
|
|
||||||
|
You are required to set ``prefix`` on your form instance. You are required to implement a ``filter_qs(queryset)``
|
||||||
|
method on your form that returns a new, filtered query set. You are required to implement a ``filter_to_strings()``
|
||||||
|
method on your form that returns a list of strings describing the currently active filters.
|
||||||
|
|
||||||
|
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{% block title %}{% trans "Orders" %}{% endblock %}
|
{% block title %}{% trans "Orders" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Orders" %}</h1>
|
<h1>{% trans "Orders" %}</h1>
|
||||||
{% if not filter_form.filtered and orders|length == 0 %}
|
{% if not filter_form.filtered and orders|length == 0 and not filter_strings %}
|
||||||
<div class="empty-collection">
|
<div class="empty-collection">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
@@ -21,57 +21,72 @@
|
|||||||
{% trans "Take your shop live" %}
|
{% trans "Take your shop live" %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg">
|
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg" target="_blank">
|
||||||
{% trans "Go to the ticket shop" %}
|
{% trans "Go to the ticket shop" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="row filter-form">
|
{% if filter_strings %}
|
||||||
<form class="col-md-2 col-xs-12"
|
<p>
|
||||||
action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}">
|
<span class="fa fa-filter"></span>
|
||||||
<div class="input-group">
|
{% trans "Search query:" %}
|
||||||
<input type="text" name="code" class="form-control" placeholder="{% trans "Order code" %}" autofocus>
|
{{ filter_strings|join:" · " }}
|
||||||
<span class="input-group-btn">
|
·
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Go!" %}</button>
|
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}?{{ request.META.QUERY_STRING }}">
|
||||||
</span>
|
<span class="fa fa-edit"></span>
|
||||||
</div>
|
{% trans "Edit" %}
|
||||||
</form>
|
</a>
|
||||||
<form class="" action="" method="get">
|
</p>
|
||||||
<div class="col-md-2 col-xs-6">
|
{% else %}
|
||||||
{% bootstrap_field filter_form.status layout='inline' %}
|
<div class="row filter-form">
|
||||||
</div>
|
<form class="col-md-2 col-xs-12"
|
||||||
{% if request.event.has_subevents %}
|
action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||||
<div class="col-md-1 col-xs-6">
|
<div class="input-group">
|
||||||
{% bootstrap_field filter_form.item layout='inline' %}
|
<input type="text" name="code" class="form-control" placeholder="{% trans "Order code" %}" autofocus>
|
||||||
</div>
|
<span class="input-group-btn">
|
||||||
<div class="col-md-2 col-xs-6">
|
<button class="btn btn-primary" type="submit">{% trans "Go!" %}</button>
|
||||||
{% 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.query layout='inline' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-xs-6">
|
|
||||||
<button class="btn btn-primary btn-block" type="submit">
|
|
||||||
<span class="fa fa-filter"></span>
|
|
||||||
<span class="hidden-md">
|
|
||||||
{% trans "Filter" %}
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
<form class="" action="" method="get">
|
||||||
</div>
|
<div class="col-md-2 col-xs-6">
|
||||||
|
{% bootstrap_field filter_form.status layout='inline' %}
|
||||||
|
</div>
|
||||||
|
{% if request.event.has_subevents %}
|
||||||
|
<div class="col-md-1 col-xs-6">
|
||||||
|
{% bootstrap_field filter_form.item layout='inline' %}
|
||||||
|
</div>
|
||||||
|
<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.query layout='inline' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 col-xs-6">
|
||||||
|
<button class="btn btn-primary btn-block" type="submit">
|
||||||
|
<span class="fa fa-filter"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 col-xs-6">
|
||||||
|
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}" class="btn btn-default btn-block" type="submit" data-toggle="tooltip" title="{% trans "Advanced search" %}">
|
||||||
|
<span class="fa fa-cog"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if filter_form.is_valid and filter_form.cleaned_data.question %}
|
{% if filter_form.is_valid and filter_form.cleaned_data.question %}
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
<span class="fa fa-filter"></span>
|
<span class="fa fa-filter"></span>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load eventurl %}
|
||||||
|
{% load urlreplace %}
|
||||||
|
{% load money %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}{% trans "Order search" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Order search" %}</h1>
|
||||||
|
<form class="form-horizontal" action="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}" method="get">
|
||||||
|
{% for f in forms %}
|
||||||
|
{% bootstrap_form_errors f layout='control' %}
|
||||||
|
{% for field in f %}
|
||||||
|
{% bootstrap_field field layout='control' %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Search" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -283,6 +283,7 @@ urlpatterns = [
|
|||||||
url(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
url(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
||||||
url(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
url(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
||||||
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
||||||
|
url(r'^orders/search$', orders.OrderSearch.as_view(), name='event.orders.search'),
|
||||||
url(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'),
|
url(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'),
|
||||||
url(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'),
|
url(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'),
|
||||||
url(r'^shredder/$', shredder.StartShredView.as_view(), name='event.shredder.start'),
|
url(r'^shredder/$', shredder.StartShredView.as_view(), name='event.shredder.start'),
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ from pretix.base.templatetags.rich_text import markdown_compile_email
|
|||||||
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
||||||
from pretix.base.views.tasks import AsyncAction
|
from pretix.base.views.tasks import AsyncAction
|
||||||
from pretix.control.forms.filter import (
|
from pretix.control.forms.filter import (
|
||||||
EventOrderFilterForm, OverviewFilterForm, RefundFilterForm,
|
EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm,
|
||||||
|
RefundFilterForm,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.orders import (
|
from pretix.control.forms.orders import (
|
||||||
CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm,
|
CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm,
|
||||||
@@ -84,6 +85,7 @@ from pretix.control.forms.orders import (
|
|||||||
OrderRefundForm, OtherOperationsForm,
|
OrderRefundForm, OtherOperationsForm,
|
||||||
)
|
)
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
|
from pretix.control.signals import order_search_forms
|
||||||
from pretix.control.views import PaginationMixin
|
from pretix.control.views import PaginationMixin
|
||||||
from pretix.helpers.safedownload import check_token
|
from pretix.helpers.safedownload import check_token
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
@@ -91,7 +93,31 @@ from pretix.presale.signals import question_form_fields
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OrderList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
class OrderSearchMixin:
|
||||||
|
def get_forms(self):
|
||||||
|
f = [
|
||||||
|
EventOrderExpertFilterForm(
|
||||||
|
data=self.request.GET,
|
||||||
|
event=self.request.event,
|
||||||
|
prefix='expert',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for recv, resp in order_search_forms.send(sender=self.request.event, request=self.request):
|
||||||
|
f.append(resp)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
class OrderSearch(OrderSearchMixin, EventPermissionRequiredMixin, TemplateView):
|
||||||
|
template_name = 'pretixcontrol/orders/search.html'
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['forms'] = self.get_forms()
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
model = Order
|
model = Order
|
||||||
context_object_name = 'orders'
|
context_object_name = 'orders'
|
||||||
template_name = 'pretixcontrol/orders/index.html'
|
template_name = 'pretixcontrol/orders/index.html'
|
||||||
@@ -105,12 +131,21 @@ class OrderList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
qs = self.filter_form.filter_qs(qs)
|
qs = self.filter_form.filter_qs(qs)
|
||||||
|
|
||||||
|
for f in self.get_forms():
|
||||||
|
if any(k.startswith(f.prefix) for k in self.request.GET.keys()) and f.is_valid():
|
||||||
|
qs = f.filter_qs(qs)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['filter_form'] = self.filter_form
|
ctx['filter_form'] = self.filter_form
|
||||||
|
|
||||||
|
ctx['filter_strings'] = []
|
||||||
|
for f in self.get_forms():
|
||||||
|
if any(k.startswith(f.prefix) for k in self.request.GET.keys()) and f.is_valid():
|
||||||
|
ctx['filter_strings'] += f.filter_to_strings()
|
||||||
|
|
||||||
# Only compute this annotations for this page (query optimization)
|
# Only compute this annotations for this page (query optimization)
|
||||||
s = OrderPosition.objects.filter(
|
s = OrderPosition.objects.filter(
|
||||||
order=OuterRef('pk')
|
order=OuterRef('pk')
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ def logged_in_client(client, event):
|
|||||||
('/control/event/{orga}/{event}/orders/overview/', 200),
|
('/control/event/{orga}/{event}/orders/overview/', 200),
|
||||||
('/control/event/{orga}/{event}/orders/export/', 200),
|
('/control/event/{orga}/{event}/orders/export/', 200),
|
||||||
('/control/event/{orga}/{event}/orders/go', 302),
|
('/control/event/{orga}/{event}/orders/go', 302),
|
||||||
|
('/control/event/{orga}/{event}/orders/search', 200),
|
||||||
('/control/event/{orga}/{event}/orders/', 200),
|
('/control/event/{orga}/{event}/orders/', 200),
|
||||||
('/control/event/{orga}/{event}/waitinglist/', 200),
|
('/control/event/{orga}/{event}/waitinglist/', 200),
|
||||||
('/control/event/{orga}/{event}/waitinglist/auto_assign', 405),
|
('/control/event/{orga}/{event}/waitinglist/auto_assign', 405),
|
||||||
|
|||||||
Reference in New Issue
Block a user