diff --git a/src/pretix/api/auth/permission.py b/src/pretix/api/auth/permission.py index 6534f68a6e..3fbe851b49 100644 --- a/src/pretix/api/auth/permission.py +++ b/src/pretix/api/auth/permission.py @@ -3,7 +3,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission from pretix.api.models import OAuthAccessToken from pretix.base.models import Device, Event, User from pretix.base.models.auth import SuperuserPermissionSet -from pretix.base.models.organizer import Organizer, TeamAPIToken +from pretix.base.models.organizer import TeamAPIToken from pretix.helpers.security import ( SessionInvalid, SessionReauthRequired, assert_session_valid, ) @@ -50,9 +50,6 @@ class EventPermission(BasePermission): return False elif 'organizer' in request.resolver_match.kwargs: - request.organizer = Organizer.objects.filter( - slug=request.resolver_match.kwargs['organizer'], - ).first() if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request): return False if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key): diff --git a/src/pretix/api/middleware.py b/src/pretix/api/middleware.py index c75623ae00..6c74c9c705 100644 --- a/src/pretix/api/middleware.py +++ b/src/pretix/api/middleware.py @@ -1,4 +1,6 @@ import json +from django.urls import resolve +from django_scopes import scope from hashlib import sha1 from django.conf import settings @@ -8,6 +10,7 @@ from django.utils.timezone import now from rest_framework import status from pretix.api.models import ApiCall +from pretix.base.models import Organizer class IdempotencyMiddleware: @@ -89,3 +92,21 @@ class IdempotencyMiddleware: for k, v in json.loads(call.response_headers).values(): r[k] = v return r + + +class ApiScopeMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request: HttpRequest): + if not request.path.startswith('/api/'): + return self.get_response(request) + + url = resolve(request.path_info) + if 'organizer' in url.kwargs: + request.organizer = Organizer.objects.filter( + slug=url.kwargs['organizer'], + ).first() + + with scope(organizer=getattr(request, 'organizer', None)): + return self.get_response(request) diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index f02bcb93bd..79e07138fb 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -6,6 +6,7 @@ from django.shortcuts import get_object_or_404 from django.utils.functional import cached_property from django.utils.timezone import now from django_filters.rest_framework import DjangoFilterBackend, FilterSet +from django_scopes import scopes_disabled from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.fields import DateTimeField @@ -25,10 +26,11 @@ from pretix.base.services.checkin import ( from pretix.helpers.database import FixedOrderBy -class CheckinListFilter(FilterSet): - class Meta: - model = CheckinList - fields = ['subevent'] +with scopes_disabled(): + class CheckinListFilter(FilterSet): + class Meta: + model = CheckinList + fields = ['subevent'] class CheckinListViewSet(viewsets.ModelViewSet): @@ -154,7 +156,7 @@ class CheckinOrderPositionFilter(OrderPositionFilter): class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = CheckinListOrderPositionSerializer - queryset = OrderPosition.objects.none() + queryset = OrderPosition.all.none() filter_backends = (DjangoFilterBackend, RichOrderingFilter) ordering = ('attendee_name_cached', 'positionid') ordering_fields = ( diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index 9dd95b0fd5..1443fbda72 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -3,6 +3,7 @@ from django.db import transaction from django.db.models import ProtectedError, Q from django.utils.timezone import now from django_filters.rest_framework import DjangoFilterBackend, FilterSet +from django_scopes import scopes_disabled from rest_framework import filters, viewsets from rest_framework.exceptions import PermissionDenied @@ -19,50 +20,51 @@ from pretix.base.models.event import SubEvent from pretix.helpers.dicts import merge_dicts -class EventFilter(FilterSet): - is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs') - is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs') - ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs') +with scopes_disabled(): + class EventFilter(FilterSet): + is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs') + is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs') + ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs') - class Meta: - model = Event - fields = ['is_public', 'live', 'has_subevents'] + class Meta: + model = Event + fields = ['is_public', 'live', 'has_subevents'] - def ends_after_qs(self, queryset, name, value): - expr = ( - Q(has_subevents=False) & - Q( - Q(Q(date_to__isnull=True) & Q(date_from__gte=value)) - | Q(Q(date_to__isnull=False) & Q(date_to__gte=value)) + def ends_after_qs(self, queryset, name, value): + expr = ( + Q(has_subevents=False) & + Q( + Q(Q(date_to__isnull=True) & Q(date_from__gte=value)) + | Q(Q(date_to__isnull=False) & Q(date_to__gte=value)) + ) ) - ) - return queryset.filter(expr) - - def is_past_qs(self, queryset, name, value): - expr = ( - Q(has_subevents=False) & - Q( - Q(Q(date_to__isnull=True) & Q(date_from__lt=now())) - | Q(Q(date_to__isnull=False) & Q(date_to__lt=now())) - ) - ) - if value: return queryset.filter(expr) - else: - return queryset.exclude(expr) - def is_future_qs(self, queryset, name, value): - expr = ( - Q(has_subevents=False) & - Q( - Q(Q(date_to__isnull=True) & Q(date_from__gte=now())) - | Q(Q(date_to__isnull=False) & Q(date_to__gte=now())) + def is_past_qs(self, queryset, name, value): + expr = ( + Q(has_subevents=False) & + Q( + Q(Q(date_to__isnull=True) & Q(date_from__lt=now())) + | Q(Q(date_to__isnull=False) & Q(date_to__lt=now())) + ) ) - ) - if value: - return queryset.filter(expr) - else: - return queryset.exclude(expr) + if value: + return queryset.filter(expr) + else: + return queryset.exclude(expr) + + def is_future_qs(self, queryset, name, value): + expr = ( + Q(has_subevents=False) & + Q( + Q(Q(date_to__isnull=True) & Q(date_from__gte=now())) + | Q(Q(date_to__isnull=False) & Q(date_to__gte=now())) + ) + ) + if value: + return queryset.filter(expr) + else: + return queryset.exclude(expr) class EventViewSet(viewsets.ModelViewSet): @@ -182,41 +184,42 @@ class CloneEventViewSet(viewsets.ModelViewSet): ) -class SubEventFilter(FilterSet): - is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs') - is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs') - ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs') +with scopes_disabled(): + class SubEventFilter(FilterSet): + is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs') + is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs') + ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs') - class Meta: - model = SubEvent - fields = ['active', 'event__live'] + class Meta: + model = SubEvent + fields = ['active', 'event__live'] - def ends_after_qs(self, queryset, name, value): - expr = Q( - Q(Q(date_to__isnull=True) & Q(date_from__gte=value)) - | Q(Q(date_to__isnull=False) & Q(date_to__gte=value)) - ) - return queryset.filter(expr) - - def is_past_qs(self, queryset, name, value): - expr = Q( - Q(Q(date_to__isnull=True) & Q(date_from__lt=now())) - | Q(Q(date_to__isnull=False) & Q(date_to__lt=now())) - ) - if value: + def ends_after_qs(self, queryset, name, value): + expr = Q( + Q(Q(date_to__isnull=True) & Q(date_from__gte=value)) + | Q(Q(date_to__isnull=False) & Q(date_to__gte=value)) + ) return queryset.filter(expr) - else: - return queryset.exclude(expr) - def is_future_qs(self, queryset, name, value): - expr = Q( - Q(Q(date_to__isnull=True) & Q(date_from__gte=now())) - | Q(Q(date_to__isnull=False) & Q(date_to__gte=now())) - ) - if value: - return queryset.filter(expr) - else: - return queryset.exclude(expr) + def is_past_qs(self, queryset, name, value): + expr = Q( + Q(Q(date_to__isnull=True) & Q(date_from__lt=now())) + | Q(Q(date_to__isnull=False) & Q(date_to__lt=now())) + ) + if value: + return queryset.filter(expr) + else: + return queryset.exclude(expr) + + def is_future_qs(self, queryset, name, value): + expr = Q( + Q(Q(date_to__isnull=True) & Q(date_from__gte=now())) + | Q(Q(date_to__isnull=False) & Q(date_to__gte=now())) + ) + if value: + return queryset.filter(expr) + else: + return queryset.exclude(expr) class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet): diff --git a/src/pretix/api/views/item.py b/src/pretix/api/views/item.py index 872db4969e..b1153e4a66 100644 --- a/src/pretix/api/views/item.py +++ b/src/pretix/api/views/item.py @@ -3,6 +3,7 @@ from django.db.models import Q from django.shortcuts import get_object_or_404 from django.utils.functional import cached_property from django_filters.rest_framework import DjangoFilterBackend, FilterSet +from django_scopes import scopes_disabled from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied @@ -22,18 +23,19 @@ from pretix.base.models import ( from pretix.helpers.dicts import merge_dicts -class ItemFilter(FilterSet): - tax_rate = django_filters.CharFilter(method='tax_rate_qs') +with scopes_disabled(): + class ItemFilter(FilterSet): + tax_rate = django_filters.CharFilter(method='tax_rate_qs') - def tax_rate_qs(self, queryset, name, value): - if value in ("0", "None", "0.00"): - return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0)) - else: - return queryset.filter(tax_rule__rate=value) + def tax_rate_qs(self, queryset, name, value): + if value in ("0", "None", "0.00"): + return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0)) + else: + return queryset.filter(tax_rule__rate=value) - class Meta: - model = Item - fields = ['active', 'category', 'admission', 'tax_rate', 'free_price'] + class Meta: + model = Item + fields = ['active', 'category', 'admission', 'tax_rate', 'free_price'] class ItemViewSet(ConditionalListView, viewsets.ModelViewSet): @@ -319,10 +321,11 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet): super().perform_destroy(instance) -class QuestionFilter(FilterSet): - class Meta: - model = Question - fields = ['ask_during_checkin', 'required', 'identifier'] +with scopes_disabled(): + class QuestionFilter(FilterSet): + class Meta: + model = Question + fields = ['ask_during_checkin', 'required', 'identifier'] class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet): @@ -418,10 +421,11 @@ class QuestionOptionViewSet(viewsets.ModelViewSet): super().perform_destroy(instance) -class QuotaFilter(FilterSet): - class Meta: - model = Quota - fields = ['subevent'] +with scopes_disabled(): + class QuotaFilter(FilterSet): + class Meta: + model = Quota + fields = ['subevent'] class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet): diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index fdb076d849..7e8e8c795f 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404 from django.utils.timezone import make_aware, now from django.utils.translation import ugettext as _ from django_filters.rest_framework import DjangoFilterBackend, FilterSet +from django_scopes import scopes_disabled from rest_framework import mixins, serializers, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ( @@ -51,16 +52,17 @@ from pretix.base.signals import ( from pretix.base.templatetags.money import money_filter -class OrderFilter(FilterSet): - email = django_filters.CharFilter(field_name='email', lookup_expr='iexact') - code = django_filters.CharFilter(field_name='code', lookup_expr='iexact') - status = django_filters.CharFilter(field_name='status', lookup_expr='iexact') - modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte') - created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte') +with scopes_disabled(): + class OrderFilter(FilterSet): + email = django_filters.CharFilter(field_name='email', lookup_expr='iexact') + code = django_filters.CharFilter(field_name='code', lookup_expr='iexact') + status = django_filters.CharFilter(field_name='status', lookup_expr='iexact') + modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte') + created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte') - class Meta: - model = Order - fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval'] + class Meta: + model = Order + fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval'] class OrderViewSet(viewsets.ModelViewSet): @@ -531,23 +533,24 @@ class OrderViewSet(viewsets.ModelViewSet): self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) -class OrderPositionFilter(FilterSet): - order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact') - has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs') - attendee_name = django_filters.CharFilter(method='attendee_name_qs') - search = django_filters.CharFilter(method='search_qs') +with scopes_disabled(): + class OrderPositionFilter(FilterSet): + order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact') + has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs') + attendee_name = django_filters.CharFilter(method='attendee_name_qs') + search = django_filters.CharFilter(method='search_qs') - def search_qs(self, queryset, name, value): - return queryset.filter( - Q(secret__istartswith=value) - | Q(attendee_name_cached__icontains=value) - | Q(addon_to__attendee_name_cached__icontains=value) - | Q(attendee_email__icontains=value) - | Q(addon_to__attendee_email__icontains=value) - | Q(order__code__istartswith=value) - | Q(order__invoice_address__name_cached__icontains=value) - | Q(order__email__icontains=value) - ) + def search_qs(self, queryset, name, value): + return queryset.filter( + Q(secret__istartswith=value) + | Q(attendee_name_cached__icontains=value) + | Q(addon_to__attendee_name_cached__icontains=value) + | Q(attendee_email__icontains=value) + | Q(addon_to__attendee_email__icontains=value) + | Q(order__code__istartswith=value) + | Q(order__invoice_address__name_cached__icontains=value) + | Q(order__email__icontains=value) + ) def has_checkin_qs(self, queryset, name, value): return queryset.filter(checkins__isnull=not value) @@ -572,7 +575,7 @@ class OrderPositionFilter(FilterSet): class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet): serializer_class = OrderPositionSerializer - queryset = OrderPosition.objects.none() + queryset = OrderPosition.all.none() filter_backends = (DjangoFilterBackend, OrderingFilter) ordering = ('order__datetime', 'positionid') ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',) @@ -960,22 +963,23 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): serializer.save() -class InvoiceFilter(FilterSet): - refers = django_filters.CharFilter(method='refers_qs') - number = django_filters.CharFilter(method='nr_qs') - order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact') +with scopes_disabled(): + class InvoiceFilter(FilterSet): + refers = django_filters.CharFilter(method='refers_qs') + number = django_filters.CharFilter(method='nr_qs') + order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact') - def refers_qs(self, queryset, name, value): - return queryset.annotate( - refers_nr=Concat('refers__prefix', 'refers__invoice_no') - ).filter(refers_nr__iexact=value) + def refers_qs(self, queryset, name, value): + return queryset.annotate( + refers_nr=Concat('refers__prefix', 'refers__invoice_no') + ).filter(refers_nr__iexact=value) - def nr_qs(self, queryset, name, value): - return queryset.filter(nr__iexact=value) + def nr_qs(self, queryset, name, value): + return queryset.filter(nr__iexact=value) - class Meta: - model = Invoice - fields = ['order', 'number', 'is_cancellation', 'refers', 'locale'] + class Meta: + model = Invoice + fields = ['order', 'number', 'is_cancellation', 'refers', 'locale'] class RetryException(APIException): diff --git a/src/pretix/api/views/voucher.py b/src/pretix/api/views/voucher.py index 187a048370..6db0a4d2c7 100644 --- a/src/pretix/api/views/voucher.py +++ b/src/pretix/api/views/voucher.py @@ -6,6 +6,7 @@ from django.utils.timezone import now from django_filters.rest_framework import ( BooleanFilter, DjangoFilterBackend, FilterSet, ) +from django_scopes import scopes_disabled from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied @@ -16,21 +17,22 @@ from pretix.api.serializers.voucher import VoucherSerializer from pretix.base.models import Voucher -class VoucherFilter(FilterSet): - active = BooleanFilter(method='filter_active') +with scopes_disabled(): + class VoucherFilter(FilterSet): + active = BooleanFilter(method='filter_active') - class Meta: - model = Voucher - fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota', - 'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent'] + class Meta: + model = Voucher + fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota', + 'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent'] - def filter_active(self, queryset, name, value): - if value: - return queryset.filter(Q(redeemed__lt=F('max_usages')) & - (Q(valid_until__isnull=True) | Q(valid_until__gt=now()))) - else: - return queryset.filter(Q(redeemed__gte=F('max_usages')) | - (Q(valid_until__isnull=False) & Q(valid_until__lte=now()))) + def filter_active(self, queryset, name, value): + if value: + return queryset.filter(Q(redeemed__lt=F('max_usages')) & + (Q(valid_until__isnull=True) | Q(valid_until__gt=now()))) + else: + return queryset.filter(Q(redeemed__gte=F('max_usages')) | + (Q(valid_until__isnull=False) & Q(valid_until__lte=now()))) class VoucherViewSet(viewsets.ModelViewSet): diff --git a/src/pretix/api/views/waitinglist.py b/src/pretix/api/views/waitinglist.py index d8bb516293..ec6c61a01c 100644 --- a/src/pretix/api/views/waitinglist.py +++ b/src/pretix/api/views/waitinglist.py @@ -1,5 +1,6 @@ import django_filters from django_filters.rest_framework import DjangoFilterBackend, FilterSet +from django_scopes import scopes_disabled from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied, ValidationError @@ -11,15 +12,16 @@ from pretix.base.models import WaitingListEntry from pretix.base.models.waitinglist import WaitingListException -class WaitingListFilter(FilterSet): - has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs') +with scopes_disabled(): + class WaitingListFilter(FilterSet): + has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs') - def has_voucher_qs(self, queryset, name, value): - return queryset.filter(voucher__isnull=not value) + def has_voucher_qs(self, queryset, name, value): + return queryset.filter(voucher__isnull=not value) - class Meta: - model = WaitingListEntry - fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent'] + class Meta: + model = WaitingListEntry + fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent'] class WaitingListViewSet(viewsets.ModelViewSet): diff --git a/src/pretix/base/models/checkin.py b/src/pretix/base/models/checkin.py index 308576aeb9..2d46f5e1f1 100644 --- a/src/pretix/base/models/checkin.py +++ b/src/pretix/base/models/checkin.py @@ -3,6 +3,7 @@ from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When from django.db.models.functions import Coalesce from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext_lazy as _ +from django_scopes import ScopedManager from pretix.base.models import LoggedModel @@ -20,6 +21,8 @@ class CheckinList(LoggedModel): 'order have not been paid. This only works with pretixdesk ' '0.3.0 or newer or pretixdroid 1.9 or newer.')) + objects = ScopedManager(organizer='event__organizer') + class Meta: ordering = ('subevent__date_from', 'name') @@ -167,6 +170,8 @@ class Checkin(models.Model): 'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT, ) + objects = ScopedManager(organizer='position__order__event__organizer') + class Meta: unique_together = (('list', 'position'),) diff --git a/src/pretix/base/models/devices.py b/src/pretix/base/models/devices.py index 69f9ae3438..bba27a0458 100644 --- a/src/pretix/base/models/devices.py +++ b/src/pretix/base/models/devices.py @@ -4,6 +4,7 @@ from django.db import models from django.db.models import Max from django.utils.crypto import get_random_string from django.utils.translation import ugettext_lazy as _ +from django_scopes import ScopedManager from pretix.base.models import LoggedModel @@ -71,6 +72,8 @@ class Device(LoggedModel): null=True, blank=True ) + objects = ScopedManager(organizer='organizer') + class Meta: unique_together = (('organizer', 'device_id'),) diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 3803f1ead0..1d6bd1c6f2 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -2,6 +2,7 @@ import string import uuid from collections import OrderedDict from datetime import datetime, time, timedelta +from django_scopes import ScopedManager from operator import attrgetter import pytz @@ -336,6 +337,8 @@ class Event(EventMixin, LoggedModel): default=False ) + objects = ScopedManager(organizer='organizer') + class Meta: verbose_name = _("Event") verbose_name_plural = _("Events") @@ -875,6 +878,8 @@ class SubEvent(EventMixin, LoggedModel): items = models.ManyToManyField('Item', through='SubEventItem') variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation') + objects = ScopedManager(organizer='event__organizer') + class Meta: verbose_name = _("Date in event series") verbose_name_plural = _("Dates in event series") diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py index 81e93fb8d8..7312542b08 100644 --- a/src/pretix/base/models/invoices.py +++ b/src/pretix/base/models/invoices.py @@ -9,6 +9,7 @@ from django.utils.crypto import get_random_string from django.utils.functional import cached_property from django.utils.translation import pgettext from django_countries.fields import CountryField +from django_scopes import ScopedManager def invoice_filename(instance, filename: str) -> str: @@ -107,6 +108,8 @@ class Invoice(models.Model): file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255) internal_reference = models.TextField(blank=True) + objects = ScopedManager(organizer='event__organizer') + @staticmethod def _to_numeric_invoice_number(number): return '{:05d}'.format(int(number)) diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index cea6e05760..05446c9b1b 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -3,6 +3,7 @@ import uuid from collections import Counter from datetime import date, datetime, time from decimal import Decimal, DecimalException +from django_scopes import ScopedManager from typing import Tuple import dateutil.parser @@ -155,28 +156,41 @@ class SubEventItemVariation(models.Model): self.subevent.event.cache.clear() -class ItemQuerySet(models.QuerySet): - def filter_available(self, channel='web', voucher=None, allow_addons=False): - q = ( - # IMPORTANT: If this is updated, also update the ItemVariation query - # in models/event.py: EventMixin.annotated() +def filter_available(qs, channel='web', voucher=None, allow_addons=False): + q = ( + # IMPORTANT: If this is updated, also update the ItemVariation query + # in models/event.py: EventMixin.annotated() Q(active=True) & Q(Q(available_from__isnull=True) | Q(available_from__lte=now())) & Q(Q(available_until__isnull=True) | Q(available_until__gte=now())) & Q(sales_channels__contains=channel) & Q(require_bundling=False) - ) - if not allow_addons: - q &= Q(Q(category__isnull=True) | Q(category__is_addon=False)) - qs = self.filter(q) + ) + if not allow_addons: + q &= Q(Q(category__isnull=True) | Q(category__is_addon=False)) + qs = qs.filter(q) - vouchq = Q(hide_without_voucher=False) - if voucher: - if voucher.item_id: - vouchq |= Q(pk=voucher.item_id) - qs = qs.filter(pk=voucher.item_id) - elif voucher.quota_id: - qs = qs.filter(quotas__in=[voucher.quota_id]) - return qs.filter(vouchq) + vouchq = Q(hide_without_voucher=False) + if voucher: + if voucher.item_id: + vouchq |= Q(pk=voucher.item_id) + qs = qs.filter(pk=voucher.item_id) + elif voucher.quota_id: + qs = qs.filter(quotas__in=[voucher.quota_id]) + return qs.filter(vouchq) + + +class ItemQuerySet(models.QuerySet): + def filter_available(self, channel='web', voucher=None, allow_addons=False): + return filter_available(self) + + +class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__): + def __init__(self): + super().__init__() + self._queryset_class = ItemQuerySet + + def filter_available(self, channel='web', voucher=None, allow_addons=False): + return filter_available(self.get_queryset()) class Item(LoggedModel): @@ -226,7 +240,7 @@ class Item(LoggedModel): :type sales_channels: bool """ - objects = ItemQuerySet.as_manager() + objects = ItemQuerySetManager() event = models.ForeignKey( Event, @@ -377,6 +391,7 @@ class Item(LoggedModel): # !!! Attention: If you add new fields here, also add them to the copying code in # pretix/control/forms/item.py if applicable. + class Meta: verbose_name = _("Product") verbose_name_plural = _("Products") @@ -591,6 +606,8 @@ class ItemVariation(models.Model): 'discounted one. This is just a cosmetic setting and will not actually impact pricing.') ) + objects = ScopedManager(organizer='item__event__organizer') + class Meta: verbose_name = _("Product variation") verbose_name_plural = _("Product variations") @@ -985,6 +1002,8 @@ class Question(LoggedModel): ) dependency_value = models.TextField(null=True, blank=True) + objects = ScopedManager(organizer='event__organizer') + class Meta: verbose_name = _("Question") verbose_name_plural = _("Questions") @@ -1234,6 +1253,8 @@ class Quota(LoggedModel): cached_availability_paid_orders = models.PositiveIntegerField(null=True, blank=True) cached_availability_time = models.DateTimeField(null=True, blank=True) + objects = ScopedManager(organizer='event__organizer') + class Meta: verbose_name = _("Quota") verbose_name_plural = _("Quotas") diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 280d06bfb0..239a8b38e3 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -6,6 +6,7 @@ import os import string from datetime import datetime, time, timedelta from decimal import Decimal +from django_scopes import ScopedManager from typing import Any, Dict, List, Union import dateutil @@ -186,6 +187,8 @@ class Order(LockModel, LoggedModel): verbose_name=_('E-mail address verified') ) + objects = ScopedManager(organizer='event__organizer') + class Meta: verbose_name = _("Order") verbose_name_plural = _("Orders") @@ -822,6 +825,8 @@ class QuestionAnswer(models.Model): max_length=255 ) + objects = ScopedManager(organizer='question__event__organizer') + @property def backend_file_url(self): if self.file: @@ -1145,6 +1150,8 @@ class OrderPayment(models.Model): ) migrated = models.BooleanField(default=False) + objects = ScopedManager(organizer='order__event__organizer') + class Meta: ordering = ('local_id',) @@ -1501,6 +1508,8 @@ class OrderRefund(models.Model): null=True, blank=True ) + objects = ScopedManager(organizer='order__event__organizer') + class Meta: ordering = ('local_id',) @@ -1562,7 +1571,7 @@ class OrderRefund(models.Model): super().save(*args, **kwargs) -class ActivePositionManager(models.Manager): +class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__): def get_queryset(self): return super().get_queryset().filter(canceled=False) @@ -1639,7 +1648,7 @@ class OrderFee(models.Model): ) canceled = models.BooleanField(default=False) - all = models.Manager() + all = ScopedManager(organizer='order__event__organizer') objects = ActivePositionManager() @property @@ -1744,7 +1753,7 @@ class OrderPosition(AbstractPosition): ) canceled = models.BooleanField(default=False) - all = models.Manager() + all = ScopedManager(organizer='order__event__organizer') objects = ActivePositionManager() class Meta: @@ -1951,6 +1960,8 @@ class CartPosition(AbstractPosition): ) is_bundled = models.BooleanField(default=False) + objects = ScopedManager(organizer='event__organizer') + class Meta: verbose_name = _("Cart position") verbose_name_plural = _("Cart positions") @@ -2000,6 +2011,8 @@ class InvoiceAddress(models.Model): blank=True ) + objects = ScopedManager(organizer='order__event__organizer') + def save(self, **kwargs): if self.order: self.order.touch() diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 053937d1e5..3f79ad0fc4 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -8,6 +8,7 @@ from django.db.models import Q from django.utils.crypto import get_random_string from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext_lazy as _ +from django_scopes import ScopedManager from ..decimal import round_decimal from .base import LoggedModel @@ -173,6 +174,8 @@ class Voucher(LoggedModel): "convenience.") ) + objects = ScopedManager(organizer='event__organizer') + class Meta: verbose_name = _("Voucher") verbose_name_plural = _("Vouchers") diff --git a/src/pretix/base/models/waitinglist.py b/src/pretix/base/models/waitinglist.py index bf283e55b2..a68ada92e2 100644 --- a/src/pretix/base/models/waitinglist.py +++ b/src/pretix/base/models/waitinglist.py @@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError from django.db import models, transaction from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext_lazy as _ +from django_scopes import ScopedManager from pretix.base.i18n import language from pretix.base.models import Voucher @@ -67,6 +68,8 @@ class WaitingListEntry(LoggedModel): ) priority = models.IntegerField(default=0) + objects = ScopedManager(organizer='event__organizer') + class Meta: verbose_name = _("Waiting list entry") verbose_name_plural = _("Waiting list entries") diff --git a/src/pretix/base/views/metrics.py b/src/pretix/base/views/metrics.py index f94479b46f..fd238c46b0 100644 --- a/src/pretix/base/views/metrics.py +++ b/src/pretix/base/views/metrics.py @@ -3,6 +3,7 @@ import hmac from django.conf import settings from django.http import HttpResponse +from django_scopes import scopes_disabled from .. import metrics @@ -15,6 +16,7 @@ def unauthed_response(): return response +@scopes_disabled() def serve_metrics(request): if not settings.METRICS_ENABLED: return unauthed_response() diff --git a/src/pretix/control/forms/checkin.py b/src/pretix/control/forms/checkin.py index e3e1106f0a..72f6de7aa2 100644 --- a/src/pretix/control/forms/checkin.py +++ b/src/pretix/control/forms/checkin.py @@ -1,6 +1,7 @@ from django import forms from django.urls import reverse from django.utils.translation import pgettext_lazy +from django_scopes.forms import SafeModelMultipleChoiceField, SafeModelChoiceField from pretix.base.models.checkin import CheckinList from pretix.control.forms.widgets import Select2 @@ -44,3 +45,7 @@ class CheckinListForm(forms.ModelForm): 'data-inverse-dependency': '<[name$=all_products]' }), } + field_classes = { + 'limit_products': SafeModelMultipleChoiceField, + 'subevent': SafeModelChoiceField, + } diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 9cd705ab95..a4eaeec3d1 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -6,6 +6,7 @@ from django.urls import reverse from django.utils.translation import ( pgettext_lazy, ugettext as __, ugettext_lazy as _, ) +from django_scopes.forms import SafeModelMultipleChoiceField, SafeModelChoiceField from i18nfield.forms import I18nFormField, I18nTextarea from pretix.base.channels import get_all_sales_channels @@ -94,6 +95,10 @@ class QuestionForm(I18nModelForm): ), 'dependency_value': forms.Select, } + field_classes = { + 'items': SafeModelMultipleChoiceField, + 'dependency_question': SafeModelChoiceField, + } class QuestionOptionForm(I18nModelForm): @@ -159,6 +164,9 @@ class QuotaForm(I18nModelForm): 'size', 'subevent' ] + field_classes = { + 'subevent': SafeModelChoiceField, + } def save(self, *args, **kwargs): creating = not self.instance.pk diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 190bf4ba22..f1af1e20ed 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -192,7 +192,7 @@ class OrderPositionAddForm(forms.Form): label=_('Product') ) addon_to = forms.ModelChoiceField( - OrderPosition.objects.none(), + OrderPosition.all.none(), required=False, label=_('Add-on to'), ) diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index 37395978f0..4c01f06f1e 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -1,3 +1,4 @@ +from django_scopes.forms import SafeModelChoiceField from urllib.parse import urlparse from django import forms @@ -149,6 +150,9 @@ class TeamForm(forms.ModelForm): 'data-inverse-dependency': '#id_all_events' }), } + field_classes = { + 'limit_events': SafeModelChoiceField + } def clean(self): data = super().clean() @@ -177,6 +181,9 @@ class DeviceForm(forms.ModelForm): 'data-inverse-dependency': '#id_all_events' }), } + field_classes = { + 'limit_events': SafeModelChoiceField + } class OrganizerSettingsForm(SettingsForm): @@ -307,3 +314,6 @@ class WebHookForm(forms.ModelForm): 'data-inverse-dependency': '#id_all_events' }), } + field_classes = { + 'limit_events': SafeModelChoiceField + } diff --git a/src/pretix/control/forms/vouchers.py b/src/pretix/control/forms/vouchers.py index d731675305..5288b855eb 100644 --- a/src/pretix/control/forms/vouchers.py +++ b/src/pretix/control/forms/vouchers.py @@ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db.models.functions import Lower from django.urls import reverse from django.utils.translation import pgettext_lazy, ugettext_lazy as _ +from django_scopes.forms import SafeModelChoiceField from pretix.base.forms import I18nModelForm from pretix.base.models import Item, Voucher @@ -35,6 +36,7 @@ class VoucherForm(I18nModelForm): ] field_classes = { 'valid_until': SplitDateTimeField, + 'subevent': SafeModelChoiceField, } widgets = { 'valid_until': SplitDateTimePickerWidget(), @@ -199,6 +201,7 @@ class VoucherBulkForm(VoucherForm): ] field_classes = { 'valid_until': SplitDateTimeField, + 'subevent': SafeModelChoiceField, } widgets = { 'valid_until': SplitDateTimePickerWidget(), diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index 45d07d1a30..30554e6ea0 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -1,3 +1,4 @@ +from django_scopes import scope from urllib.parse import quote, urljoin, urlparse from django.conf import settings @@ -17,7 +18,7 @@ from pretix.helpers.security import ( ) -class PermissionMiddleware(MiddlewareMixin): +class PermissionMiddleware: """ This middleware enforces all requests to the control app to require login. Additionally, it enforces all requests to "control:event." URLs @@ -34,6 +35,10 @@ class PermissionMiddleware(MiddlewareMixin): "user.settings.notifications.off", ) + def __init__(self, get_response=None): + self.get_response = get_response + super().__init__() + def _login_redirect(self, request): # Taken from django/contrib/auth/decorators.py path = request.build_absolute_uri() @@ -52,19 +57,19 @@ class PermissionMiddleware(MiddlewareMixin): return redirect_to_login( path, resolved_login_url, REDIRECT_FIELD_NAME) - def process_request(self, request): + def __call__(self, request): url = resolve(request.path_info) url_name = url.url_name if not request.path.startswith(get_script_prefix() + 'control'): # This middleware should only touch the /control subpath - return + return self.get_response(request) if hasattr(request, 'organizer'): # If the user is on a organizer's subdomain, he should be redirected to pretix return redirect(urljoin(settings.SITE_URL, request.get_full_path())) if url_name in self.EXCEPTIONS: - return + return self.get_response(request) if not request.user.is_authenticated: return self._login_redirect(request) @@ -79,10 +84,11 @@ class PermissionMiddleware(MiddlewareMixin): return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path())) if 'event' in url.kwargs and 'organizer' in url.kwargs: - request.event = Event.objects.filter( - slug=url.kwargs['event'], - organizer__slug=url.kwargs['organizer'], - ).select_related('organizer').first() + with scope(organizer=None): + request.event = Event.objects.filter( + slug=url.kwargs['event'], + organizer__slug=url.kwargs['organizer'], + ).select_related('organizer').first() if not request.event or not request.user.has_event_permission(request.event.organizer, request.event, request=request): raise Http404(_("The selected event was not found or you " @@ -104,6 +110,9 @@ class PermissionMiddleware(MiddlewareMixin): else: request.orgapermset = request.user.get_organizer_permission_set(request.organizer) + with scope(organizer=getattr(request, 'organizer', None)): + return self.get_response(request) + class AuditLogMiddleware: diff --git a/src/pretix/plugins/pretixdroid/forms.py b/src/pretix/plugins/pretixdroid/forms.py index 508da8398e..fe93d7536e 100644 --- a/src/pretix/plugins/pretixdroid/forms.py +++ b/src/pretix/plugins/pretixdroid/forms.py @@ -1,6 +1,7 @@ from django import forms from django.urls import reverse from django.utils.translation import ugettext_lazy as _ +from django_scopes.forms import SafeModelMultipleChoiceField, SafeModelChoiceField from pretix.control.forms.widgets import Select2 from pretix.plugins.pretixdroid.models import AppConfiguration @@ -16,6 +17,10 @@ class AppConfigurationForm(forms.ModelForm): }), 'app': forms.RadioSelect } + field_classes = { + 'items': SafeModelMultipleChoiceField, + 'list': SafeModelChoiceField, + } def __init__(self, **kwargs): self.event = kwargs.pop('event') diff --git a/src/pretix/presale/middleware.py b/src/pretix/presale/middleware.py index c7d468c221..8650400157 100644 --- a/src/pretix/presale/middleware.py +++ b/src/pretix/presale/middleware.py @@ -1,25 +1,33 @@ from django.urls import resolve from django.utils.deprecation import MiddlewareMixin +from django_scopes import scope from pretix.presale.signals import process_response from .utils import _detect_event -class EventMiddleware(MiddlewareMixin): - def process_request(self, request): +class EventMiddleware: + def __init__(self, get_response=None): + self.get_response = get_response + super().__init__() + + def __call__(self, request): url = resolve(request.path_info) request._namespace = url.namespace if url.namespace != 'presale': - return + return self.get_response(request) if 'organizer' in url.kwargs or 'event' in url.kwargs: redirect = _detect_event(request, require_live=url.url_name != 'event.widget.productlist') if redirect: return redirect - def process_response(self, request, response): - if hasattr(request, '_namespace') and request._namespace == 'presale' and hasattr(request, 'event'): - for receiver, r in process_response.send(request.event, request=request, response=response): - response = r + with scope(organizer=getattr(request, 'organizer', None)): + response = self.get_response(request) + + if hasattr(request, '_namespace') and request._namespace == 'presale' and hasattr(request, 'event'): + for receiver, r in process_response.send(request.event, request=request, response=response): + response = r + return response diff --git a/src/pretix/presale/utils.py b/src/pretix/presale/utils.py index ff6496599e..90e45fbb6e 100644 --- a/src/pretix/presale/utils.py +++ b/src/pretix/presale/utils.py @@ -1,4 +1,5 @@ import warnings +from django_scopes import scope from importlib import import_module from urllib.parse import urljoin @@ -17,6 +18,7 @@ from pretix.presale.signals import process_request, process_response SessionStore = import_module(settings.SESSION_ENGINE).SessionStore +@scope(organizer=None) def _detect_event(request, require_live=True, require_plugin=None): if hasattr(request, '_event_detected'): return @@ -151,10 +153,11 @@ def _event_view(function=None, require_live=True, require_plugin=None): if ret: return ret else: - response = func(request=request, *args, **kwargs) - for receiver, r in process_response.send(request.event, request=request, response=response): - response = r - return response + with scope(organizer=getattr(request, 'organizer', None)): + response = func(request=request, *args, **kwargs) + for receiver, r in process_response.send(request.event, request=request, response=response): + response = r + return response for attrname in dir(func): # Preserve flags like csrf_exempt diff --git a/src/pretix/settings.py b/src/pretix/settings.py index c871b2b0e1..ecefc5244f 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -343,6 +343,7 @@ MIDDLEWARE = [ 'pretix.base.middleware.LocaleMiddleware', 'pretix.base.middleware.SecurityMiddleware', 'pretix.presale.middleware.EventMiddleware', + 'pretix.api.middleware.ApiScopeMiddleware', ] try: diff --git a/src/requirements/production.txt b/src/requirements/production.txt index be498da42f..fe1b1f1240 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -9,6 +9,7 @@ django-formset-js-improved==0.5.0.2 django-compressor==2.2.* django-hierarkey==1.0.*,>=1.0.3 django-filter==2.1.* +django-scopes==1.1.* reportlab==3.5.* PyPDF2==1.26.* Pillow==5.* diff --git a/src/setup.py b/src/setup.py index 8c2e5dc00b..0077a28b10 100644 --- a/src/setup.py +++ b/src/setup.py @@ -97,6 +97,7 @@ setup( 'django-compressor==2.2.*', 'django-hierarkey==1.0.*,>=1.0.2', 'django-filter==2.1.*', + 'django-scopes==1.1.*', 'reportlab==3.5.*', 'Pillow==5.*', 'PyPDF2==1.26.*', diff --git a/src/tests/api/conftest.py b/src/tests/api/conftest.py index 5f2242a188..cc96cf8bad 100644 --- a/src/tests/api/conftest.py +++ b/src/tests/api/conftest.py @@ -1,7 +1,9 @@ from datetime import datetime import pytest +from django.test import utils from django.utils.timezone import now +from django_scopes import scopes_disabled from pytz import UTC from rest_framework.test import APIClient @@ -144,3 +146,6 @@ def taxrule(event): @pytest.fixture def taxrule2(event2): return event2.tax_rules.create(name="VAT", rate=25) + + +utils.setup_databases = scopes_disabled()(utils.setup_databases)