diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py
index 23288003d..fc2b7ec3a 100644
--- a/src/pretix/control/forms/filter.py
+++ b/src/pretix/control/forms/filter.py
@@ -39,6 +39,7 @@ from urllib.parse import urlencode
from django import forms
from django.apps import apps
from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
from django.db.models import (
Count, Exists, F, Max, Model, OrderBy, OuterRef, Q, QuerySet,
)
@@ -59,7 +60,7 @@ from pretix.base.models import (
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
- Voucher,
+ User, Voucher,
)
from pretix.base.signals import register_payment_providers
from pretix.base.timeframes import (
@@ -3007,3 +3008,91 @@ class OutgoingMailFilterForm(FilterForm):
qs = qs.order_by("-created", "-pk")
return qs
+
+
+class LogFilterForm(FilterForm):
+ source = forms.ChoiceField(
+ label=_('Source'),
+ choices=[
+ ('', _('All sources')),
+ ('team', _('Team actions')),
+ ('customer', _('Customer actions')),
+ ('device', _('Device actions')),
+ ],
+ required=False
+ )
+ device = SafeModelChoiceField(
+ label=_('Device'),
+ empty_label=_('All devices'),
+ queryset=Device.objects.none(),
+ required=False
+ )
+ user_email = forms.EmailField(
+ label=_('User email'),
+ widget=forms.EmailInput(
+ attrs={"placeholder": _('All users')}
+ ),
+ required=False
+ )
+ action_type = forms.CharField(
+ widget=forms.HiddenInput,
+ required=False,
+ )
+ content_type = forms.ModelChoiceField(
+ queryset=ContentType.objects.all(),
+ widget=forms.HiddenInput,
+ required=False,
+ )
+ object = forms.IntegerField(
+ widget=forms.HiddenInput,
+ required=False
+ )
+
+ def __init__(self, *args, **kwargs):
+ self.organizer = kwargs.pop('organizer')
+ super().__init__(*args, **kwargs)
+
+ self.fields['device'].queryset = self.organizer.devices.all().order_by('device_id')
+ self.fields['device'].widget = Select2(
+ attrs={
+ 'data-model-select2': 'generic',
+ 'data-select2-url': reverse('control:organizer.devices.select2', kwargs={
+ 'organizer': self.organizer.slug,
+ }),
+ 'data-placeholder': _('All devices'),
+ }
+ )
+ self.fields['device'].widget.choices = self.fields['device'].choices
+ self.fields['device'].label = _('Device')
+
+ def filter_qs(self, qs):
+ fdata = self.cleaned_data
+ if fdata.get('source') == 'team':
+ qs = qs.filter(user__isnull=False)
+ elif fdata.get('source') == 'device':
+ qs = qs.filter(device__isnull=False)
+ elif fdata.get('source') == 'customer':
+ qs = qs.filter(user__isnull=True, device__isnull=True)
+
+ if fdata.get('device'):
+ qs = qs.filter(device_id=fdata['device'].pk)
+
+ if fdata.get('action_type'):
+ qs = qs.filter(action_type__in=fdata['action_type'].split(','))
+
+ if fdata.get('user_email'):
+ try:
+ user = User.objects.get(email=fdata['user_email'].lower())
+ qs = qs.filter(user=user)
+ except User.DoesNotExist:
+ # Do not actively leak info that this user does not exist. Yes, this will likely have a
+ # timing attack possibility, but with the magic that database query planners do,
+ # it's probably impossible to avoid that.
+ qs = qs.none()
+
+ if fdata.get('content_type'):
+ qs = qs.filter(content_type=fdata.get('content_type'))
+
+ if fdata.get('object'):
+ qs = qs.filter(object_id=fdata.get('object'))
+ return qs
diff --git a/src/pretix/control/templates/pretixcontrol/event/logs.html b/src/pretix/control/templates/pretixcontrol/event/logs.html
index 7fae7b6f4..cad530109 100644
--- a/src/pretix/control/templates/pretixcontrol/event/logs.html
+++ b/src/pretix/control/templates/pretixcontrol/event/logs.html
@@ -1,41 +1,12 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load static %}
+{% load bootstrap3 %}
+{% load icon %}
{% block title %}{% trans "Event logs" %}{% endblock %}
{% block inside %}
{% trans "Event logs" %}
-
+ {% include "pretixcontrol/fragment_log_filter_form.html" %}
{% for log in logs %}
-
diff --git a/src/pretix/control/templates/pretixcontrol/fragment_log_filter_form.html b/src/pretix/control/templates/pretixcontrol/fragment_log_filter_form.html
new file mode 100644
index 000000000..aab452248
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/fragment_log_filter_form.html
@@ -0,0 +1,38 @@
+{% load i18n %}
+{% load bootstrap3 %}
+{% load icon %}
+
+
+
+ {% trans "Filter" %}
+
+
+
+
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/logs.html b/src/pretix/control/templates/pretixcontrol/organizers/logs.html
index aa3f32a63..258fee952 100644
--- a/src/pretix/control/templates/pretixcontrol/organizers/logs.html
+++ b/src/pretix/control/templates/pretixcontrol/organizers/logs.html
@@ -4,24 +4,7 @@
{% block title %}{% trans "Organizer logs" %}{% endblock %}
{% block inside %}
{% trans "Organizer logs" %}
-
+ {% include "pretixcontrol/fragment_log_filter_form.html" %}
{% for log in logs %}
-
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py
index 90e685232..632880da6 100644
--- a/src/pretix/control/views/event.py
+++ b/src/pretix/control/views/event.py
@@ -60,7 +60,7 @@ from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
JsonResponse,
)
-from django.shortcuts import get_object_or_404, redirect
+from django.shortcuts import redirect
from django.urls import NoReverseMatch, reverse
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html
@@ -119,6 +119,7 @@ from ...helpers.compat import CompatDeleteView
from ...helpers.format import (
PlainHtmlAlternativeString, SafeFormatter, format_map,
)
+from ..forms.filter import LogFilterForm
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
@@ -1282,32 +1283,20 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
]
qs = qs.filter(content_type__in=allowed_types)
- if self.request.GET.get('user') == 'yes':
- qs = qs.filter(user__isnull=False)
- elif self.request.GET.get('user') == 'no':
- qs = qs.filter(user__isnull=True)
- elif self.request.GET.get('user', '').startswith('d-'):
- qs = qs.filter(device_id=self.request.GET.get('user')[2:])
- elif self.request.GET.get('user'):
- qs = qs.filter(user_id=self.request.GET.get('user'))
-
- if self.request.GET.get('action_type'):
- qs = qs.filter(action_type=self.request.GET['action_type'])
-
- if self.request.GET.get('content_type'):
- qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
-
- if self.request.GET.get('object'):
- qs = qs.filter(object_id=self.request.GET.get('object'))
+ if self.filter_form.is_valid():
+ qs = self.filter_form.filter_qs(qs)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
- ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
- ctx['devicelist'] = self.request.event.logentry_set.order_by('device__name').distinct().values('device__id', 'device__name')
+ ctx['filter_form'] = self.filter_form
return ctx
+ @cached_property
+ def filter_form(self):
+ return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
+
class EventComment(EventPermissionRequiredMixin, View):
permission = 'event.settings.general:write'
diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py
index 62f26c899..3c04dae43 100644
--- a/src/pretix/control/views/organizer.py
+++ b/src/pretix/control/views/organizer.py
@@ -113,7 +113,8 @@ from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.exports import ScheduledOrganizerExportForm
from pretix.control.forms.filter import (
CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm,
- OrganizerFilterForm, ReusableMediaFilterForm, TeamFilterForm,
+ LogFilterForm, OrganizerFilterForm, ReusableMediaFilterForm,
+ TeamFilterForm,
)
from pretix.control.forms.orders import ExporterForm
from pretix.control.forms.organizer import (
@@ -2672,16 +2673,21 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
- if self.request.GET.get('action_type'):
- qs = qs.filter(action_type=self.request.GET['action_type'])
- if self.request.GET.get('user'):
- qs = qs.filter(user_id=self.request.GET.get('user'))
+
+ if self.filter_form.is_valid():
+ qs = self.filter_form.filter_qs(qs)
+
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
+ ctx['filter_form'] = self.filter_form
return ctx
+ @cached_property
+ def filter_form(self):
+ return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
+
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = MembershipType