Move like/boost/reply counts onto Post model

This commit is contained in:
Andrew Godwin 2022-12-31 13:48:35 -07:00
parent e5ef34a1b9
commit 0fc8ff4965
10 changed files with 98 additions and 72 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 4.1.4 on 2022-12-31 20:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("activities", "0006_fanout_subject_identity_alter_fanout_type"),
]
operations = [
migrations.AddField(
model_name="post",
name="stats",
field=models.JSONField(blank=True, null=True),
),
]

View file

@ -279,6 +279,9 @@ class Post(StatorModel):
blank=True,
)
# Like/Boost/etc counts
stats = models.JSONField(blank=True, null=True)
# When the post was originally created (as opposed to when we received it)
published = models.DateTimeField(default=timezone.now)
@ -404,6 +407,17 @@ class Post(StatorModel):
return ""
return "summary-" + text.slugify(self.summary, allow_unicode=True)
@property
def stats_with_defaults(self):
"""
Returns the stats dict with counts of likes/etc. in it
"""
return {
"likes": self.stats.get("likes", 0) if self.stats else 0,
"boosts": self.stats.get("boosts", 0) if self.stats else 0,
"replies": self.stats.get("replies", 0) if self.stats else 0,
}
### Async helpers ###
async def afetch_full(self) -> "Post":
@ -461,6 +475,9 @@ class Post(StatorModel):
if attachments:
post.attachments.set(attachments)
post.save()
# Recalculate parent stats for replies
if reply_to:
reply_to.calculate_stats()
return post
def edit_local(
@ -516,6 +533,26 @@ class Post(StatorModel):
)
await tag.atransition_perform(HashtagStates.outdated)
def calculate_stats(self, save=True):
"""
Recalculates our stats dict
"""
from activities.models import PostInteraction, PostInteractionStates
self.stats = {
"likes": self.interactions.filter(
type=PostInteraction.Types.like,
state__in=PostInteractionStates.group_active(),
).count(),
"boosts": self.interactions.filter(
type=PostInteraction.Types.boost,
state__in=PostInteractionStates.group_active(),
).count(),
"replies": Post.objects.filter(in_reply_to=self.object_uri).count(),
}
if save:
self.save()
### ActivityPub (outbound) ###
def to_ap(self) -> dict:
@ -674,7 +711,7 @@ class Post(StatorModel):
# Do we have one with the right ID?
created = False
try:
post = cls.objects.select_related("author__domain").get(
post: Post = cls.objects.select_related("author__domain").get(
object_uri=data["id"]
)
except cls.DoesNotExist:
@ -757,10 +794,18 @@ class Post(StatorModel):
focal_x=focal_x,
focal_y=focal_y,
)
# Calculate stats in case we have existing replies
post.calculate_stats(save=False)
post.save()
# Potentially schedule a fetch of the reply parent
# Potentially schedule a fetch of the reply parent, and recalculate
# its stats if it's here already.
if post.in_reply_to:
cls.ensure_object_uri(post.in_reply_to)
try:
parent = cls.by_object_uri(post.in_reply_to)
except cls.DoesNotExist:
cls.ensure_object_uri(post.in_reply_to)
else:
parent.calculate_stats()
return post
@classmethod
@ -945,9 +990,9 @@ class Post(StatorModel):
else []
),
"emojis": [emoji.to_mastodon_json() for emoji in self.emojis.usable()],
"reblogs_count": self.interactions.filter(type="boost").count(),
"favourites_count": self.interactions.filter(type="like").count(),
"replies_count": 0,
"reblogs_count": self.stats_with_defaults["boosts"],
"favourites_count": self.stats_with_defaults["likes"],
"replies_count": self.stats_with_defaults["replies"],
"url": self.absolute_object_uri(),
"in_reply_to_id": reply_parent.pk if reply_parent else None,
"in_reply_to_account_id": reply_parent.author.pk if reply_parent else None,

View file

@ -302,6 +302,8 @@ class PostInteraction(StatorModel):
TimelineEvent.add_post_interaction(interaction.post.author, interaction)
# Force it into fanned_out as it's not ours
interaction.transition_perform(PostInteractionStates.fanned_out)
# Recalculate post stats
interaction.post.calculate_stats()
@classmethod
def handle_undo_ap(cls, data):
@ -322,6 +324,8 @@ class PostInteraction(StatorModel):
interaction.timeline_events.all().delete()
# Force it into undone_fanned_out as it's not ours
interaction.transition_perform(PostInteractionStates.undone_fanned_out)
# Recalculate post stats
interaction.post.calculate_stats()
### Mastodon API ###

View file

