Ensure total ordering of paginated lists (#3061)

This commit is contained in:
Raphael Michel
2023-02-24 10:51:51 +01:00
committed by GitHub
parent c2d720b3b9
commit 7d4b575150
26 changed files with 232 additions and 77 deletions

View File

@@ -19,9 +19,19 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from rest_framework.filters import OrderingFilter
from rest_framework.pagination import PageNumberPagination
from pretix.helpers import get_deterministic_ordering
class Pagination(PageNumberPagination):
page_size_query_param = 'page_size'
max_page_size = 50
class TotalOrderingFilter(OrderingFilter):
def get_ordering(self, request, queryset, view):
o = super().get_ordering(request, queryset, view)
o = get_deterministic_ordering(queryset.model, o)
return o

View File

@@ -24,10 +24,11 @@ from calendar import timegm
from django.db.models import Max
from django.http import HttpResponse
from django.utils.http import http_date, parse_http_date_safe
from rest_framework.filters import OrderingFilter
from pretix.api.pagination import TotalOrderingFilter
class RichOrderingFilter(OrderingFilter):
class RichOrderingFilter(TotalOrderingFilter):
def filter_queryset(self, request, queryset, view):
ordering = self.get_ordering(request, queryset, view)

View File

@@ -29,11 +29,11 @@ from django.utils.translation import gettext as _
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from rest_framework.serializers import as_serializer_error
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.cart import (
CartPositionCreateSerializer, CartPositionSerializer,
)
@@ -47,7 +47,7 @@ from pretix.base.services.locking import NoLockManager
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = CartPositionSerializer
queryset = CartPosition.objects.none()
filter_backends = (OrderingFilter,)
filter_backends = (TotalOrderingFilter,)
ordering = ('datetime',)
ordering_fields = ('datetime', 'cart_id')
lookup_field = 'id'

View File

@@ -36,8 +36,8 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.discount import DiscountSerializer
from pretix.api.views import ConditionalListView
from pretix.base.models import CartPosition, Discount
@@ -52,7 +52,7 @@ with scopes_disabled():
class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = DiscountSerializer
queryset = Discount.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
filterset_class = DiscountFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')

View File

@@ -39,11 +39,12 @@ from django.db.models import Prefetch, 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, serializers, views, viewsets
from rest_framework import serializers, views, viewsets
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.response import Response
from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
@@ -127,7 +128,7 @@ class EventViewSet(viewsets.ModelViewSet):
lookup_url_kwarg = 'event'
lookup_value_regex = '[^/]+'
permission_classes = (EventCRUDPermission,)
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('slug',)
ordering_fields = ('date_from', 'slug')
filterset_class = EventFilter
@@ -379,7 +380,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SubEventSerializer
queryset = SubEvent.objects.none()
write_permission = 'can_change_event_settings'
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
filterset_class = SubEventFilter
ordering = ('date_from',)
ordering_fields = ('id', 'date_from', 'last_modified')

View File

@@ -41,9 +41,9 @@ from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
@@ -75,7 +75,7 @@ with scopes_disabled():
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
filterset_class = ItemFilter
@@ -138,7 +138,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
class ItemVariationViewSet(viewsets.ModelViewSet):
serializer_class = ItemVariationSerializer
queryset = ItemVariation.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = None
@@ -208,7 +208,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
class ItemBundleViewSet(viewsets.ModelViewSet):
serializer_class = ItemBundleSerializer
queryset = ItemBundle.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
ordering_fields = ('id',)
ordering = ('id',)
permission = None
@@ -260,7 +260,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
class ItemAddOnViewSet(viewsets.ModelViewSet):
serializer_class = ItemAddOnSerializer
queryset = ItemAddOn.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = None
@@ -318,7 +318,7 @@ class ItemCategoryFilter(FilterSet):
class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = ItemCategorySerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
filterset_class = ItemCategoryFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
@@ -373,7 +373,7 @@ with scopes_disabled():
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = QuestionSerializer
queryset = Question.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
filterset_class = QuestionFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
@@ -418,7 +418,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
class QuestionOptionViewSet(viewsets.ModelViewSet):
serializer_class = QuestionOptionSerializer
queryset = QuestionOption.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('position',)
permission = None
@@ -475,7 +475,7 @@ with scopes_disabled():
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = QuotaSerializer
queryset = Quota.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
filterset_class = QuotaFilter
ordering_fields = ('id', 'size')
ordering = ('id',)

View File

@@ -43,11 +43,11 @@ from rest_framework.decorators import action
from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
)
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer,
@@ -181,7 +181,7 @@ with scopes_disabled():
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('datetime',)
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
filterset_class = OrderFilter
@@ -1749,7 +1749,7 @@ class RetryException(APIException):
class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = InvoiceSerializer
queryset = Invoice.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('nr',)
ordering_fields = ('nr', 'date')
filterset_class = InvoiceFilter
@@ -1842,7 +1842,7 @@ with scopes_disabled():
class RevokedSecretViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = RevokedTicketSecretSerializer
queryset = RevokedTicketSecret.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('-created',)
ordering_fields = ('created', 'secret')
filterset_class = RevokedSecretFilter
@@ -1865,7 +1865,7 @@ with scopes_disabled():
class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = BlockedTicketSecretSerializer
queryset = BlockedTicketSecret.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('-updated', '-pk')
filterset_class = BlockedSecretFilter
permission = 'can_view_orders'

