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,
since_id: str | None,
limit: int | None,
home: bool = False,
) -> 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)
return PaginationResult(
results=list(queryset[:limit]),
limit=limit,
)
def paginate_home(
self,
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.
"""
filters = {}
id_field = "id"
reverse = False
if home:
# The home timeline interleaves Post IDs and PostInteraction IDs in an
# annotated field called "subject_id".
id_field = "subject_id"
queryset = queryset.annotate(
event_id=Case(
subject_id=Case(
When(type=TimelineEvent.Types.post, then=F("subject_post_id")),
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"):
queryset = queryset.filter(event_id__lt=max_id)
filters[f"{id_field}__lt"] = max_id
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"):
# Min ID requires items _immediately_ newer than specified, so we
# invert the ordering to accommodate
queryset = queryset.filter(event_id__gt=min_id).order_by("event_id")
else:
queryset = queryset.order_by("-event_id")
filters[f"{id_field}__gt"] = min_id
reverse = True
# 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(
results=list(queryset[:limit]),
results=results,
limit=limit,
)

View file

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