Fix page ordering (#535)

This commit is contained in:
Dan Watson 2023-03-10 11:10:34 -05:00 committed by GitHub
parent 6e8149675c
commit 61830a9a9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 33 additions and 44 deletions

View file

@ -216,57 +216,45 @@ class MastodonPaginator:
max_id: str | None, max_id: str | None,
since_id: str | None, since_id: str | None,
limit: int | None, limit: int | None,
home: bool = False,
) -> PaginationResult[TM]: ) -> PaginationResult[TM]:
# These "does not start with interaction" checks can be removed after a
# couple months, when clients have flushed them out.
if max_id and not max_id.startswith("interaction"):
queryset = queryset.filter(id__lt=max_id)
if since_id and not since_id.startswith("interaction"):
queryset = queryset.filter(id__gt=since_id)
if min_id and not min_id.startswith("interaction"):
# Min ID requires items _immediately_ newer than specified, so we
# invert the ordering to accommodate
queryset = queryset.filter(id__gt=min_id).order_by("id")
else:
queryset = queryset.order_by("-id")
limit = min(limit or self.default_limit, self.max_limit) limit = min(limit or self.default_limit, self.max_limit)
return PaginationResult( filters = {}
results=list(queryset[:limit]), id_field = "id"
limit=limit, reverse = False
) if home:
# The home timeline interleaves Post IDs and PostInteraction IDs in an
def paginate_home( # annotated field called "subject_id".
self, id_field = "subject_id"
queryset,
min_id: str | None,
max_id: str | None,
since_id: str | None,
limit: int | None,
) -> PaginationResult:
"""
The home timeline requires special handling where we mix Posts and
PostInteractions together.
"""
queryset = queryset.annotate( queryset = queryset.annotate(
event_id=Case( subject_id=Case(
When(type=TimelineEvent.Types.post, then=F("subject_post_id")), When(type=TimelineEvent.Types.post, then=F("subject_post_id")),
default=F("subject_post_interaction"), default=F("subject_post_interaction"),
) )
) )
# These "does not start with interaction" checks can be removed after a
# couple months, when clients have flushed them out.
if max_id and not max_id.startswith("interaction"): if max_id and not max_id.startswith("interaction"):
queryset = queryset.filter(event_id__lt=max_id) filters[f"{id_field}__lt"] = max_id
if since_id and not since_id.startswith("interaction"): if since_id and not since_id.startswith("interaction"):
queryset = queryset.filter(event_id__gt=since_id) filters[f"{id_field}__gt"] = since_id
if min_id and not min_id.startswith("interaction"): if min_id and not min_id.startswith("interaction"):
# Min ID requires items _immediately_ newer than specified, so we # Min ID requires items _immediately_ newer than specified, so we
# invert the ordering to accommodate # invert the ordering to accommodate
queryset = queryset.filter(event_id__gt=min_id).order_by("event_id") filters[f"{id_field}__gt"] = min_id
else: reverse = True
queryset = queryset.order_by("-event_id")
# Default is to order by ID descending (newest first), except for min_id
# queries, which should order by ID for limiting, then reverse the results to be
# consistent. The clearest explanation of this I've found so far is this:
# https://mastodon.social/@Gargron/100846335353411164
ordering = id_field if reverse else f"-{id_field}"
results = list(queryset.filter(**filters).order_by(ordering)[:limit])
if reverse:
results.reverse()
limit = min(limit or self.default_limit, self.max_limit)
return PaginationResult( return PaginationResult(
results=list(queryset[:limit]), results=results,
limit=limit, limit=limit,
) )

View file

@ -1,7 +1,7 @@
from django.http import HttpRequest from django.http import HttpRequest
from hatchway import ApiError, ApiResponse, api_view from hatchway import ApiError, ApiResponse, api_view
from activities.models import Post from activities.models import Post, TimelineEvent
from activities.services import TimelineService from activities.services import TimelineService
from api import schemas from api import schemas
from api.decorators import scope_required from api.decorators import scope_required
@ -34,12 +34,13 @@ def home(
"subject_post_interaction__post__mentions__domain", "subject_post_interaction__post__mentions__domain",
"subject_post_interaction__post__author__posts", "subject_post_interaction__post__author__posts",
) )
pager = paginator.paginate_home( pager: PaginationResult[TimelineEvent] = paginator.paginate(
queryset, queryset,
min_id=min_id, min_id=min_id,
max_id=max_id, max_id=max_id,
since_id=since_id, since_id=since_id,
limit=limit, limit=limit,
home=True,
) )
return PaginatingApiResponse( return PaginatingApiResponse(
schemas.Status.map_from_timeline_event(pager.results, request.identity), schemas.Status.map_from_timeline_event(pager.results, request.identity),