View File

@@ -28,9 +28,7 @@ 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 (
filters, mixins, serializers, status, views, viewsets,
)
from rest_framework import mixins, serializers, status, views, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
@@ -38,6 +36,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from pretix.api.models import OAuthAccessToken
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.organizer import (
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
@@ -62,7 +61,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
lookup_field = 'slug'
lookup_url_kwarg = 'organizer'
lookup_value_regex = '[^/]+'
filter_backends = (filters.OrderingFilter,)
filter_backends = (TotalOrderingFilter,)
ordering = ('slug',)
ordering_fields = ('name', 'slug')

View File

@@ -31,9 +31,9 @@ from django_scopes import scopes_disabled
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
@@ -59,7 +59,7 @@ with scopes_disabled():
class VoucherViewSet(viewsets.ModelViewSet):
serializer_class = VoucherSerializer
queryset = Voucher.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('id',)
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
filterset_class = VoucherFilter

View File

@@ -25,9 +25,9 @@ from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.models import WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
@@ -47,8 +47,8 @@ with scopes_disabled():
class WaitingListViewSet(viewsets.ModelViewSet):
serializer_class = WaitingListSerializer
queryset = WaitingListEntry.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('created',)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('created', 'pk',)
ordering_fields = ('id', 'created', 'email', 'item')
filterset_class = WaitingListFilter
permission = 'can_view_orders'

View File

@@ -0,0 +1,56 @@
# Generated by Django 3.2.16 on 2023-02-01 10:58
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0233_ignore_from_quota_while_blocked'),
]
operations = [
migrations.AlterField(
model_name='logentry',
name='datetime',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='order',
name='datetime',
field=models.DateTimeField(),
),
migrations.AlterField(
model_name='order',
name='last_modified',
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name='scheduledeventexport',
name='export_form_data',
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='scheduledorganizerexport',
name='export_form_data',
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='transaction',
name='datetime',
field=models.DateTimeField(),
),
migrations.AlterIndexTogether(
name='logentry',
index_together={('datetime', 'id')},
),
migrations.AlterIndexTogether(
name='order',
index_together={('datetime', 'id'), ('last_modified', 'id')},
),
migrations.AlterIndexTogether(
name='transaction',
index_together={('datetime', 'id')},
),
]

View File

@@ -97,7 +97,7 @@ class CheckinList(LoggedModel):
objects = ScopedManager(organizer='event__organizer')
class Meta:
ordering = ('subevent__date_from', 'name')
ordering = ('subevent__date_from', 'name', 'pk')
def positions_query(self, ignore_status=False):
from . import Order, OrderPosition

View File

@@ -612,7 +612,7 @@ class Event(EventMixin, LoggedModel):
class Meta:
verbose_name = _("Event")
verbose_name_plural = _("Events")
ordering = ("date_from", "name")
ordering = ("date_from", "name", "slug")
unique_together = (('organizer', 'slug'),)
def __str__(self):

View File

