mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-22 07:10:59 +00:00
Move like/boost/reply counts onto Post model
This commit is contained in:
parent
e5ef34a1b9
commit
0fc8ff4965
10 changed files with 98 additions and 72 deletions
18
activities/migrations/0007_post_stats.py
Normal file
18
activities/migrations/0007_post_stats.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -279,6 +279,9 @@ class Post(StatorModel):
|
||||||
blank=True,
|
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)
|
# When the post was originally created (as opposed to when we received it)
|
||||||
published = models.DateTimeField(default=timezone.now)
|
published = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
@ -404,6 +407,17 @@ class Post(StatorModel):
|
||||||
return ""
|
return ""
|
||||||
return "summary-" + text.slugify(self.summary, allow_unicode=True)
|
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 helpers ###
|
||||||
|
|
||||||
async def afetch_full(self) -> "Post":
|
async def afetch_full(self) -> "Post":
|
||||||
|
@ -461,6 +475,9 @@ class Post(StatorModel):
|
||||||
if attachments:
|
if attachments:
|
||||||
post.attachments.set(attachments)
|
post.attachments.set(attachments)
|
||||||
post.save()
|
post.save()
|
||||||
|
# Recalculate parent stats for replies
|
||||||
|
if reply_to:
|
||||||
|
reply_to.calculate_stats()
|
||||||
return post
|
return post
|
||||||
|
|
||||||
def edit_local(
|
def edit_local(
|
||||||
|
@ -516,6 +533,26 @@ class Post(StatorModel):
|
||||||
)
|
)
|
||||||
await tag.atransition_perform(HashtagStates.outdated)
|
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) ###
|
### ActivityPub (outbound) ###
|
||||||
|
|
||||||
def to_ap(self) -> dict:
|
def to_ap(self) -> dict:
|
||||||
|
@ -674,7 +711,7 @@ class Post(StatorModel):
|
||||||
# Do we have one with the right ID?
|
# Do we have one with the right ID?
|
||||||
created = False
|
created = False
|
||||||
try:
|
try:
|
||||||
post = cls.objects.select_related("author__domain").get(
|
post: Post = cls.objects.select_related("author__domain").get(
|
||||||
object_uri=data["id"]
|
object_uri=data["id"]
|
||||||
)
|
)
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
|
@ -757,10 +794,18 @@ class Post(StatorModel):
|
||||||
focal_x=focal_x,
|
focal_x=focal_x,
|
||||||
focal_y=focal_y,
|
focal_y=focal_y,
|
||||||
)
|
)
|
||||||
|
# Calculate stats in case we have existing replies
|
||||||
|
post.calculate_stats(save=False)
|
||||||
post.save()
|
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:
|
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
|
return post
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -945,9 +990,9 @@ class Post(StatorModel):
|
||||||
else []
|
else []
|
||||||
),
|
),
|
||||||
"emojis": [emoji.to_mastodon_json() for emoji in self.emojis.usable()],
|
"emojis": [emoji.to_mastodon_json() for emoji in self.emojis.usable()],
|
||||||
"reblogs_count": self.interactions.filter(type="boost").count(),
|
"reblogs_count": self.stats_with_defaults["boosts"],
|
||||||
"favourites_count": self.interactions.filter(type="like").count(),
|
"favourites_count": self.stats_with_defaults["likes"],
|
||||||
"replies_count": 0,
|
"replies_count": self.stats_with_defaults["replies"],
|
||||||
"url": self.absolute_object_uri(),
|
"url": self.absolute_object_uri(),
|
||||||
"in_reply_to_id": reply_parent.pk if reply_parent else None,
|
"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,
|
"in_reply_to_account_id": reply_parent.author.pk if reply_parent else None,
|
||||||
|
|
|
@ -302,6 +302,8 @@ class PostInteraction(StatorModel):
|
||||||
TimelineEvent.add_post_interaction(interaction.post.author, interaction)
|
TimelineEvent.add_post_interaction(interaction.post.author, interaction)
|
||||||
# Force it into fanned_out as it's not ours
|
# Force it into fanned_out as it's not ours
|
||||||
interaction.transition_perform(PostInteractionStates.fanned_out)
|
interaction.transition_perform(PostInteractionStates.fanned_out)
|
||||||
|
# Recalculate post stats
|
||||||
|
interaction.post.calculate_stats()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def handle_undo_ap(cls, data):
|
def handle_undo_ap(cls, data):
|
||||||
|
@ -322,6 +324,8 @@ class PostInteraction(StatorModel):
|
||||||
interaction.timeline_events.all().delete()
|
interaction.timeline_events.all().delete()
|
||||||
# Force it into undone_fanned_out as it's not ours
|
# Force it into undone_fanned_out as it's not ours
|
||||||
interaction.transition_perform(PostInteractionStates.undone_fanned_out)
|
interaction.transition_perform(PostInteractionStates.undone_fanned_out)
|
||||||
|
# Recalculate post stats
|
||||||
|
interaction.post.calculate_stats()
|
||||||
|
|
||||||
### Mastodon API ###
|
### Mastodon API ###
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from activities.models import (
|
from activities.models import (
|
||||||
Post,
|
Post,
|
||||||
PostInteraction,
|
PostInteraction,
|
||||||
|
@ -31,22 +29,6 @@ class PostService:
|
||||||
"author",
|
"author",
|
||||||
"author__domain",
|
"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):
|
def __init__(self, post: Post):
|
||||||
|
@ -63,6 +45,7 @@ class PostService:
|
||||||
)[0]
|
)[0]
|
||||||
if interaction.state not in PostInteractionStates.group_active():
|
if interaction.state not in PostInteractionStates.group_active():
|
||||||
interaction.transition_perform(PostInteractionStates.new)
|
interaction.transition_perform(PostInteractionStates.new)
|
||||||
|
self.post.calculate_stats()
|
||||||
|
|
||||||
def uninteract_as(self, identity, type):
|
def uninteract_as(self, identity, type):
|
||||||
"""
|
"""
|
||||||
|
@ -74,6 +57,7 @@ class PostService:
|
||||||
post=self.post,
|
post=self.post,
|
||||||
):
|
):
|
||||||
interaction.transition_perform(PostInteractionStates.undone)
|
interaction.transition_perform(PostInteractionStates.undone)
|
||||||
|
self.post.calculate_stats()
|
||||||
|
|
||||||
def like_as(self, identity: Identity):
|
def like_as(self, identity: Identity):
|
||||||
self.interact_as(identity, PostInteraction.Types.like)
|
self.interact_as(identity, PostInteraction.Types.like)
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from activities.models import (
|
from activities.models import Hashtag, Post, TimelineEvent
|
||||||
Hashtag,
|
|
||||||
Post,
|
|
||||||
PostInteraction,
|
|
||||||
PostInteractionStates,
|
|
||||||
TimelineEvent,
|
|
||||||
)
|
|
||||||
from activities.services import PostService
|
from activities.services import PostService
|
||||||
from users.models import Identity
|
from users.models import Identity
|
||||||
|
|
||||||
|
@ -21,38 +15,19 @@ class TimelineService:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def event_queryset(cls):
|
def event_queryset(cls):
|
||||||
return (
|
return TimelineEvent.objects.select_related(
|
||||||
TimelineEvent.objects.select_related(
|
"subject_post",
|
||||||
"subject_post",
|
"subject_post__author",
|
||||||
"subject_post__author",
|
"subject_post__author__domain",
|
||||||
"subject_post__author__domain",
|
"subject_identity",
|
||||||
"subject_identity",
|
"subject_identity__domain",
|
||||||
"subject_identity__domain",
|
"subject_post_interaction",
|
||||||
"subject_post_interaction",
|
"subject_post_interaction__identity",
|
||||||
"subject_post_interaction__identity",
|
"subject_post_interaction__identity__domain",
|
||||||
"subject_post_interaction__identity__domain",
|
).prefetch_related(
|
||||||
)
|
"subject_post__attachments",
|
||||||
.prefetch_related(
|
"subject_post__mentions",
|
||||||
"subject_post__attachments",
|
"subject_post__emojis",
|
||||||
"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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def home(self) -> models.QuerySet[TimelineEvent]:
|
def home(self) -> models.QuerySet[TimelineEvent]:
|
||||||
|
|
|
@ -93,10 +93,8 @@ class Like(View):
|
||||||
service = PostService(post)
|
service = PostService(post)
|
||||||
if self.undo:
|
if self.undo:
|
||||||
service.unlike_as(request.identity)
|
service.unlike_as(request.identity)
|
||||||
post.like_count = max(0, post.like_count - 1)
|
|
||||||
else:
|
else:
|
||||||
service.like_as(request.identity)
|
service.like_as(request.identity)
|
||||||
post.like_count += 1
|
|
||||||
# Return either a redirect or a HTMX snippet
|
# Return either a redirect or a HTMX snippet
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
return render(
|
return render(
|
||||||
|
@ -127,10 +125,8 @@ class Boost(View):
|
||||||
service = PostService(post)
|
service = PostService(post)
|
||||||
if self.undo:
|
if self.undo:
|
||||||
service.unboost_as(request.identity)
|
service.unboost_as(request.identity)
|
||||||
post.boost_count = max(0, post.boost_count - 1)
|
|
||||||
else:
|
else:
|
||||||
service.boost_as(request.identity)
|
service.boost_as(request.identity)
|
||||||
post.boost_count += 1
|
|
||||||
# Return either a redirect or a HTMX snippet
|
# Return either a redirect or a HTMX snippet
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
return render(
|
return render(
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{% if post.pk in interactions.boost %}
|
{% if post.pk in interactions.boost %}
|
||||||
<a title="Unboost" class="active" hx-post="{{ post.urls.action_unboost }}" hx-swap="outerHTML">
|
<a title="Unboost" class="active" hx-post="{{ post.urls.action_unboost }}" hx-swap="outerHTML">
|
||||||
<i class="fa-solid fa-retweet"></i>
|
<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>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a title="Boost" hx-post="{{ post.urls.action_boost }}" hx-swap="outerHTML">
|
<a title="Boost" hx-post="{{ post.urls.action_boost }}" hx-swap="outerHTML">
|
||||||
<i class="fa-solid fa-retweet"></i>
|
<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>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{% if post.pk in interactions.like %}
|
{% if post.pk in interactions.like %}
|
||||||
<a title="Unlike" class="active" hx-post="{{ post.urls.action_unlike }}" hx-swap="outerHTML" role="menuitem">
|
<a title="Unlike" class="active" hx-post="{{ post.urls.action_unlike }}" hx-swap="outerHTML" role="menuitem">
|
||||||
<i class="fa-solid fa-star"></i>
|
<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>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a title="Like" hx-post="{{ post.urls.action_like }}" hx-swap="outerHTML" role="menuitem">
|
<a title="Like" hx-post="{{ post.urls.action_like }}" hx-swap="outerHTML" role="menuitem">
|
||||||
<i class="fa-solid fa-star"></i>
|
<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>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
<a title="Reply" href="{{ post.urls.action_reply }}" role="menuitem">
|
<a title="Reply" href="{{ post.urls.action_reply }}" role="menuitem">
|
||||||
<i class="fa-solid fa-reply"></i>
|
<i class="fa-solid fa-reply"></i>
|
||||||
|
<span class="like-count">{{ post.stats_with_defaults.replies }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -13,6 +13,9 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="follow",
|
model_name="follow",
|
||||||
name="boosts",
|
name="boosts",
|
||||||
field=models.BooleanField(default=True),
|
field=models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Also follow boosts from this user",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue