From b3e67ffe3aa61047be950f0f07cad09fe767269a Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 9 Nov 2023 14:51:09 -0500 Subject: [PATCH] Implement reaction aggregation. --- activities/models/post.py | 14 ++++- tests/activities/models/test_reactions.py | 77 +++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/activities/models/post.py b/activities/models/post.py index 3b08de2..1e7073c 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -610,12 +610,24 @@ class Post(StatorModel): "likes": self.interactions.filter( type=PostInteraction.Types.like, state__in=PostInteractionStates.group_active(), - ).count(), + ) + .values("identity") + .distinct() + .count(), # This counts each user that's had any likes/reactions "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(), + "reactions": { + row["value"]: row["count"] + for row in self.interactions.filter( + type=PostInteraction.Types.like, + state__in=PostInteractionStates.group_active(), + ) + .values("value") + .annotate(count=models.Count("identity")) + }, } if save: self.save() diff --git a/tests/activities/models/test_reactions.py b/tests/activities/models/test_reactions.py index 40313ff..ea073a1 100644 --- a/tests/activities/models/test_reactions.py +++ b/tests/activities/models/test_reactions.py @@ -230,6 +230,83 @@ def test_react_undo_mismatched( assert len(events) == 1 +@pytest.mark.django_db +@pytest.mark.parametrize("local", [True, False]) +@pytest.mark.parametrize("reaction", ["\U0001F607"]) +def test_react_stats( + identity: Identity, + other_identity: Identity, + remote_identity: Identity, + stator, + local: bool, + reaction: str, +): + """ + Checks basic post stats generation + """ + post = Post.create_local(author=identity, content="I love birds!") + if local: + PostService(post).like_as(other_identity, reaction) + else: + message = { + "id": "test", + "type": "Like", + "actor": remote_identity.actor_uri, + "object": post.object_uri, + "content": reaction, + } + InboxMessage.objects.create(message=message) + + # Run stator thrice - to receive the post, make fanouts and then process them + stator.run_single_cycle() + stator.run_single_cycle() + stator.run_single_cycle() + + post.refresh_from_db() + + assert "reactions" in post.stats + assert post.stats["reactions"] == {reaction: 1} + + +@pytest.mark.django_db +@pytest.mark.parametrize("local", [True, False]) +def test_react_stats_multiple( + identity: Identity, + other_identity: Identity, + remote_identity: Identity, + stator, + local: bool, +): + """ + Ensures that multiple reactions get aggregated correctly. + + Basically, if the same person leaves multiple reactions, aggregate all of them into one Like. + """ + post = Post.create_local(author=identity, content="I love birds!") + for i, reaction in enumerate("abc"): + if local: + PostService(post).like_as(other_identity, reaction) + else: + message = { + "id": f"test{i}", + "type": "Like", + "actor": remote_identity.actor_uri, + "object": post.object_uri, + "content": reaction, + } + InboxMessage.objects.create(message=message) + + # Run stator thrice - to receive the post, make fanouts and then process them + stator.run_single_cycle() + stator.run_single_cycle() + stator.run_single_cycle() + + post.refresh_from_db() + + assert post.stats["reactions"] == {"a": 1, "b": 1, "c": 1} + assert post.stats["likes"] == 1 + + # TODO: Test that multiple reactions can be added and deleted correctly # TODO: How should plain likes and reactions from the same source be handled?