@@ -72,7 +72,7 @@ class LogEntry(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(db_index=True)
content_object = GenericForeignKey('content_type', 'object_id')
datetime = models.DateTimeField(auto_now_add=True, db_index=True)
datetime = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
@@ -88,6 +88,9 @@ class LogEntry(models.Model):
class Meta:
ordering = ('-datetime', '-id')
index_together = [
['datetime', 'id']
]
def display(self):
from ..signals import logentry_display

View File

@@ -211,7 +211,7 @@ class Order(LockModel, LoggedModel):
)
secret = models.CharField(max_length=32, default=generate_secret)
datetime = models.DateTimeField(
verbose_name=_("Date"), db_index=True
verbose_name=_("Date"), db_index=False
)
cancellation_date = models.DateTimeField(
null=True, blank=True
@@ -252,7 +252,7 @@ class Order(LockModel, LoggedModel):
null=True, blank=True
)
last_modified = models.DateTimeField(
auto_now=True, db_index=True
auto_now=True, db_index=False
)
require_approval = models.BooleanField(
default=False
@@ -268,7 +268,11 @@ class Order(LockModel, LoggedModel):
class Meta:
verbose_name = _("Order")
verbose_name_plural = _("Orders")
ordering = ("-datetime",)
ordering = ("-datetime", "-pk")
index_together = [
["datetime", "id"],
["last_modified", "id"],
]
def __str__(self):
return self.full_code
@@ -2618,7 +2622,6 @@ class Transaction(models.Model):
)
datetime = models.DateTimeField(
verbose_name=_("Date"),
db_index=True,
)
migrated = models.BooleanField(
default=False
@@ -2669,6 +2672,9 @@ class Transaction(models.Model):
class Meta:
ordering = 'datetime', 'pk'
index_together = [
['datetime', 'id']
]
def save(self, *args, **kwargs):
if not self.fee_type and not self.item:

View File

@@ -93,7 +93,7 @@ class Organizer(LoggedModel):
class Meta:
verbose_name = _("Organizer")
verbose_name_plural = _("Organizers")
ordering = ("name",)
ordering = ("name", "slug")
def __str__(self) -> str:
return self.name

View File

@@ -112,7 +112,7 @@ class WaitingListEntry(LoggedModel):
class Meta:
verbose_name = _("Waiting list entry")
verbose_name_plural = _("Waiting list entries")
ordering = ('-priority', 'created')
ordering = ('-priority', 'created', 'pk')
def __str__(self):
return '%s waits for %s' % (str(self.email), str(self.item))

View File

@@ -58,13 +58,15 @@ from pretix.base.models import (
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent,
SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
)
from pretix.base.signals import register_payment_providers
from pretix.control.forms.widgets import Select2
from pretix.control.signals import order_search_filter_q
from pretix.helpers.countries import CachedCountries
from pretix.helpers.database import rolledback_transaction
from pretix.helpers.database import (
get_deterministic_ordering, rolledback_transaction,
)
from pretix.helpers.dicts import move_to_end
from pretix.helpers.i18n import i18ncomp
@@ -380,7 +382,9 @@ class OrderFilterForm(FilterForm):
)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
qs = qs.order_by(*get_deterministic_ordering(Order, self.get_order_by()))
else:
qs = qs.order_by('-datetime', '-pk')
if fdata.get('provider'):
qs = qs.annotate(
@@ -1044,11 +1048,11 @@ class OrderPaymentSearchFilterForm(forms.Form):
if fdata.get('ordering'):
p = self.cleaned_data.get('ordering')
if p.startswith('-') and p not in self.orders:
qs = qs.order_by('-' + self.orders[p[1:]])
qs = qs.order_by(*get_deterministic_ordering(OrderPayment, '-' + self.orders[p[1:]]))
else:
qs = qs.order_by(self.orders[p])
qs = qs.order_by(*get_deterministic_ordering(OrderPayment, self.orders[p]))
else:
qs = qs.order_by('-created')
qs = qs.order_by('-created', '-pk')
return qs
@@ -1226,9 +1230,9 @@ class SubEventFilterForm(FilterForm):
qs = qs.filter(f)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
qs = qs.order_by(*get_deterministic_ordering(SubEvent, self.get_order_by()))
else:
qs = qs.order_by('-date_from')
qs = qs.order_by('-date_from', '-pk')
return qs
@@ -1462,9 +1466,7 @@ class TeamFilterForm(FilterForm):
)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
qs = qs.order_by('name')
qs = qs.order_by(*get_deterministic_ordering(Team, self.get_order_by()))
return qs.distinct()
@@ -1619,7 +1621,7 @@ class EventFilterForm(FilterForm):
qs = qs.filter(f)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
qs = qs.order_by(*get_deterministic_ordering(Event, self.get_order_by()))
return qs
@@ -1764,11 +1766,11 @@ class CheckinListAttendeeFilterForm(FilterForm):
if isinstance(ob, dict):
ob = dict(ob)
o = ob.pop('_order')
qs = qs.annotate(**ob).order_by(o)
qs = qs.annotate(**ob).order_by(*get_deterministic_ordering(OrderPosition, [o]))
elif isinstance(ob, (list, tuple)):
qs = qs.order_by(*ob)
qs = qs.order_by(*get_deterministic_ordering(OrderPosition, ob))
else:
qs = qs.order_by(ob)
qs = qs.order_by(*get_deterministic_ordering(OrderPosition, [ob]))
if fdata.get('item'):
qs = qs.filter(item=fdata.get('item'))
@@ -2011,11 +2013,11 @@ class VoucherFilterForm(FilterForm):
if isinstance(ob, dict):
ob = dict(ob)
o = ob.pop('_order')
qs = qs.annotate(**ob).order_by(o)
qs = qs.annotate(**ob).order_by(*get_deterministic_ordering(Voucher, o))
elif isinstance(ob, (list, tuple)):
qs = qs.order_by(*ob)
qs = qs.order_by(*get_deterministic_ordering(Voucher, ob))
else:
qs = qs.order_by(ob)
qs = qs.order_by(*get_deterministic_ordering(Voucher, ob))
return qs
@@ -2098,9 +2100,7 @@ class RefundFilterForm(FilterForm):
OrderRefund.REFUND_STATE_EXTERNAL])
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
qs = qs.order_by('-created')
qs = qs.order_by(*get_deterministic_ordering(OrderRefund, self.get_order_by()))
return qs

