mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-26 01:01:00 +00:00
Fix tests and most of pagination
This commit is contained in:
parent
91116fe6f8
commit
fb881dd5de
7 changed files with 65 additions and 60 deletions
|
@ -117,36 +117,17 @@ class PaginationResult:
|
||||||
class MastodonPaginator:
|
class MastodonPaginator:
|
||||||
"""
|
"""
|
||||||
Paginates in the Mastodon style (max_id, min_id, etc).
|
Paginates in the Mastodon style (max_id, min_id, etc).
|
||||||
|
Note that this basically _requires_ us to always do it on IDs, so we do.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
anchor_model: type[models.Model],
|
|
||||||
sort_attribute: str = "created",
|
|
||||||
default_limit: int = 20,
|
default_limit: int = 20,
|
||||||
max_limit: int = 40,
|
max_limit: int = 40,
|
||||||
):
|
):
|
||||||
self.anchor_model = anchor_model
|
|
||||||
self.sort_attribute = sort_attribute
|
|
||||||
self.default_limit = default_limit
|
self.default_limit = default_limit
|
||||||
self.max_limit = max_limit
|
self.max_limit = max_limit
|
||||||
|
|
||||||
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 None
|
|
||||||
try:
|
|
||||||
return self.anchor_model.objects.get(pk=anchor_id)
|
|
||||||
except self.anchor_model.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def paginate(
|
def paginate(
|
||||||
self,
|
self,
|
||||||
queryset,
|
queryset,
|
||||||
|
@ -156,32 +137,57 @@ class MastodonPaginator:
|
||||||
limit: int | None,
|
limit: int | None,
|
||||||
) -> PaginationResult:
|
) -> PaginationResult:
|
||||||
if max_id:
|
if max_id:
|
||||||
anchor = self.get_anchor(max_id)
|
queryset = queryset.filter(id__lt=max_id)
|
||||||
if anchor is None:
|
|
||||||
return PaginationResult.empty()
|
|
||||||
queryset = queryset.filter(
|
|
||||||
**{self.sort_attribute + "__lt": getattr(anchor, self.sort_attribute)}
|
|
||||||
)
|
|
||||||
|
|
||||||
if since_id:
|
if since_id:
|
||||||
anchor = self.get_anchor(since_id)
|
queryset = queryset.filter(id__gt=since_id)
|
||||||
if anchor is None:
|
|
||||||
return PaginationResult.empty()
|
|
||||||
queryset = queryset.filter(
|
|
||||||
**{self.sort_attribute + "__gt": getattr(anchor, self.sort_attribute)}
|
|
||||||
)
|
|
||||||
|
|
||||||
if min_id:
|
if min_id:
|
||||||
# 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
|
||||||
anchor = self.get_anchor(min_id)
|
queryset = queryset.filter(id__gt=min_id).order_by("id")
|
||||||
if anchor is None:
|
|
||||||
return PaginationResult.empty()
|
|
||||||
queryset = queryset.filter(
|
|
||||||
**{self.sort_attribute + "__gt": getattr(anchor, self.sort_attribute)}
|
|
||||||
).order_by(self.sort_attribute)
|
|
||||||
else:
|
else:
|
||||||
queryset = queryset.order_by("-" + self.sort_attribute)
|
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.
|
||||||
|
"""
|
||||||
|
if max_id:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
models.Q(subject_post_id__lt=max_id)
|
||||||
|
| models.Q(subject_post_interaction_id__lt=max_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if since_id:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
models.Q(subject_post_id__gt=max_id)
|
||||||
|
| models.Q(subject_post_interaction_id__gt=max_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if min_id:
|
||||||
|
# Min ID requires items _immediately_ newer than specified, so we
|
||||||
|
# invert the ordering to accommodate
|
||||||
|
queryset = queryset.filter(
|
||||||
|
models.Q(subject_post_id__gt=max_id)
|
||||||
|
| models.Q(subject_post_interaction_id__gt=max_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(
|
return PaginationResult(
|
||||||
|
|
|
@ -3,7 +3,6 @@ from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from ninja import Field
|
from ninja import Field
|
||||||
|
|
||||||
from activities.models import Post
|
|
||||||
from activities.services import SearchService
|
from activities.services import SearchService
|
||||||
from api import schemas
|
from api import schemas
|
||||||
from api.decorators import identity_required
|
from api.decorators import identity_required
|
||||||
|
@ -151,7 +150,7 @@ def account_statuses(
|
||||||
if tagged:
|
if tagged:
|
||||||
queryset = queryset.tagged_with(tagged)
|
queryset = queryset.tagged_with(tagged)
|
||||||
# Get user posts with pagination
|
# Get user posts with pagination
|
||||||
paginator = MastodonPaginator(Post, sort_attribute="published")
|
paginator = MastodonPaginator()
|
||||||
pager = paginator.paginate(
|
pager = paginator.paginate(
|
||||||
queryset,
|
queryset,
|
||||||
min_id=min_id,
|
min_id=min_id,
|
||||||
|
@ -219,7 +218,7 @@ def account_following(
|
||||||
|
|
||||||
service = IdentityService(identity)
|
service = IdentityService(identity)
|
||||||
|
|
||||||
paginator = MastodonPaginator(Identity, max_limit=80, sort_attribute="username")
|
paginator = MastodonPaginator(max_limit=80)
|
||||||
pager = paginator.paginate(
|
pager = paginator.paginate(
|
||||||
service.following(),
|
service.following(),
|
||||||
min_id=min_id,
|
min_id=min_id,
|
||||||
|
|
|
@ -35,7 +35,7 @@ def notifications(
|
||||||
queryset = TimelineService(request.identity).notifications(
|
queryset = TimelineService(request.identity).notifications(
|
||||||
[base_types[r] for r in requested_types if r in base_types]
|
[base_types[r] for r in requested_types if r in base_types]
|
||||||
)
|
)
|
||||||
paginator = MastodonPaginator(TimelineEvent)
|
paginator = MastodonPaginator()
|
||||||
pager = paginator.paginate(
|
pager = paginator.paginate(
|
||||||
queryset,
|
queryset,
|
||||||
min_id=min_id,
|
min_id=min_id,
|
||||||
|
|
|
@ -16,7 +16,6 @@ from activities.services import PostService
|
||||||
from api import schemas
|
from api import schemas
|
||||||
from api.views.base import api_router
|
from api.views.base import api_router
|
||||||
from core.models import Config
|
from core.models import Config
|
||||||
from users.models import Identity
|
|
||||||
|
|
||||||
from ..decorators import identity_required
|
from ..decorators import identity_required
|
||||||
from ..pagination import MastodonPaginator
|
from ..pagination import MastodonPaginator
|
||||||
|
@ -142,20 +141,18 @@ def favourited_by(
|
||||||
# a concept of "private status" yet.
|
# a concept of "private status" yet.
|
||||||
post = get_object_or_404(Post, pk=id)
|
post = get_object_or_404(Post, pk=id)
|
||||||
|
|
||||||
paginator = MastodonPaginator(Identity, sort_attribute="published")
|
paginator = MastodonPaginator()
|
||||||
pager = paginator.paginate(
|
pager = paginator.paginate(
|
||||||
post.interactions.filter(
|
post.interactions.filter(
|
||||||
type=PostInteraction.Types.like,
|
type=PostInteraction.Types.like,
|
||||||
state__in=PostInteractionStates.group_active(),
|
state__in=PostInteractionStates.group_active(),
|
||||||
)
|
).select_related("identity"),
|
||||||
.select_related("identity")
|
|
||||||
.order_by("published"),
|
|
||||||
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,
|
||||||
)
|
)
|
||||||
pager.jsonify_identities()
|
pager.jsonify_results(lambda r: r.identity.to_mastodon_json(include_counts=False))
|
||||||
|
|
||||||
if pager.results:
|
if pager.results:
|
||||||
response.headers["Link"] = pager.link_header(
|
response.headers["Link"] = pager.link_header(
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
|
|
||||||
from activities.models import Post
|
|
||||||
from activities.services import TimelineService
|
from activities.services import TimelineService
|
||||||
from api import schemas
|
from api import schemas
|
||||||
from api.decorators import identity_required
|
from api.decorators import identity_required
|
||||||
|
@ -20,9 +19,9 @@ def home(
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
):
|
):
|
||||||
# Grab a paginated result set of instances
|
# Grab a paginated result set of instances
|
||||||
paginator = MastodonPaginator(Post, sort_attribute="published")
|
paginator = MastodonPaginator()
|
||||||
queryset = TimelineService(request.identity).home()
|
queryset = TimelineService(request.identity).home()
|
||||||
pager = paginator.paginate(
|
pager = paginator.paginate_home(
|
||||||
queryset,
|
queryset,
|
||||||
min_id=min_id,
|
min_id=min_id,
|
||||||
max_id=max_id,
|
max_id=max_id,
|
||||||
|
@ -61,7 +60,7 @@ def public(
|
||||||
if only_media:
|
if only_media:
|
||||||
queryset = queryset.filter(attachments__id__isnull=True)
|
queryset = queryset.filter(attachments__id__isnull=True)
|
||||||
# Grab a paginated result set of instances
|
# Grab a paginated result set of instances
|
||||||
paginator = MastodonPaginator(Post, sort_attribute="published")
|
paginator = MastodonPaginator()
|
||||||
pager = paginator.paginate(
|
pager = paginator.paginate(
|
||||||
queryset,
|
queryset,
|
||||||
min_id=min_id,
|
min_id=min_id,
|
||||||
|
@ -101,7 +100,7 @@ def hashtag(
|
||||||
if only_media:
|
if only_media:
|
||||||
queryset = queryset.filter(attachments__id__isnull=True)
|
queryset = queryset.filter(attachments__id__isnull=True)
|
||||||
# Grab a paginated result set of instances
|
# Grab a paginated result set of instances
|
||||||
paginator = MastodonPaginator(Post, sort_attribute="published")
|
paginator = MastodonPaginator()
|
||||||
pager = paginator.paginate(
|
pager = paginator.paginate(
|
||||||
queryset,
|
queryset,
|
||||||
min_id=min_id,
|
min_id=min_id,
|
||||||
|
|
|
@ -38,15 +38,18 @@ def test_webfinger_system_actor(client):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_delete_actor(client, identity):
|
def test_delete_unknown_actor(client, identity):
|
||||||
|
"""
|
||||||
|
Tests that unknown actor delete messages are dropped
|
||||||
|
"""
|
||||||
data = {
|
data = {
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
"actor": "https://mastodon.social/users/fakec8b6984105c8f15070a2",
|
"actor": "https://mastodon.test/users/fakec8b6984105c8f15070a2",
|
||||||
"id": "https://mastodon.social/users/fakec8b6984105c8f15070a2#delete",
|
"id": "https://mastodon.test/users/fakec8b6984105c8f15070a2#delete",
|
||||||
"object": "https://mastodon.social/users/fakec8b6984105c8f15070a2",
|
"object": "https://mastodon.test/users/fakec8b6984105c8f15070a2",
|
||||||
"signature": {
|
"signature": {
|
||||||
"created": "2022-12-06T03:54:28Z",
|
"created": "2022-12-06T03:54:28Z",
|
||||||
"creator": "https://mastodon.social/users/fakec8b6984105c8f15070a2#main-key",
|
"creator": "https://mastodon.test/users/fakec8b6984105c8f15070a2#main-key",
|
||||||
"signatureValue": "This value doesn't matter",
|
"signatureValue": "This value doesn't matter",
|
||||||
"type": "RsaSignature2017",
|
"type": "RsaSignature2017",
|
||||||
},
|
},
|
||||||
|
@ -56,4 +59,5 @@ def test_delete_actor(client, identity):
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
identity.inbox_uri, data=data, content_type="application/activity+json"
|
identity.inbox_uri, data=data, content_type="application/activity+json"
|
||||||
)
|
)
|
||||||
|
print(resp.content)
|
||||||
assert resp.status_code == 202
|
assert resp.status_code == 202
|
||||||
|
|
|
@ -132,7 +132,7 @@ class Inbox(View):
|
||||||
if (
|
if (
|
||||||
document["type"] == "Delete"
|
document["type"] == "Delete"
|
||||||
and document["actor"] == document["object"]
|
and document["actor"] == document["object"]
|
||||||
and not identity.pk
|
and identity._state.adding
|
||||||
):
|
):
|
||||||
# We don't have an Identity record for the user. No-op
|
# We don't have an Identity record for the user. No-op
|
||||||
exceptions.capture_message(
|
exceptions.capture_message(
|
||||||
|
|
Loading…
Reference in a new issue