From 9c3806a17542ae6dc7ea4ece4ba87670172e556b Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 29 Dec 2022 11:46:25 -0700 Subject: [PATCH] Refactor link header and fix empty page case --- api/pagination.py | 30 ++++++++++++++++++++++++------ api/views/accounts.py | 25 +++++++++++-------------- api/views/notifications.py | 8 +------- api/views/timelines.py | 25 +++++++------------------ 4 files changed, 43 insertions(+), 45 deletions(-) diff --git a/api/pagination.py b/api/pagination.py index 8ebbd92..7b12d87 100644 --- a/api/pagination.py +++ b/api/pagination.py @@ -7,11 +7,19 @@ from django.http import HttpRequest @dataclasses.dataclass class PaginationResult: + """ + Represents a pagination result for Mastodon (it does Link header stuff) + """ + #: A list of objects that matched the pagination query. results: list[models.Model] + #: The actual applied limit, which may be different from what was requested. limit: int - sort_attribute: str + + @classmethod + def empty(cls): + return cls(results=[], limit=20) def next(self, request: HttpRequest, allowed_params: list[str]): """ @@ -37,6 +45,17 @@ class PaginationResult: return f"{request.build_absolute_uri(request.path)}?{urllib.parse.urlencode(params)}" + def link_header(self, request: HttpRequest, allowed_params: list[str]): + """ + Creates a link header for the given request + """ + return ", ".join( + ( + f'<{self.next(request, allowed_params)}>; rel="next"', + f'<{self.prev(request, allowed_params)}>; rel="prev"', + ) + ) + @staticmethod def filter_params(request: HttpRequest, allowed_params: list[str]): params = {} @@ -71,12 +90,12 @@ class MastodonPaginator: max_id: str | None, since_id: str | None, limit: int | None, - ): + ) -> PaginationResult: if max_id: try: anchor = self.anchor_model.objects.get(pk=max_id) except self.anchor_model.DoesNotExist: - return [] + return PaginationResult.empty() queryset = queryset.filter( **{self.sort_attribute + "__lt": getattr(anchor, self.sort_attribute)} ) @@ -85,7 +104,7 @@ class MastodonPaginator: try: anchor = self.anchor_model.objects.get(pk=since_id) except self.anchor_model.DoesNotExist: - return [] + return PaginationResult.empty() queryset = queryset.filter( **{self.sort_attribute + "__gt": getattr(anchor, self.sort_attribute)} ) @@ -96,7 +115,7 @@ class MastodonPaginator: try: anchor = self.anchor_model.objects.get(pk=min_id) except self.anchor_model.DoesNotExist: - return [] + return PaginationResult.empty() queryset = queryset.filter( **{self.sort_attribute + "__gt": getattr(anchor, self.sort_attribute)} ).order_by(self.sort_attribute) @@ -107,5 +126,4 @@ class MastodonPaginator: return PaginationResult( results=list(queryset[:limit]), limit=limit, - sort_attribute=self.sort_attribute, ) diff --git a/api/views/accounts.py b/api/views/accounts.py index 71a5c74..f3328b2 100644 --- a/api/views/accounts.py +++ b/api/views/accounts.py @@ -132,20 +132,17 @@ def account_statuses( ) if pager.results: - params = [ - "limit", - "id", - "exclude_reblogs", - "exclude_replies", - "only_media", - "pinned", - "tagged", - ] - response.headers["Link"] = ", ".join( - ( - f'<{pager.next(request, params)}>; rel="next"', - f'<{pager.prev(request, params)}>; rel="prev"', - ) + response.headers["Link"] = pager.link_header( + request, + [ + "limit", + "id", + "exclude_reblogs", + "exclude_replies", + "only_media", + "pinned", + "tagged", + ], ) interactions = PostInteraction.get_post_interactions( diff --git a/api/views/notifications.py b/api/views/notifications.py index 79a3a7d..1d31e40 100644 --- a/api/views/notifications.py +++ b/api/views/notifications.py @@ -45,13 +45,7 @@ def notifications( ) if pager.results: - params = ["limit", "account_id"] - response.headers["Link"] = ", ".join( - ( - f'<{pager.next(request, params)}>; rel="next"', - f'<{pager.prev(request, params)}>; rel="prev"', - ) - ) + response.headers["Link"] = pager.link_header(request, ["limit", "account_id"]) interactions = PostInteraction.get_event_interactions( pager.results, request.identity diff --git a/api/views/timelines.py b/api/views/timelines.py index 1ac4964..e767fde 100644 --- a/api/views/timelines.py +++ b/api/views/timelines.py @@ -33,12 +33,7 @@ def home( ) if pager.results: - response.headers["Link"] = ", ".join( - ( - f"<{pager.next(request, ['limit'])}>; rel=\"next\"", - f"<{pager.prev(request, ['limit'])}>; rel=\"prev\"", - ) - ) + response.headers["Link"] = pager.link_header(request, ["limit"]) return [ event.to_mastodon_status_json(interactions=interactions) @@ -79,12 +74,9 @@ def public( ) if pager.results: - params = ["limit", "local", "remote", "only_media"] - response.headers["Link"] = ", ".join( - ( - f'<{pager.next(request, params)}>; rel="next"', - f'<{pager.prev(request, params)}>; rel="prev"', - ) + response.headers["Link"] = pager.link_header( + request, + ["limit", "local", "remote", "only_media"], ) interactions = PostInteraction.get_post_interactions( @@ -123,12 +115,9 @@ def hashtag( ) if pager.results: - params = ["limit", "local", "hashtag", "only_media"] - response.headers["Link"] = ", ".join( - ( - f'<{pager.next(request, params)}>; rel="next"', - f'<{pager.prev(request, params)}>; rel="prev"', - ) + response.headers["Link"] = pager.link_header( + request, + ["limit", "local", "remote", "only_media"], ) interactions = PostInteraction.get_post_interactions(