View File

@@ -447,6 +447,7 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
context_object_name = 'checkins'
permission = 'can_view_orders'
template_name = 'pretixcontrol/checkin/checkins.html'
ordering = ('-datetime', '-pk')
def get_queryset(self):
qs = Checkin.all.filter(

View File

@@ -1061,7 +1061,7 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
def get_queryset(self):
qs = self.request.event.logentry_set.all().select_related(
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime')
).order_by('-datetime', '-pk')
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders',
request=self.request):

View File

@@ -816,16 +816,18 @@ class QuotaList(PaginationMixin, ListView):
qs = qs.filter(subevent_id=s)
valid_orders = {
'-date': ('-subevent__date_from', 'name'),
'date': ('subevent__date_from', '-name'),
'size': ('size', 'name'),
'-size': ('-size', '-name'),
'name': ('name',),
'-name': ('-name',),
'-date': ('-subevent__date_from', 'name', 'pk'),
'date': ('subevent__date_from', '-name', '-pk'),
'size': ('size', 'name', 'pk'),
'-size': ('-size', '-name', '-pk'),
'name': ('name', 'pk'),
'-name': ('-name', '-pk'),
}
if self.request.GET.get("ordering", "-date") in valid_orders:
qs = qs.order_by(*valid_orders[self.request.GET.get("ordering", "-date")])
else:
qs = qs.order_by('name', 'subevent__date_from', 'pk')
return qs

View File

@@ -2529,7 +2529,7 @@ class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView):
def get_queryset(self):
qs = OrderRefund.objects.filter(
order__event=self.request.event
).select_related('order')
).select_related('order').order_by('-created', '-pk')
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)

View File

