Add generic paginator for API
This commit is contained in:
parent
f892c0c4ce
commit
7f02d51ba0
45
api/pagination.py
Normal file
45
api/pagination.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
class MastodonPaginator:
|
||||||
|
"""
|
||||||
|
Paginates in the Mastodon style (max_id, min_id, etc)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
anchor_model,
|
||||||
|
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
|
||||||
|
|
||||||
|
def paginate(
|
||||||
|
self,
|
||||||
|
queryset,
|
||||||
|
min_id: str | None,
|
||||||
|
max_id: str | None,
|
||||||
|
since_id: str | None,
|
||||||
|
limit: int | None,
|
||||||
|
):
|
||||||
|
if max_id:
|
||||||
|
anchor = self.anchor_model.objects.get(pk=max_id)
|
||||||
|
queryset = queryset.filter(
|
||||||
|
**{self.sort_attribute + "__lt": getattr(anchor, self.sort_attribute)}
|
||||||
|
)
|
||||||
|
if since_id:
|
||||||
|
anchor = self.anchor_model.objects.get(pk=since_id)
|
||||||
|
queryset = queryset.filter(
|
||||||
|
**{self.sort_attribute + "__gt": getattr(anchor, self.sort_attribute)}
|
||||||
|
)
|
||||||
|
if min_id:
|
||||||
|
# Min ID requires items _immediately_ newer than specified, so we
|
||||||
|
# invert the ordering to accomodate
|
||||||
|
anchor = self.anchor_model.objects.get(pk=min_id)
|
||||||
|
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)
|
||||||
|
return list(queryset[: min(limit or self.default_limit, self.max_limit)])
|
@ -3,6 +3,7 @@ from django.shortcuts import get_object_or_404
|
|||||||
from activities.models import Post, PostInteraction
|
from activities.models import Post, PostInteraction
|
||||||
from api import schemas
|
from api import schemas
|
||||||
from api.decorators import identity_required
|
from api.decorators import identity_required
|
||||||
|
from api.pagination import MastodonPaginator
|
||||||
from api.views.base import api_router
|
from api.views.base import api_router
|
||||||
from users.models import Identity
|
from users.models import Identity
|
||||||
|
|
||||||
@ -67,7 +68,7 @@ def account_statuses(
|
|||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
):
|
):
|
||||||
identity = get_object_or_404(Identity, pk=id)
|
identity = get_object_or_404(Identity, pk=id)
|
||||||
posts = (
|
queryset = (
|
||||||
identity.posts.not_hidden()
|
identity.posts.not_hidden()
|
||||||
.unlisted(include_replies=not exclude_replies)
|
.unlisted(include_replies=not exclude_replies)
|
||||||
.select_related("author")
|
.select_related("author")
|
||||||
@ -77,20 +78,16 @@ def account_statuses(
|
|||||||
if pinned:
|
if pinned:
|
||||||
return []
|
return []
|
||||||
if only_media:
|
if only_media:
|
||||||
posts = posts.filter(attachments__pk__isnull=False)
|
queryset = queryset.filter(attachments__pk__isnull=False)
|
||||||
if tagged:
|
if tagged:
|
||||||
posts = posts.tagged_with(tagged)
|
queryset = queryset.tagged_with(tagged)
|
||||||
if max_id:
|
paginator = MastodonPaginator(Post)
|
||||||
anchor_post = Post.objects.get(pk=max_id)
|
posts = paginator.paginate(
|
||||||
posts = posts.filter(created__lt=anchor_post.created)
|
queryset,
|
||||||
if since_id:
|
min_id=min_id,
|
||||||
anchor_post = Post.objects.get(pk=since_id)
|
max_id=max_id,
|
||||||
posts = posts.filter(created__gt=anchor_post.created)
|
since_id=since_id,
|
||||||
if min_id:
|
limit=limit,
|
||||||
# Min ID requires LIMIT posts _immediately_ newer than specified, so we
|
)
|
||||||
# invert the ordering to accomodate
|
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
|
||||||
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
|
|
||||||
posts = list(posts[:limit])
|
|
||||||
interactions = PostInteraction.get_post_interactions(posts, request.identity)
|
interactions = PostInteraction.get_post_interactions(posts, request.identity)
|
||||||
return [post.to_mastodon_json(interactions=interactions) for post in posts]
|
return [post.to_mastodon_json(interactions=interactions) for post in queryset]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from activities.models import Post, PostInteraction, TimelineEvent
|
from activities.models import PostInteraction, TimelineEvent
|
||||||
from api import schemas
|
from api import schemas
|
||||||
from api.decorators import identity_required
|
from api.decorators import identity_required
|
||||||
|
from api.pagination import MastodonPaginator
|
||||||
from api.views.base import api_router
|
from api.views.base import api_router
|
||||||
|
|
||||||
|
|
||||||
@ -14,8 +15,6 @@ def notifications(
|
|||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
account_id: str | None = None,
|
account_id: str | None = None,
|
||||||
):
|
):
|
||||||
if limit > 40:
|
|
||||||
limit = 40
|
|
||||||
# Types/exclude_types use weird syntax so we have to handle them manually
|
# Types/exclude_types use weird syntax so we have to handle them manually
|
||||||
base_types = {
|
base_types = {
|
||||||
"favourite": TimelineEvent.Types.liked,
|
"favourite": TimelineEvent.Types.liked,
|
||||||
@ -29,7 +28,7 @@ def notifications(
|
|||||||
requested_types = set(base_types.keys())
|
requested_types = set(base_types.keys())
|
||||||
requested_types.difference_update(excluded_types)
|
requested_types.difference_update(excluded_types)
|
||||||
# Use that to pull relevant events
|
# Use that to pull relevant events
|
||||||
events = (
|
queryset = (
|
||||||
TimelineEvent.objects.filter(
|
TimelineEvent.objects.filter(
|
||||||
identity=request.identity,
|
identity=request.identity,
|
||||||
type__in=[base_types[r] for r in requested_types],
|
type__in=[base_types[r] for r in requested_types],
|
||||||
@ -37,18 +36,14 @@ def notifications(
|
|||||||
.order_by("-created")
|
.order_by("-created")
|
||||||
.select_related("subject_post", "subject_post__author", "subject_identity")
|
.select_related("subject_post", "subject_post__author", "subject_identity")
|
||||||
)
|
)
|
||||||
if max_id:
|
paginator = MastodonPaginator(TimelineEvent)
|
||||||
anchor_post = Post.objects.get(pk=max_id)
|
events = paginator.paginate(
|
||||||
events = events.filter(created__lt=anchor_post.created)
|
queryset,
|
||||||
if since_id:
|
min_id=min_id,
|
||||||
anchor_post = Post.objects.get(pk=since_id)
|
max_id=max_id,
|
||||||
events = events.filter(created__gt=anchor_post.created)
|
since_id=since_id,
|
||||||
if min_id:
|
limit=limit,
|
||||||
# Min ID requires LIMIT events _immediately_ newer than specified, so we
|
)
|
||||||
# invert the ordering to accomodate
|
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
|
||||||
events = events.filter(created__gt=anchor_post.created).order_by("created")
|
|
||||||
events = list(events[:limit])
|
|
||||||
interactions = PostInteraction.get_event_interactions(events, request.identity)
|
interactions = PostInteraction.get_event_interactions(events, request.identity)
|
||||||
return [
|
return [
|
||||||
event.to_mastodon_notification_json(interactions=interactions)
|
event.to_mastodon_notification_json(interactions=interactions)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from activities.models import Post, PostInteraction, TimelineEvent
|
from activities.models import Post, PostInteraction, TimelineEvent
|
||||||
|
from api import schemas
|
||||||
from .. import schemas
|
from api.decorators import identity_required
|
||||||
from ..decorators import identity_required
|
from api.pagination import MastodonPaginator
|
||||||
from .base import api_router
|
from api.views.base import api_router
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/v1/timelines/home", response=list[schemas.Status])
|
@api_router.get("/v1/timelines/home", response=list[schemas.Status])
|
||||||
@ -14,9 +14,8 @@ def home(
|
|||||||
min_id: str | None = None,
|
min_id: str | None = None,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
):
|
):
|
||||||
if limit > 40:
|
paginator = MastodonPaginator(Post)
|
||||||
limit = 40
|
queryset = (
|
||||||
events = (
|
|
||||||
TimelineEvent.objects.filter(
|
TimelineEvent.objects.filter(
|
||||||
identity=request.identity,
|
identity=request.identity,
|
||||||
type__in=[TimelineEvent.Types.post],
|
type__in=[TimelineEvent.Types.post],
|
||||||
@ -25,18 +24,13 @@ def home(
|
|||||||
.prefetch_related("subject_post__attachments")
|
.prefetch_related("subject_post__attachments")
|
||||||
.order_by("-created")
|
.order_by("-created")
|
||||||
)
|
)
|
||||||
if max_id:
|
events = paginator.paginate(
|
||||||
anchor_post = Post.objects.get(pk=max_id)
|
queryset,
|
||||||
events = events.filter(created__lt=anchor_post.created)
|
min_id=min_id,
|
||||||
if since_id:
|
max_id=max_id,
|
||||||
anchor_post = Post.objects.get(pk=since_id)
|
since_id=since_id,
|
||||||
events = events.filter(created__gt=anchor_post.created)
|
limit=limit,
|
||||||
if min_id:
|
)
|
||||||
# Min ID requires LIMIT events _immediately_ newer than specified, so we
|
|
||||||
# invert the ordering to accomodate
|
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
|
||||||
events = events.filter(created__gt=anchor_post.created).order_by("created")
|
|
||||||
events = list(events[:limit])
|
|
||||||
interactions = PostInteraction.get_event_interactions(events, request.identity)
|
interactions = PostInteraction.get_event_interactions(events, request.identity)
|
||||||
return [
|
return [
|
||||||
event.subject_post.to_mastodon_json(interactions=interactions)
|
event.subject_post.to_mastodon_json(interactions=interactions)
|
||||||
@ -56,32 +50,26 @@ def public(
|
|||||||
min_id: str | None = None,
|
min_id: str | None = None,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
):
|
):
|
||||||
if limit > 40:
|
queryset = (
|
||||||
limit = 40
|
|
||||||
posts = (
|
|
||||||
Post.objects.public()
|
Post.objects.public()
|
||||||
.select_related("author")
|
.select_related("author")
|
||||||
.prefetch_related("attachments")
|
.prefetch_related("attachments")
|
||||||
.order_by("-created")
|
.order_by("-created")
|
||||||
)
|
)
|
||||||
if local:
|
if local:
|
||||||
posts = posts.filter(local=True)
|
queryset = queryset.filter(local=True)
|
||||||
elif remote:
|
elif remote:
|
||||||
posts = posts.filter(local=False)
|
queryset = queryset.filter(local=False)
|
||||||
if only_media:
|
if only_media:
|
||||||
posts = posts.filter(attachments__id__isnull=True)
|
queryset = queryset.filter(attachments__id__isnull=True)
|
||||||
if max_id:
|
paginator = MastodonPaginator(Post)
|
||||||
anchor_post = Post.objects.get(pk=max_id)
|
posts = paginator.paginate(
|
||||||
posts = posts.filter(created__lt=anchor_post.created)
|
queryset,
|
||||||
if since_id:
|
min_id=min_id,
|
||||||
anchor_post = Post.objects.get(pk=since_id)
|
max_id=max_id,
|
||||||
posts = posts.filter(created__gt=anchor_post.created)
|
since_id=since_id,
|
||||||
if min_id:
|
limit=limit,
|
||||||
# Min ID requires LIMIT posts _immediately_ newer than specified, so we
|
)
|
||||||
# invert the ordering to accomodate
|
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
|
||||||
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
|
|
||||||
posts = list(posts[:limit])
|
|
||||||
interactions = PostInteraction.get_post_interactions(posts, request.identity)
|
interactions = PostInteraction.get_post_interactions(posts, request.identity)
|
||||||
return [post.to_mastodon_json(interactions=interactions) for post in posts]
|
return [post.to_mastodon_json(interactions=interactions) for post in posts]
|
||||||
|
|
||||||
@ -100,7 +88,7 @@ def hashtag(
|
|||||||
):
|
):
|
||||||
if limit > 40:
|
if limit > 40:
|
||||||
limit = 40
|
limit = 40
|
||||||
posts = (
|
queryset = (
|
||||||
Post.objects.public()
|
Post.objects.public()
|
||||||
.tagged_with(hashtag)
|
.tagged_with(hashtag)
|
||||||
.select_related("author")
|
.select_related("author")
|
||||||
@ -108,21 +96,17 @@ def hashtag(
|
|||||||
.order_by("-created")
|
.order_by("-created")
|
||||||
)
|
)
|
||||||
if local:
|
if local:
|
||||||
posts = posts.filter(local=True)
|
queryset = queryset.filter(local=True)
|
||||||
if only_media:
|
if only_media:
|
||||||
posts = posts.filter(attachments__id__isnull=True)
|
queryset = queryset.filter(attachments__id__isnull=True)
|
||||||
if max_id:
|
paginator = MastodonPaginator(Post)
|
||||||
anchor_post = Post.objects.get(pk=max_id)
|
posts = paginator.paginate(
|
||||||
posts = posts.filter(created__lt=anchor_post.created)
|
queryset,
|
||||||
if since_id:
|
min_id=min_id,
|
||||||
anchor_post = Post.objects.get(pk=since_id)
|
max_id=max_id,
|
||||||
posts = posts.filter(created__gt=anchor_post.created)
|
since_id=since_id,
|
||||||
if min_id:
|
limit=limit,
|
||||||
# Min ID requires LIMIT posts _immediately_ newer than specified, so we
|
)
|
||||||
# invert the ordering to accomodate
|
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
|
||||||
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
|
|
||||||
posts = list(posts[:limit])
|
|
||||||
interactions = PostInteraction.get_post_interactions(posts, request.identity)
|
interactions = PostInteraction.get_post_interactions(posts, request.identity)
|
||||||
return [post.to_mastodon_json(interactions=interactions) for post in posts]
|
return [post.to_mastodon_json(interactions=interactions) for post in posts]
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user