@ -1,5 +1,3 @@
from django.db import models
from activities.models import (
Post,
PostInteraction,
@ -31,22 +29,6 @@ class PostService:
"author",
"author__domain",
)
.annotate(
like_count=models.Count(
"interactions",
filter=models.Q(
interactions__type=PostInteraction.Types.like,
interactions__state__in=PostInteractionStates.group_active(),
),
),
boost_count=models.Count(
"interactions",
filter=models.Q(
interactions__type=PostInteraction.Types.boost,
interactions__state__in=PostInteractionStates.group_active(),
),
),
)
)
def __init__(self, post: Post):
@ -63,6 +45,7 @@ class PostService:
)[0]
if interaction.state not in PostInteractionStates.group_active():
interaction.transition_perform(PostInteractionStates.new)
self.post.calculate_stats()
def uninteract_as(self, identity, type):
"""
@ -74,6 +57,7 @@ class PostService:
post=self.post,
):
interaction.transition_perform(PostInteractionStates.undone)
self.post.calculate_stats()
def like_as(self, identity: Identity):
self.interact_as(identity, PostInteraction.Types.like)

View file

@ -1,12 +1,6 @@
from django.db import models
from activities.models import (
Hashtag,
Post,
PostInteraction,
PostInteractionStates,
TimelineEvent,
)
from activities.models import Hashtag, Post, TimelineEvent
from activities.services import PostService
from users.models import Identity
@ -21,38 +15,19 @@ class TimelineService:
@classmethod
def event_queryset(cls):
return (
TimelineEvent.objects.select_related(
"subject_post",
"subject_post__author",
"subject_post__author__domain",
"subject_identity",
"subject_identity__domain",
"subject_post_interaction",
"subject_post_interaction__identity",
"subject_post_interaction__identity__domain",
)
.prefetch_related(
"subject_post__attachments",
"subject_post__mentions",
"subject_post__emojis",
)
.annotate(
like_count=models.Count(
"subject_post__interactions",
filter=models.Q(
subject_post__interactions__type=PostInteraction.Types.like,
subject_post__interactions__state__in=PostInteractionStates.group_active(),
),
),
boost_count=models.Count(
"subject_post__interactions",
filter=models.Q(
subject_post__interactions__type=PostInteraction.Types.boost,
subject_post__interactions__state__in=PostInteractionStates.group_active(),
),
),
)
return TimelineEvent.objects.select_related(
"subject_post",
"subject_post__author",
"subject_post__author__domain",
"subject_identity",
"subject_identity__domain",
"subject_post_interaction",
"subject_post_interaction__identity",
"subject_post_interaction__identity__domain",
).prefetch_related(
"subject_post__attachments",
"subject_post__mentions",
"subject_post__emojis",
)
def home(self) -> models.QuerySet[TimelineEvent]:

View file

@ -93,10 +93,8 @@ class Like(View):
service = PostService(post)
if self.undo:
service.unlike_as(request.identity)
post.like_count = max(0, post.like_count - 1)
else:
service.like_as(request.identity)
post.like_count += 1
# Return either a redirect or a HTMX snippet
if request.htmx:
return render(
@ -127,10 +125,8 @@ class Boost(View):
service = PostService(post)
if self.undo:
service.unboost_as(request.identity)
post.boost_count = max(0, post.boost_count - 1)
else:
service.boost_as(request.identity)
post.boost_count += 1
# Return either a redirect or a HTMX snippet
if request.htmx:
return render(

View file

@ -1,11 +1,11 @@
{% if post.pk in interactions.boost %}
<a title="Unboost" class="active" hx-post="{{ post.urls.action_unboost }}" hx-swap="outerHTML">
<i class="fa-solid fa-retweet"></i>
<span class="like-count">{% if event.boost_count is not None %}{{ event.boost_count }}{% else %}{{ post.boost_count }}{% endif %}</span>
<span class="like-count">{{ post.stats_with_defaults.boosts }}</span>
</a>
{% else %}
<a title="Boost" hx-post="{{ post.urls.action_boost }}" hx-swap="outerHTML">
<i class="fa-solid fa-retweet"></i>
<span class="like-count">{% if event.boost_count is not None %}{{ event.boost_count }}{% else %}{{ post.boost_count }}{% endif %}</span>
<span class="like-count">{{ post.stats_with_defaults.boosts }}</span>
</a>
{% endif %}

View file

@ -1,11 +1,11 @@
{% if post.pk in interactions.like %}
<a title="Unlike" class="active" hx-post="{{ post.urls.action_unlike }}" hx-swap="outerHTML" role="menuitem">
<i class="fa-solid fa-star"></i>
<span class="like-count">{% if event.like_count is not None %}{{ event.like_count }}{% else %}{{ post.like_count }}{% endif %}</span>
<span class="like-count">{{ post.stats_with_defaults.likes }}</span>
</a>
{% else %}
<a title="Like" hx-post="{{ post.urls.action_like }}" hx-swap="outerHTML" role="menuitem">
<i class="fa-solid fa-star"></i>
<span class="like-count">{% if event.like_count is not None %}{{ event.like_count }}{% else %}{{ post.like_count }}{% endif %}</span>
<span class="like-count">{{ post.stats_with_defaults.likes }}</span>
</a>
{% endif %}

View file

@ -1,4 +1,5 @@
<a title="Reply" href="{{ post.urls.action_reply }}" role="menuitem">
<i class="fa-solid fa-reply"></i>
<span class="like-count">{{ post.stats_with_defaults.replies }}</span>
</a>

View file

@ -13,6 +13,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="follow",
name="boosts",
field=models.BooleanField(default=True),
field=models.BooleanField(
default=True,
help_text="Also follow boosts from this user",
),
),
]