@@ -556,7 +556,7 @@ class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, P
memcount=Count('members', distinct=True),
eventcount=Count('limit_events', distinct=True),
invcount=Count('invites', distinct=True)
).all().order_by('name')
).all().order_by('name', 'pk')
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
return qs
@@ -2040,6 +2040,8 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
context_object_name = 'logs'
def get_queryset(self):
# technically, we'd also need to sort by pk since this is a paginated list, but in this case we just can't
# bear the performance cost
qs = self.request.organizer.all_logentries().select_related(
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime')
@@ -2437,7 +2439,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
q |= Q(email__iexact=self.customer.email)
qs = Order.objects.filter(
q
).select_related('event').order_by('-datetime')
).select_related('event').order_by('-datetime', 'pk')
return qs
@cached_property

View File

@@ -21,8 +21,11 @@
#
import contextlib
from django.core.exceptions import FieldDoesNotExist
from django.db import connection, transaction
from django.db.models import Aggregate, Expression, Field, Lookup, Value
from django.db.models import (
Aggregate, Expression, F, Field, Lookup, OrderBy, Value,
)
from django.utils.functional import lazy
@@ -148,3 +151,74 @@ class PostgresWindowFrame(Expression):
# This is a short-hand for .select_for_update(of=("self,")), that falls back gracefully on databases that don't support
# the SELECT FOR UPDATE OF ... query.
OF_SELF = lazy(lambda: ("self",) if connection.features.has_select_for_update_of else (), tuple)()
def get_deterministic_ordering(model, ordering):
"""
Ensure a deterministic order across all database backends. Search for a
single field or unique together set of fields providing a total
ordering. If these are missing, augment the ordering with a descendant
primary key.
This has mostly been vendored from
https://github.com/django/django/blob/d8e1442ce2c56282785dd806e5c1147975e8c857/django/contrib/admin/views/main.py#L390
"""
if isinstance(ordering, str):
ordering = (ordering,)
ordering = list(ordering)
ordering_fields = set()
total_ordering_fields = {"pk"} | {
field.attname
for field in model._meta.fields
if field.unique and not field.null
}
for part in ordering:
# Search for single field providing a total ordering.
field_name = None
if isinstance(part, str):
field_name = part.lstrip("-")
elif isinstance(part, F):
field_name = part.name
elif isinstance(part, OrderBy) and isinstance(part.expression, F):
field_name = part.expression.name
if field_name:
# Normalize attname references by using get_field().
try:
field = model._meta.get_field(field_name)
except FieldDoesNotExist:
# Could be "?" for random ordering or a related field
# lookup. Skip this part of introspection for now.
continue
# Ordering by a related field name orders by the referenced
# model's ordering. Skip this part of introspection for now.
if field.remote_field and field_name == field.name:
continue
if field.attname in total_ordering_fields:
break
ordering_fields.add(field.attname)
else:
# No single total ordering field, try unique_together and total
# unique constraints.
constraint_field_names = (
*model._meta.unique_together,
*(
constraint.fields
for constraint in model._meta.total_unique_constraints
),
)
for field_names in constraint_field_names:
# Normalize attname references by using get_field().
fields = [
model._meta.get_field(field_name) for field_name in field_names
]
# Composite unique constraints containing a nullable column
# cannot ensure total ordering.
if any(field.null for field in fields):
continue
if ordering_fields.issuperset(field.attname for field in fields):
break
else:
# If no set of unique fields is present in the ordering, rely
# on the primary key to provide total ordering.
ordering.append("-pk")
return ordering

View File

@@ -23,8 +23,8 @@ from django.core.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.plugins.sendmail.models import Rule
@@ -73,7 +73,7 @@ with scopes_disabled():
class RuleViewSet(viewsets.ModelViewSet):
queryset = Rule.objects.none()
serializer_class = RuleSerializer
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
filterset_class = RuleFilter
ordering = ('id',)
ordering_fields = ('id',)

View File

@@ -700,7 +700,7 @@ class ListRules(EventPermissionRequiredMixin, PaginationMixin, ListView):
),
).prefetch_related(
'limit_products'
)
).order_by('-send_date', 'subject', 'pk')
class DeleteRule(EventPermissionRequiredMixin, DeleteView):
@@ -746,7 +746,7 @@ class ScheduleView(EventPermissionRequiredMixin, PaginationMixin, ListView):
def get_queryset(self):
return self.rule.scheduledmail_set.select_related('subevent').order_by(
'-computed_datetime'
'-computed_datetime', '-pk'
)
def get_context_data(self, **kwargs):