Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 0 additions & 55 deletions src/feed/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,61 +966,6 @@ def _get_grant_image(grant):
return None


class ActivityFeedEntrySerializer(FeedEntrySerializer):
"""
Serializer for activity feed entries that includes fundraise contributions.
"""

contributions = serializers.SerializerMethodField()

class Meta:
model = FeedEntry
fields = FeedEntrySerializer.Meta.fields + [
"contributions",
]

def get_contributions(self, obj):
"""
Return fundraise contributors for entries whose unified document has a
fundraise.
"""
if not obj.unified_document:
return None

fundraises = getattr(obj.unified_document, "prefetched_fundraises", None)
if fundraises is None:
fundraises = list(obj.unified_document.fundraises.all())

if not fundraises:
return None

fundraise = fundraises[0]
aggregated = fundraise.get_contributors_summary()

result = []
for entry in aggregated.top:
serializer = SimpleUserSerializer(entry.user)
user_result = serializer.data
user_result["total_contribution"] = {
"rsc": entry.total_rsc,
"usd": entry.total_usd,
}
user_result["contributions"] = [
{
"amount": contribution.amount,
"currency": contribution.currency,
"date": contribution.date,
}
for contribution in entry.contributions
]
result.append(user_result)

return {
"total": aggregated.total,
"top": result,
}


class GrantFeedEntrySerializer(FeedEntrySerializer):
"""Serializer for grant feed entries"""

