Bookmarks (#537)
This commit is contained in:
parent
758e6633c4
commit
cedcc8fa7c
|
@ -329,6 +329,8 @@ class Post(StatorModel):
|
||||||
action_unlike = "{view}unlike/"
|
action_unlike = "{view}unlike/"
|
||||||
action_boost = "{view}boost/"
|
action_boost = "{view}boost/"
|
||||||
action_unboost = "{view}unboost/"
|
action_unboost = "{view}unboost/"
|
||||||
|
action_bookmark = "{view}bookmark/"
|
||||||
|
action_unbookmark = "{view}unbookmark/"
|
||||||
action_delete = "{view}delete/"
|
action_delete = "{view}delete/"
|
||||||
action_edit = "{view}edit/"
|
action_edit = "{view}edit/"
|
||||||
action_report = "{view}report/"
|
action_report = "{view}report/"
|
||||||
|
@ -1059,7 +1061,7 @@ class Post(StatorModel):
|
||||||
|
|
||||||
### Mastodon API ###
|
### Mastodon API ###
|
||||||
|
|
||||||
def to_mastodon_json(self, interactions=None, identity=None):
|
def to_mastodon_json(self, interactions=None, bookmarks=None, identity=None):
|
||||||
reply_parent = None
|
reply_parent = None
|
||||||
if self.in_reply_to:
|
if self.in_reply_to:
|
||||||
# Load the PK and author.id explicitly to prevent a SELECT on the entire author Identity
|
# Load the PK and author.id explicitly to prevent a SELECT on the entire author Identity
|
||||||
|
@ -1128,4 +1130,6 @@ class Post(StatorModel):
|
||||||
if interactions:
|
if interactions:
|
||||||
value["favourited"] = self.pk in interactions.get("like", [])
|
value["favourited"] = self.pk in interactions.get("like", [])
|
||||||
value["reblogged"] = self.pk in interactions.get("boost", [])
|
value["reblogged"] = self.pk in interactions.get("boost", [])
|
||||||
|
if bookmarks:
|
||||||
|
value["bookmarked"] = self.pk in bookmarks
|
||||||
return value
|
return value
|
||||||
|
|
|
@ -221,10 +221,10 @@ class TimelineEvent(models.Model):
|
||||||
raise ValueError(f"Cannot convert {self.type} to notification JSON")
|
raise ValueError(f"Cannot convert {self.type} to notification JSON")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def to_mastodon_status_json(self, interactions=None, identity=None):
|
def to_mastodon_status_json(self, interactions=None, bookmarks=None, identity=None):
|
||||||
if self.type == self.Types.post:
|
if self.type == self.Types.post:
|
||||||
return self.subject_post.to_mastodon_json(
|
return self.subject_post.to_mastodon_json(
|
||||||
interactions=interactions, identity=identity
|
interactions=interactions, bookmarks=bookmarks, identity=identity
|
||||||
)
|
)
|
||||||
elif self.type == self.Types.boost:
|
elif self.type == self.Types.boost:
|
||||||
return self.subject_post_interaction.to_mastodon_status_json(
|
return self.subject_post_interaction.to_mastodon_status_json(
|
||||||
|
|
|
@ -102,3 +102,13 @@ class TimelineService:
|
||||||
)
|
)
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def bookmarks(self) -> models.QuerySet[Post]:
|
||||||
|
"""
|
||||||
|
Return all bookmarked posts for an identity
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
PostService.queryset()
|
||||||
|
.filter(bookmarks__identity=self.identity)
|
||||||
|
.order_by("-id")
|
||||||
|
)
|
||||||
|
|
|
@ -146,6 +146,39 @@ class Boost(View):
|
||||||
return redirect(post.urls.view)
|
return redirect(post.urls.view)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(identity_required, name="dispatch")
|
||||||
|
class Bookmark(View):
|
||||||
|
"""
|
||||||
|
Adds/removes a bookmark from the current identity to the post
|
||||||
|
"""
|
||||||
|
|
||||||
|
undo = False
|
||||||
|
|
||||||
|
def post(self, request, handle, post_id):
|
||||||
|
identity = by_handle_or_404(self.request, handle, local=False)
|
||||||
|
post = get_object_or_404(
|
||||||
|
PostService.queryset()
|
||||||
|
.filter(author=identity)
|
||||||
|
.visible_to(request.identity, include_replies=True),
|
||||||
|
pk=post_id,
|
||||||
|
)
|
||||||
|
if self.undo:
|
||||||
|
request.identity.bookmarks.filter(post=post).delete()
|
||||||
|
else:
|
||||||
|
request.identity.bookmarks.get_or_create(post=post)
|
||||||
|
# Return either a redirect or a HTMX snippet
|
||||||
|
if request.htmx:
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"activities/_bookmark.html",
|
||||||
|
{
|
||||||
|
"post": post,
|
||||||
|
"bookmarks": set() if self.undo else {post.pk},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return redirect(post.urls.view)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
@method_decorator(identity_required, name="dispatch")
|
||||||
class Delete(TemplateView):
|
class Delete(TemplateView):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -7,6 +7,7 @@ from activities.models import Hashtag, PostInteraction, TimelineEvent
|
||||||
from activities.services import TimelineService
|
from activities.services import TimelineService
|
||||||
from core.decorators import cache_page
|
from core.decorators import cache_page
|
||||||
from users.decorators import identity_required
|
from users.decorators import identity_required
|
||||||
|
from users.models import Bookmark
|
||||||
|
|
||||||
from .compose import Compose
|
from .compose import Compose
|
||||||
|
|
||||||
|
@ -31,6 +32,9 @@ class Home(TemplateView):
|
||||||
event_page,
|
event_page,
|
||||||
self.request.identity,
|
self.request.identity,
|
||||||
),
|
),
|
||||||
|
"bookmarks": Bookmark.for_identity(
|
||||||
|
self.request.identity, event_page, "subject_post_id"
|
||||||
|
),
|
||||||
"current_page": "home",
|
"current_page": "home",
|
||||||
"allows_refresh": True,
|
"allows_refresh": True,
|
||||||
"page_obj": event_page,
|
"page_obj": event_page,
|
||||||
|
@ -68,6 +72,9 @@ class Tag(ListView):
|
||||||
context["interactions"] = PostInteraction.get_post_interactions(
|
context["interactions"] = PostInteraction.get_post_interactions(
|
||||||
context["page_obj"], self.request.identity
|
context["page_obj"], self.request.identity
|
||||||
)
|
)
|
||||||
|
context["bookmarks"] = Bookmark.for_identity(
|
||||||
|
self.request.identity, context["page_obj"]
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,6 +98,9 @@ class Local(ListView):
|
||||||
context["interactions"] = PostInteraction.get_post_interactions(
|
context["interactions"] = PostInteraction.get_post_interactions(
|
||||||
context["page_obj"], self.request.identity
|
context["page_obj"], self.request.identity
|
||||||
)
|
)
|
||||||
|
context["bookmarks"] = Bookmark.for_identity(
|
||||||
|
self.request.identity, context["page_obj"]
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,6 +122,9 @@ class Federated(ListView):
|
||||||
context["interactions"] = PostInteraction.get_post_interactions(
|
context["interactions"] = PostInteraction.get_post_interactions(
|
||||||
context["page_obj"], self.request.identity
|
context["page_obj"], self.request.identity
|
||||||
)
|
)
|
||||||
|
context["bookmarks"] = Bookmark.for_identity(
|
||||||
|
self.request.identity, context["page_obj"]
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -173,4 +186,7 @@ class Notifications(ListView):
|
||||||
context["page_obj"],
|
context["page_obj"],
|
||||||
self.request.identity,
|
self.request.identity,
|
||||||
)
|
)
|
||||||
|
context["bookmarks"] = Bookmark.for_identity(
|
||||||
|
self.request.identity, context["page_obj"], "subject_post_id"
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
|
@ -165,10 +165,13 @@ class Status(Schema):
|
||||||
cls,
|
cls,
|
||||||
post: activities_models.Post,
|
post: activities_models.Post,
|
||||||
interactions: dict[str, set[str]] | None = None,
|
interactions: dict[str, set[str]] | None = None,
|
||||||
|
bookmarks: set[str] | None = None,
|
||||||
identity: users_models.Identity | None = None,
|
identity: users_models.Identity | None = None,
|
||||||
) -> "Status":
|
) -> "Status":
|
||||||
return cls(
|
return cls(
|
||||||
**post.to_mastodon_json(interactions=interactions, identity=identity)
|
**post.to_mastodon_json(
|
||||||
|
interactions=interactions, bookmarks=bookmarks, identity=identity
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -180,8 +183,11 @@ class Status(Schema):
|
||||||
interactions = activities_models.PostInteraction.get_post_interactions(
|
interactions = activities_models.PostInteraction.get_post_interactions(
|
||||||
posts, identity
|
posts, identity
|
||||||
)
|
)
|
||||||
|
bookmarks = users_models.Bookmark.for_identity(identity, posts)
|
||||||
return [
|
return [
|
||||||
cls.from_post(post, interactions=interactions, identity=identity)
|
cls.from_post(
|
||||||
|
post, interactions=interactions, bookmarks=bookmarks, identity=identity
|
||||||
|
)
|
||||||
for post in posts
|
for post in posts
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -190,11 +196,12 @@ class Status(Schema):
|
||||||
cls,
|
cls,
|
||||||
timeline_event: activities_models.TimelineEvent,
|
timeline_event: activities_models.TimelineEvent,
|
||||||
interactions: dict[str, set[str]] | None = None,
|
interactions: dict[str, set[str]] | None = None,
|
||||||
|
bookmarks: set[str] | None = None,
|
||||||
identity: users_models.Identity | None = None,
|
identity: users_models.Identity | None = None,
|
||||||
) -> "Status":
|
) -> "Status":
|
||||||
return cls(
|
return cls(
|
||||||
**timeline_event.to_mastodon_status_json(
|
**timeline_event.to_mastodon_status_json(
|
||||||
interactions=interactions, identity=identity
|
interactions=interactions, bookmarks=bookmarks, identity=identity
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -207,8 +214,13 @@ class Status(Schema):
|
||||||
interactions = activities_models.PostInteraction.get_event_interactions(
|
interactions = activities_models.PostInteraction.get_event_interactions(
|
||||||
events, identity
|
events, identity
|
||||||
)
|
)
|
||||||
|
bookmarks = users_models.Bookmark.for_identity(
|
||||||
|
identity, events, "subject_post_id"
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
cls.from_timeline_event(event, interactions=interactions, identity=identity)
|
cls.from_timeline_event(
|
||||||
|
event, interactions=interactions, bookmarks=bookmarks, identity=identity
|
||||||
|
)
|
||||||
for event in events
|
for event in events
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,8 @@ urlpatterns = [
|
||||||
path("v1/statuses/<id>/favourited_by", statuses.favourited_by),
|
path("v1/statuses/<id>/favourited_by", statuses.favourited_by),
|
||||||
path("v1/statuses/<id>/reblog", statuses.reblog_status),
|
path("v1/statuses/<id>/reblog", statuses.reblog_status),
|
||||||
path("v1/statuses/<id>/unreblog", statuses.unreblog_status),
|
path("v1/statuses/<id>/unreblog", statuses.unreblog_status),
|
||||||
|
path("v1/statuses/<id>/bookmark", statuses.bookmark_status),
|
||||||
|
path("v1/statuses/<id>/unbookmark", statuses.unbookmark_status),
|
||||||
# Tags
|
# Tags
|
||||||
path("v1/followed_tags", tags.followed_tags),
|
path("v1/followed_tags", tags.followed_tags),
|
||||||
# Timelines
|
# Timelines
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from hatchway import api_view
|
from hatchway import api_view
|
||||||
|
|
||||||
|
from activities.models import Post
|
||||||
|
from activities.services import TimelineService
|
||||||
from api import schemas
|
from api import schemas
|
||||||
from api.decorators import scope_required
|
from api.decorators import scope_required
|
||||||
|
from api.pagination import MastodonPaginator, PaginatingApiResponse, PaginationResult
|
||||||
|
|
||||||
|
|
||||||
@scope_required("read:bookmarks")
|
@scope_required("read:bookmarks")
|
||||||
|
@ -14,5 +17,17 @@ def bookmarks(
|
||||||
min_id: str | None = None,
|
min_id: str | None = None,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
) -> list[schemas.Status]:
|
) -> list[schemas.Status]:
|
||||||
# We don't implement this yet
|
queryset = TimelineService(request.identity).bookmarks()
|
||||||
return []
|
paginator = MastodonPaginator()
|
||||||
|
pager: PaginationResult[Post] = paginator.paginate(
|
||||||
|
queryset,
|
||||||
|
min_id=min_id,
|
||||||
|
max_id=max_id,
|
||||||
|
since_id=since_id,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
return PaginatingApiResponse(
|
||||||
|
schemas.Status.map_from_post(pager.results, request.identity),
|
||||||
|
request=request,
|
||||||
|
include_params=["limit"],
|
||||||
|
)
|
||||||
|
|
|
@ -267,3 +267,25 @@ def unreblog_status(request, id: str) -> schemas.Status:
|
||||||
return schemas.Status.from_post(
|
return schemas.Status.from_post(
|
||||||
post, interactions=interactions, identity=request.identity
|
post, interactions=interactions, identity=request.identity
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@scope_required("write:bookmarks")
|
||||||
|
@api_view.post
|
||||||
|
def bookmark_status(request, id: str) -> schemas.Status:
|
||||||
|
post = post_for_id(request, id)
|
||||||
|
request.identity.bookmarks.get_or_create(post=post)
|
||||||
|
interactions = PostInteraction.get_post_interactions([post], request.identity)
|
||||||
|
return schemas.Status.from_post(
|
||||||
|
post, interactions=interactions, bookmarks={post.pk}, identity=request.identity
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@scope_required("write:bookmarks")
|
||||||
|
@api_view.post
|
||||||
|
def unbookmark_status(request, id: str) -> schemas.Status:
|
||||||
|
post = post_for_id(request, id)
|
||||||
|
request.identity.bookmarks.filter(post=post).delete()
|
||||||
|
interactions = PostInteraction.get_post_interactions([post], request.identity)
|
||||||
|
return schemas.Status.from_post(
|
||||||
|
post, interactions=interactions, identity=request.identity
|
||||||
|
)
|
||||||
|
|
|
@ -242,6 +242,10 @@ urlpatterns = [
|
||||||
path("@<handle>/posts/<int:post_id>/unlike/", posts.Like.as_view(undo=True)),
|
path("@<handle>/posts/<int:post_id>/unlike/", posts.Like.as_view(undo=True)),
|
||||||
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
|
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
|
||||||
|
path("@<handle>/posts/<int:post_id>/bookmark/", posts.Bookmark.as_view()),
|
||||||
|
path(
|
||||||
|
"@<handle>/posts/<int:post_id>/unbookmark/", posts.Bookmark.as_view(undo=True)
|
||||||
|
),
|
||||||
path("@<handle>/posts/<int:post_id>/delete/", posts.Delete.as_view()),
|
path("@<handle>/posts/<int:post_id>/delete/", posts.Delete.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
|
path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
|
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% if post.pk in bookmarks %}
|
||||||
|
<a title="Unbookmark" class="active" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_unbookmark }}" hx-swap="outerHTML" tabindex="0">
|
||||||
|
<i class="fa-solid fa-bookmark"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a title="Bookmark" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_bookmark }}" hx-swap="outerHTML" tabindex="0">
|
||||||
|
<i class="fa-regular fa-bookmark"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
|
@ -82,6 +82,7 @@
|
||||||
{% include "activities/_reply.html" %}
|
{% include "activities/_reply.html" %}
|
||||||
{% include "activities/_like.html" %}
|
{% include "activities/_like.html" %}
|
||||||
{% include "activities/_boost.html" %}
|
{% include "activities/_boost.html" %}
|
||||||
|
{% include "activities/_bookmark.html" %}
|
||||||
<a title="Menu" class="menu" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" role="menuitem" aria-haspopup="menu" tabindex="0">
|
<a title="Menu" class="menu" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" role="menuitem" aria-haspopup="menu" tabindex="0">
|
||||||
<i class="fa-solid fa-bars"></i>
|
<i class="fa-solid fa-bars"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -8,6 +8,7 @@ from activities.admin import IdentityLocalFilter
|
||||||
from users.models import (
|
from users.models import (
|
||||||
Announcement,
|
Announcement,
|
||||||
Block,
|
Block,
|
||||||
|
Bookmark,
|
||||||
Domain,
|
Domain,
|
||||||
Follow,
|
Follow,
|
||||||
Identity,
|
Identity,
|
||||||
|
@ -221,3 +222,9 @@ class ReportAdmin(admin.ModelAdmin):
|
||||||
class AnnouncementAdmin(admin.ModelAdmin):
|
class AnnouncementAdmin(admin.ModelAdmin):
|
||||||
list_display = ["id", "published", "start", "end", "text"]
|
list_display = ["id", "published", "start", "end", "text"]
|
||||||
autocomplete_fields = ["seen"]
|
autocomplete_fields = ["seen"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Bookmark)
|
||||||
|
class BookmarkAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["id", "identity", "post", "created"]
|
||||||
|
raw_id_fields = ["identity", "post"]
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-03-11 00:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("activities", "0012_in_reply_to_index"),
|
||||||
|
("users", "0014_domain_notes"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Bookmark",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"identity",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="bookmarks",
|
||||||
|
to="users.identity",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"post",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="bookmarks",
|
||||||
|
to="activities.post",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"unique_together": {("identity", "post")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,5 +1,6 @@
|
||||||
from .announcement import Announcement # noqa
|
from .announcement import Announcement # noqa
|
||||||
from .block import Block, BlockStates # noqa
|
from .block import Block, BlockStates # noqa
|
||||||
|
from .bookmark import Bookmark # noqa
|
||||||
from .domain import Domain # noqa
|
from .domain import Domain # noqa
|
||||||
from .follow import Follow, FollowStates # noqa
|
from .follow import Follow, FollowStates # noqa
|
||||||
from .identity import Identity, IdentityStates # noqa
|
from .identity import Identity, IdentityStates # noqa
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Bookmark(models.Model):
|
||||||
|
"""
|
||||||
|
A (private) bookmark of a Post by an Identity
|
||||||
|
"""
|
||||||
|
|
||||||
|
identity = models.ForeignKey(
|
||||||
|
"users.Identity",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="bookmarks",
|
||||||
|
)
|
||||||
|
post = models.ForeignKey(
|
||||||
|
"activities.Post",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="bookmarks",
|
||||||
|
)
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [("identity", "post")]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"#{self.id}: {self.identity} → {self.post}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_identity(cls, identity, posts=None, field="id"):
|
||||||
|
"""
|
||||||
|
Returns a set of bookmarked Post IDs for the given identity. If `posts` is
|
||||||
|
specified, it is used to filter bookmarks matching those in the list.
|
||||||
|
"""
|
||||||
|
if identity is None:
|
||||||
|
return set()
|
||||||
|
queryset = cls.objects.filter(identity=identity)
|
||||||
|
if posts:
|
||||||
|
queryset = queryset.filter(post_id__in=[getattr(p, field) for p in posts])
|
||||||
|
return set(queryset.values_list("post_id", flat=True))
|
Loading…
Reference in New Issue