2022-12-29 09:31:32 -08:00
|
|
|
import dataclasses
|
|
|
|
import urllib.parse
|
|
|
|
|
2022-12-11 23:54:51 -08:00
|
|
|
from django.db import models
|
2022-12-29 09:31:32 -08:00
|
|
|
from django.http import HttpRequest
|
|
|
|
|
2022-12-29 13:00:37 -08:00
|
|
|
from activities.models import PostInteraction
|
|
|
|
|
2022-12-29 09:31:32 -08:00
|
|
|
|
|
|
|
@dataclasses.dataclass
|
|
|
|
class PaginationResult:
|
2022-12-29 10:46:25 -08:00
|
|
|
"""
|
|
|
|
Represents a pagination result for Mastodon (it does Link header stuff)
|
|
|
|
"""
|
|
|
|
|
2022-12-29 09:31:32 -08:00
|
|
|
#: A list of objects that matched the pagination query.
|
|
|
|
results: list[models.Model]
|
2022-12-29 10:46:25 -08:00
|
|
|
|
2022-12-29 09:31:32 -08:00
|
|
|
#: The actual applied limit, which may be different from what was requested.
|
|
|
|
limit: int
|
2022-12-29 10:46:25 -08:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def empty(cls):
|
|
|
|
return cls(results=[], limit=20)
|
2022-12-29 09:31:32 -08:00
|
|
|
|
|
|
|
def next(self, request: HttpRequest, allowed_params: list[str]):
|
|
|
|
"""
|
|
|
|
Returns a URL to the next page of results.
|
|
|
|
"""
|
|
|
|
if not self.results:
|
|
|
|
return None
|
|
|
|
|
|
|
|
params = self.filter_params(request, allowed_params)
|
|
|
|
params["max_id"] = self.results[-1].pk
|
|
|
|
|
|
|
|
return f"{request.build_absolute_uri(request.path)}?{urllib.parse.urlencode(params)}"
|
|
|
|
|
|
|
|
def prev(self, request: HttpRequest, allowed_params: list[str]):
|
|
|
|
"""
|
|
|
|
Returns a URL to the previous page of results.
|
|
|
|
"""
|
|
|
|
if not self.results:
|
|
|
|
return None
|
|
|
|
|
|
|
|
params = self.filter_params(request, allowed_params)
|
|
|
|
params["min_id"] = self.results[0].pk
|
|
|
|
|
|
|
|
return f"{request.build_absolute_uri(request.path)}?{urllib.parse.urlencode(params)}"
|
|
|
|
|
2022-12-29 10:46:25 -08:00
|
|
|
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"',
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2022-12-29 09:31:32 -08:00
|
|
|
@staticmethod
|
|
|
|
def filter_params(request: HttpRequest, allowed_params: list[str]):
|
|
|
|
params = {}
|
|
|
|
for key in allowed_params:
|
|
|
|
value = request.GET.get(key, None)
|
|
|
|
if value:
|
|
|
|
params[key] = value
|
|
|
|
return params
|
2022-12-11 23:54:51 -08:00
|
|
|
|
|
|
|
|
2022-12-11 23:38:02 -08:00
|
|
|
class MastodonPaginator:
|
|
|
|
"""
|
2022-12-29 13:00:37 -08:00
|
|
|
Paginates in the Mastodon style (max_id, min_id, etc).
|
2022-12-11 23:38:02 -08:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
2022-12-11 23:54:51 -08:00
|
|
|
anchor_model: type[models.Model],
|
2022-12-11 23:38:02 -08:00
|
|
|
sort_attribute: str = "created",
|
|
|
|
default_limit: int = 20,
|
|
|
|
max_limit: int = 40,
|
|
|
|
):
|
|
|
|
self.anchor_model = anchor_model
|
|
|
|
self.sort_attribute = sort_attribute
|
|
|
|
self.default_limit = default_limit
|
|
|
|
self.max_limit = max_limit
|
|
|
|
|
2022-12-29 13:00:37 -08:00
|
|
|
def get_anchor(self, anchor_id: str):
|
|
|
|
"""
|
|
|
|
Gets an anchor object by ID.
|
|
|
|
It's possible that the anchor object might be an interaction, in which
|
|
|
|
case we recurse down to its post.
|
|
|
|
"""
|
|
|
|
if anchor_id.startswith("interaction-"):
|
|
|
|
try:
|
|
|
|
return PostInteraction.objects.get(pk=anchor_id[12:])
|
|
|
|
except PostInteraction.DoesNotExist:
|
|
|
|
return PaginationResult.empty()
|
|
|
|
try:
|
|
|
|
return self.anchor_model.objects.get(pk=anchor_id)
|
|
|
|
except self.anchor_model.DoesNotExist:
|
|
|
|
return PaginationResult.empty()
|
|
|
|
|
2022-12-11 23:38:02 -08:00
|
|
|
def paginate(
|
|
|
|
self,
|
|
|
|
queryset,
|
|
|
|
min_id: str | None,
|
|
|
|
max_id: str | None,
|
|
|
|
since_id: str | None,
|
|
|
|
limit: int | None,
|
2022-12-29 10:46:25 -08:00
|
|
|
) -> PaginationResult:
|
2022-12-11 23:38:02 -08:00
|
|
|
if max_id:
|
2022-12-29 13:00:37 -08:00
|
|
|
anchor = self.get_anchor(max_id)
|
2022-12-11 23:38:02 -08:00
|
|
|
queryset = queryset.filter(
|
|
|
|
**{self.sort_attribute + "__lt": getattr(anchor, self.sort_attribute)}
|
|
|
|
)
|
2022-12-29 09:31:32 -08:00
|
|
|
|
2022-12-11 23:38:02 -08:00
|
|
|
if since_id:
|
2022-12-29 13:00:37 -08:00
|
|
|
anchor = self.get_anchor(since_id)
|
2022-12-11 23:38:02 -08:00
|
|
|
queryset = queryset.filter(
|
|
|
|
**{self.sort_attribute + "__gt": getattr(anchor, self.sort_attribute)}
|
|
|
|
)
|
2022-12-29 09:31:32 -08:00
|
|
|
|
2022-12-11 23:38:02 -08:00
|
|
|
if min_id:
|
|
|
|
# Min ID requires items _immediately_ newer than specified, so we
|
2022-12-29 09:31:32 -08:00
|
|
|
# invert the ordering to accommodate
|
2022-12-29 13:00:37 -08:00
|
|
|
anchor = self.get_anchor(min_id)
|
2022-12-11 23:38:02 -08:00
|
|
|
queryset = queryset.filter(
|
|
|
|
**{self.sort_attribute + "__gt": getattr(anchor, self.sort_attribute)}
|
|
|
|
).order_by(self.sort_attribute)
|
|
|
|
else:
|
|
|
|
queryset = queryset.order_by("-" + self.sort_attribute)
|
2022-12-29 09:31:32 -08:00
|
|
|
|
|
|
|
limit = min(limit or self.default_limit, self.max_limit)
|
|
|
|
return PaginationResult(
|
|
|
|
results=list(queryset[:limit]),
|
|
|
|
limit=limit,
|
|
|
|
)
|