Expand Down
200 changes: 1 addition & 199 deletions src/feed/tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from feed.models import FeedEntry
from feed.serializers import (
ActivityFeedEntrySerializer,

CommentSerializer,
ContentObjectSerializer,
FeedEntrySerializer,
Expand Down Expand Up @@ -2447,204 +2447,6 @@ def test_fundraise_without_application_has_no_associated_grants(
self.assertEqual(serializer.data["associated_grants"], [])


class ActivityFeedEntrySerializerTests(AWSMockTestCase):
"""
Test cases for the ActivityFeedEntrySerializer.
"""

def setUp(self):
super().setUp()
self.user = create_random_default_user("activity_feed_user")

def _create_feed_entry(self, unified_doc, post):
return FeedEntry.objects.create(
content_type=ContentType.objects.get_for_model(ResearchhubPost),
object_id=post.id,
user=self.user,
action="PUBLISH",
action_date=post.created_date,
unified_document=unified_doc,
)

def _create_fundraise_post(self):
unified_doc = ResearchhubUnifiedDocument.objects.create(
document_type=document_type.PREREGISTRATION,
)
post = ResearchhubPost.objects.create(
title="Prereg Post",
created_by=self.user,
document_type=document_type.PREREGISTRATION,
renderable_text="A preregistration post",
unified_document=unified_doc,
)
return unified_doc, post

def _create_purchase(self, fundraise, user, amount):
ct = ContentType.objects.get_for_model(Fundraise)
return Purchase.objects.create(
user=user,
content_type=ct,
object_id=fundraise.id,
purchase_type=Purchase.FUNDRAISE_CONTRIBUTION,
purchase_method=Purchase.OFF_CHAIN,
amount=str(amount),
)

def _create_usd_contribution(self, fundraise, user, amount_cents):
return UsdFundraiseContribution.objects.create(
user=user,
fundraise=fundraise,
amount_cents=amount_cents,
fee_cents=int(amount_cents * 0.09),
origin_fund_id="test-origin",
destination_org_id="test-destination",
)

def test_contributions_none_without_fundraise(self):
"""Feed entry on a regular post should have contributions=None."""
# Arrange
unified_doc = ResearchhubUnifiedDocument.objects.create(
document_type=document_type.DISCUSSION,
)
post = ResearchhubPost.objects.create(
title="Discussion Post",
created_by=self.user,
document_type=document_type.DISCUSSION,
renderable_text="Just a discussion",
unified_document=unified_doc,
)
feed_entry = self._create_feed_entry(unified_doc, post)

# Act
serializer = ActivityFeedEntrySerializer(feed_entry)

# Assert
self.assertIsNone(serializer.data["contributions"])

@patch(
"purchase.related_models.rsc_exchange_rate_model.RscExchangeRate.get_latest_exchange_rate"
)
def test_contributions_empty_with_fundraise_no_purchases(self, mock_usd_to_rsc):
"""Fundraise with no purchases should return total=0, top=[]."""
# Arrange
mock_usd_to_rsc.return_value = 1.0

unified_doc, post = self._create_fundraise_post()
Fundraise.objects.create(
unified_document=unified_doc,
created_by=self.user,
goal_amount=Decimal("100.00"),
goal_currency=USD,
status=Fundraise.OPEN,
)
feed_entry = self._create_feed_entry(unified_doc, post)

# Act
serializer = ActivityFeedEntrySerializer(feed_entry)
contributions = serializer.data["contributions"]

# Assert
self.assertEqual(contributions["total"], 0)
self.assertEqual(contributions["top"], [])

@patch(
"purchase.related_models.rsc_exchange_rate_model.RscExchangeRate.get_latest_exchange_rate"
)
def test_contributions_single_contributor(self, mock_usd_to_rsc):
"""Single purchase should produce one contributor entry."""
# Arrange
mock_usd_to_rsc.return_value = 1.0

unified_doc, post = self._create_fundraise_post()
fundraise = Fundraise.objects.create(
unified_document=unified_doc,
created_by=self.user,
goal_amount=Decimal("500.00"),
goal_currency=USD,
status=Fundraise.OPEN,
)
self._create_purchase(fundraise, self.user, 50.0)
feed_entry = self._create_feed_entry(unified_doc, post)

# Act
serializer = ActivityFeedEntrySerializer(feed_entry)
contributions = serializer.data["contributions"]

# Assert
self.assertEqual(contributions["total"], 1)
self.assertEqual(len(contributions["top"]), 1)
top_entry = contributions["top"][0]
self.assertEqual(top_entry["total_contribution"]["rsc"], 50.0)
self.assertEqual(top_entry["total_contribution"]["usd"], 0)
self.assertEqual(len(top_entry["contributions"]), 1)
self.assertEqual(top_entry["contributions"][0]["amount"], 50.0)
self.assertEqual(top_entry["contributions"][0]["currency"], "RSC")

@patch(
"purchase.related_models.rsc_exchange_rate_model.RscExchangeRate.get_latest_exchange_rate"
)
def test_contributions_multiple_purchases_aggregated(self, mock_usd_to_rsc):
"""Multiple purchases by the same user should be aggregated."""
# Arrange
mock_usd_to_rsc.return_value = 1.0

unified_doc, post = self._create_fundraise_post()
fundraise = Fundraise.objects.create(
unified_document=unified_doc,
created_by=self.user,
goal_amount=Decimal("1000.00"),
goal_currency=USD,
status=Fundraise.OPEN,
)
self._create_purchase(fundraise, self.user, 30.0)
self._create_purchase(fundraise, self.user, 70.0)
feed_entry = self._create_feed_entry(unified_doc, post)

# Act
serializer = ActivityFeedEntrySerializer(feed_entry)
contributions = serializer.data["contributions"]

# Assert
self.assertEqual(contributions["total"], 1)
top_entry = contributions["top"][0]
self.assertEqual(top_entry["total_contribution"]["rsc"], 100.0)
self.assertEqual(top_entry["total_contribution"]["usd"], 0)
self.assertEqual(len(top_entry["contributions"]), 2)

@patch(
"purchase.related_models.rsc_exchange_rate_model.RscExchangeRate.get_latest_exchange_rate"
)
def test_contributions_include_usd_contributions(self, mock_usd_to_rsc):
"""RSC and USD contributions from the same user should both appear."""
# Arrange
mock_usd_to_rsc.return_value = 1.0

unified_doc, post = self._create_fundraise_post()
fundraise = Fundraise.objects.create(
unified_document=unified_doc,
created_by=self.user,
goal_amount=Decimal("500.00"),
goal_currency=USD,
status=Fundraise.OPEN,
)
contributor = create_random_default_user("mixed_contributor")
self._create_purchase(fundraise, contributor, 10.0)
self._create_usd_contribution(fundraise, contributor, 2500)
feed_entry = self._create_feed_entry(unified_doc, post)

# Act
serializer = ActivityFeedEntrySerializer(feed_entry)
contributions = serializer.data["contributions"]
top_entry = contributions["top"][0]

# Assert
self.assertEqual(top_entry["total_contribution"]["rsc"], 10.0)
self.assertEqual(top_entry["total_contribution"]["usd"], 25.0)
self.assertEqual(len(top_entry["contributions"]), 2)
currencies = {c["currency"] for c in top_entry["contributions"]}
self.assertEqual(currencies, {"RSC", "USD"})


class FundraiseContributionContentSerializerTests(AWSMockTestCase):
"""
Test cases for the FundraiseContributionContentSerializer.
Expand Down
52 changes: 9 additions & 43 deletions src/feed/views/activity_feed_view.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Prefetch
from rest_framework.viewsets import ModelViewSet

from feed.models import FeedEntry
from feed.serializers import ActivityFeedEntrySerializer
from feed.serializers import FeedEntrySerializer
from feed.views.common import FeedPagination
from feed.views.feed_view_mixin import FeedViewMixin
from purchase.models import Fundraise, UsdFundraiseContribution
from purchase.related_models.grant_application_model import GrantApplication
from purchase.related_models.grant_model import Grant
from purchase.related_models.purchase_model import Purchase
from researchhub_comment.constants.rh_comment_thread_types import (
COMMUNITY_REVIEW,
PEER_REVIEW,
Expand All @@ -34,7 +31,7 @@ class ActivityFeedViewSet(FeedViewMixin, ModelViewSet):
returns only comments across all grant-related documents.
"""

serializer_class = ActivityFeedEntrySerializer
serializer_class = FeedEntrySerializer
permission_classes = []
pagination_class = FeedPagination
http_method_names = ["get", "head", "options"]
Expand All @@ -53,44 +50,13 @@ def list(self, request, *args, **kwargs):
return response

def get_queryset(self):
queryset = (
FeedEntry.objects.select_related(
"content_type",
"unified_document",
"user",
"user__author_profile",
"user__userverification",
)
.prefetch_related(
Prefetch(
"unified_document__fundraises",
queryset=Fundraise.objects.prefetch_related(
Prefetch(
"purchases",
queryset=Purchase.objects.select_related(
"user",
"user__author_profile",
"user__userverification",
).order_by("-created_date"),
to_attr="prefetched_purchases",
),
Prefetch(
"usd_contributions",
queryset=UsdFundraiseContribution.objects.select_related(
"user",
"user__author_profile",
"user__userverification",
)
.filter(is_refunded=False)
.order_by("-created_date"),
to_attr="prefetched_usd_contributions",
),
),
to_attr="prefetched_fundraises",
),
)
.order_by("-action_date")
)
queryset = FeedEntry.objects.select_related(
"content_type",
"unified_document",
"user",
"user__author_profile",
"user__userverification",
).order_by("-action_date")

scope = self.request.query_params.get("scope", "").lower()
grant_id = self.request.query_params.get("grant_id")
Expand Down
Loading