diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html index db78d0482c..16528d0454 100644 --- a/src/pretix/control/templates/pretixcontrol/base.html +++ b/src/pretix/control/templates/pretixcontrol/base.html @@ -189,6 +189,13 @@ {% trans "Organizers" %} +
  • + + + {% trans "Order search" %} + +
  • {% for nav in nav_global %}
  • {% trans "Order search" %} +
    +
    + + + + +
    +
    +
    + + + + + + + + + + + + + {% for o in orders %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans "Order code" %} + + {% trans "Event" %} + + {% trans "User" %} + + {% trans "Order total" %} + + {% trans "Order date" %} + + {% trans "Status" %} + +
    + + + {{ o.event.slug|upper }}-{{ o.code }} + + + {{ o.event.name }}{{ o.email }}{{ o.total|floatformat:2 }} {{ o.event.currency }}{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}
    + {% trans "We couldn't find any orders that you have access to and that match your search query." %} +
    +
    + {% include "pretixcontrol/pagination.html" %} +{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 5551775fbd..24ce890ca0 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -2,7 +2,7 @@ from django.conf.urls import include, url from pretix.control.views import ( auth, checkin, dashboards, event, global_settings, item, main, orders, - organizer, typeahead, user, vouchers, waitinglist, + organizer, search, typeahead, user, vouchers, waitinglist, ) urlpatterns = [ @@ -46,6 +46,7 @@ urlpatterns = [ url(r'^events/$', main.EventList.as_view(), name='events'), url(r'^events/add$', main.EventWizard.as_view(), name='events.add'), url(r'^events/typeahead/$', typeahead.event_list, name='events.typeahead'), + url(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'), url(r'^event/(?P[^/]+)/(?P[^/]+)/', include([ url(r'^$', dashboards.event_index, name='event.index'), url(r'^live/$', event.EventLive.as_view(), name='event.live'), diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 9eeea7b323..a7985064e9 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -55,6 +55,8 @@ class OrderList(EventPermissionRequiredMixin, ListView): qs = qs.filter( Q(email__icontains=u) | Q(positions__attendee_name__icontains=u) | Q(positions__attendee_email__icontains=u) + | Q(invoice_address__name__icontains=u) + | Q(invoice_address__company__icontains=u) ) if self.request.GET.get("status", "") != "": s = self.request.GET.get("status", "") diff --git a/src/pretix/control/views/search.py b/src/pretix/control/views/search.py new file mode 100644 index 0000000000..a3dafb64b2 --- /dev/null +++ b/src/pretix/control/views/search.py @@ -0,0 +1,47 @@ +from django.db.models import Q +from django.views.generic import ListView + +from pretix.base.models import Order + + +class OrderSearch(ListView): + model = Order + context_object_name = 'orders' + paginate_by = 30 + template_name = 'pretixcontrol/search/orders.html' + + def get_queryset(self): + qs = Order.objects.all() + if not self.request.user.is_superuser: + qs = qs.filter( + Q(event__organizer_id__in=self.request.user.teams.filter( + all_events=True, can_view_orders=True).values_list('organizer', flat=True)) + | Q(event_id__in=self.request.user.teams.filter( + can_view_orders=True).values_list('limit_events__id', flat=True)) + ) + + if self.request.GET.get("query", "") != "": + u = self.request.GET.get("query", "") + if "-" in u: + code = (Q(event__slug__icontains=u.split("-")[0]) + & Q(code__icontains=Order.normalize_code(u.split("-")[1]))) + else: + code = Q(code__icontains=Order.normalize_code(u)) + qs = qs.filter( + code + | Q(email__icontains=u) + | Q(positions__attendee_name__icontains=u) + | Q(positions__attendee_email__icontains=u) + | Q(invoice_address__name__icontains=u) + | Q(invoice_address__company__icontains=u) + ) + print(qs.query) + + if self.request.GET.get("ordering", "") != "": + p = self.request.GET.get("ordering", "") + p_admissable = ('event', '-event', '-code', 'code', '-email', 'email', '-total', 'total', '-datetime', + 'datetime', '-status', 'status') + if p in p_admissable: + qs = qs.order_by(p) + + return qs.distinct().prefetch_related('event', 'event__organizer') diff --git a/src/pretix/static/pretixcontrol/scss/_forms.scss b/src/pretix/static/pretixcontrol/scss/_forms.scss index 7d2968810b..d191d4cfb8 100644 --- a/src/pretix/static/pretixcontrol/scss/_forms.scss +++ b/src/pretix/static/pretixcontrol/scss/_forms.scss @@ -142,3 +142,12 @@ pre.mail-preview { margin-bottom: 20px; } } + +.search-line { + width: 100%; + margin-bottom: 20px; + + .input-group-btn { + width: 1% !important; + } +} diff --git a/src/tests/control/test_search.py b/src/tests/control/test_search.py new file mode 100644 index 0000000000..a8cefe9236 --- /dev/null +++ b/src/tests/control/test_search.py @@ -0,0 +1,146 @@ +import datetime +from decimal import Decimal + +from django.utils.timezone import now +from tests.base import SoupTest + +from pretix.base.models import ( + Event, InvoiceAddress, Item, Order, OrderPosition, Organizer, Team, User, +) + + +class OrderSearchTest(SoupTest): + def setUp(self): + super().setUp() + self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy') + self.orga1 = Organizer.objects.create(name='CCC', slug='ccc') + self.event1 = Event.objects.create( + organizer=self.orga1, name='30C3', slug='30c3', + date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc), + plugins='pretix.plugins.banktransfer,tests.testdummy' + ) + self.event2 = Event.objects.create( + organizer=self.orga1, name='31C3', slug='31c3', + date_from=datetime.datetime(2014, 12, 26, tzinfo=datetime.timezone.utc), + ) + + o1 = Order.objects.create( + code='FO1A', event=self.event1, email='dummy1@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() + datetime.timedelta(days=10), + total=14, payment_provider='banktransfer', locale='en' + ) + InvoiceAddress.objects.create(order=o1, company="Test Ltd.", name="Peter Miller") + ticket1 = Item.objects.create(event=self.event1, name='Early-bird ticket', + category=None, default_price=23, + admission=True) + OrderPosition.objects.create( + order=o1, + item=ticket1, + variation=None, + price=Decimal("14"), + attendee_name="Peter", + attendee_email="att@att.com" + ) + + o2 = Order.objects.create( + code='FO2', event=self.event2, email='dummy2@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() + datetime.timedelta(days=10), + total=14, payment_provider='banktransfer', locale='en' + ) + ticket2 = Item.objects.create(event=self.event1, name='Early-bird ticket', + category=None, default_price=23, + admission=True) + OrderPosition.objects.create( + order=o2, + item=ticket2, + variation=None, + price=Decimal("14"), + attendee_name="Mark" + ) + + self.team = Team.objects.create(organizer=self.orga1, can_view_orders=True) + self.team.members.add(self.user) + self.team.limit_events.add(self.event1) + + self.client.login(email='dummy@dummy.dummy', password='dummy') + + def test_team_limit_event(self): + resp = self.client.get('/control/search/orders/').rendered_content + assert 'FO1' in resp + assert 'FO2' not in resp + + def test_team_limit_event_wrong_permission(self): + self.team.can_view_orders = False + self.team.save() + resp = self.client.get('/control/search/orders/').rendered_content + assert 'FO1' not in resp + assert 'FO2' not in resp + + def test_team_all_events(self): + self.team.all_events = True + self.team.save() + resp = self.client.get('/control/search/orders/').rendered_content + assert 'FO1' in resp + assert 'FO2' in resp + + def test_team_all_events_wrong_permission(self): + self.team.all_events = True + self.team.can_view_orders = False + self.team.save() + resp = self.client.get('/control/search/orders/').rendered_content + assert 'FO1' not in resp + assert 'FO2' not in resp + + def test_team_none(self): + self.team.members.clear() + resp = self.client.get('/control/search/orders/').rendered_content + assert 'FO1' not in resp + assert 'FO2' not in resp + + def test_suberuser(self): + self.user.is_superuser = True + self.user.save() + self.team.members.clear() + resp = self.client.get('/control/search/orders/').rendered_content + assert 'FO1' in resp + assert 'FO2' in resp + + def test_filter_email(self): + resp = self.client.get('/control/search/orders/?query=dummy1@dummy').rendered_content + assert 'FO1' in resp + resp = self.client.get('/control/search/orders/?query=dummynope').rendered_content + assert 'FO1' not in resp + + def test_filter_attendee_name(self): + resp = self.client.get('/control/search/orders/?query=Pete').rendered_content + assert 'FO1' in resp + resp = self.client.get('/control/search/orders/?query=Mark').rendered_content + assert 'FO1' not in resp + + def test_filter_attendee_email(self): + resp = self.client.get('/control/search/orders/?query=att.com').rendered_content + assert 'FO1' in resp + resp = self.client.get('/control/search/orders/?query=nope.com').rendered_content + assert 'FO1' not in resp + + def test_filter_invoice_address(self): + resp = self.client.get('/control/search/orders/?query=Ltd').rendered_content + assert 'FO1' in resp + resp = self.client.get('/control/search/orders/?query=Miller').rendered_content + assert 'FO1' in resp + + def test_filter_code(self): + resp = self.client.get('/control/search/orders/?query=FO1').rendered_content + assert '30C3-FO1' in resp + resp = self.client.get('/control/search/orders/?query=30c3-FO1').rendered_content + assert '30C3-FO1' in resp + resp = self.client.get('/control/search/orders/?query=30C3-fO1A').rendered_content + assert '30C3-FO1' in resp + resp = self.client.get('/control/search/orders/?query=30C3-fo14').rendered_content + assert '30C3-FO1' in resp + resp = self.client.get('/control/search/orders/?query=31c3-FO1').rendered_content + assert '30C3-FO1' not in resp + resp = self.client.get('/control/search/orders/?query=FO2').rendered_content + assert '30C3-FO1' not in resp