Skip to content
Open
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
39 changes: 38 additions & 1 deletion src/feed/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def _apply_custom_sorting(self, queryset: QuerySet, model_config: dict, request:
return self._apply_most_applicants_sorting(queryset, model_config['model_class'])
elif ordering == 'amount_raised':
return self._apply_amount_raised_sorting(queryset, model_config['model_class'])
elif ordering == 'leaderboard':
return self._apply_leaderboard_sorting(queryset)
else:
# For any other ordering field, fall back to DRF's standard ordering
return super().filter_queryset(request, queryset, view)
Expand All @@ -102,7 +104,7 @@ def get_ordering(self, request: Request, queryset: QuerySet, view: Any):
if fields:
field = fields[0]
field_name = field.lstrip('-')
custom_fields = ['newest', 'best', 'upvotes', 'most_applicants', 'amount_raised']
custom_fields = ['newest', 'best', 'upvotes', 'most_applicants', 'amount_raised', 'leaderboard']
if field_name in custom_fields:
return [field]
ordering_fields = getattr(view, 'ordering_fields', None)
Expand Down Expand Up @@ -230,6 +232,41 @@ def _apply_most_applicants_sorting(self, queryset: QuerySet, model_class: Union[
)
).order_by("-contributor_count", "-created_date")

def _apply_leaderboard_sorting(self, queryset: QuerySet) -> QuerySet:
"""Return top OPEN grants sorted by total contributions to their proposals."""
leaderboard_size = 5
now = timezone.now()

queryset = queryset.filter(
Q(unified_document__grants__status=Grant.OPEN),
Q(unified_document__grants__end_date__isnull=True)
| Q(unified_document__grants__end_date__gt=now),
)

# Build the ORM path: grant post → grant → applications → proposal → fundraise → escrow
grant = "unified_document__grants"
proposals = f"{grant}__applications__preregistration_post"
fundraises = f"{proposals}__unified_document__fundraises"
escrow = f"{fundraises}__escrow"

queryset = queryset.annotate(
total_funded=Coalesce(
Sum(F(f"{escrow}__amount_holding") + F(f"{escrow}__amount_paid")),
Value(0),
output_field=DecimalField(max_digits=19, decimal_places=10),
),
grant_amount=Coalesce(
Sum(
F("unified_document__grants__amount"),
output_field=DecimalField(max_digits=19, decimal_places=2),
),
Value(0),
output_field=DecimalField(max_digits=19, decimal_places=2),
),
)

return queryset.order_by("-total_funded", "-grant_amount")[:leaderboard_size]

def _apply_amount_raised_sorting(self, queryset: QuerySet, model_class: Union[Type[Grant], Type[Fundraise]]) -> QuerySet:
if model_class == Grant:
return queryset.annotate(
Expand Down
48 changes: 46 additions & 2 deletions src/feed/tests/views/test_grant_feed_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
from decimal import Decimal

import pytz
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from rest_framework.test import APITestCase

from purchase.models import Grant, GrantApplication
from purchase.models import Fundraise, Grant, GrantApplication
from reputation.models import Escrow
from researchhub_document.helpers import create_post
from researchhub_document.related_models.constants.document_type import (
GRANT,
PREREGISTRATION,
)
from researchhub_document.related_models.researchhub_post_model import ResearchhubPost
from researchhub_document.related_models.researchhub_unified_document_model import (
ResearchhubUnifiedDocument,
)
from user.tests.helpers import create_random_authenticated_user


Expand Down Expand Up @@ -543,4 +548,43 @@ def test_created_by_filter_returns_empty_for_non_creator(self):
response = self.client.get(f"/api/grant_feed/?created_by={self.user.id}")

# Assert
self.assertEqual(len(response.data["results"]), 0)
self.assertEqual(len(response.data["results"]), 0)

def test_leaderboard_ordering(self):
"""Top 5 OPEN grants: funded first, then by budget; closed excluded."""
# Arrange — 6 open grants (+ 1 from setUp = 7 open, 2 closed/completed)
grants = []
for i in range(6):
post = create_post(created_by=self.moderator, document_type=GRANT, title=f"Grant {i}")
grants.append(Grant.objects.create(
created_by=self.moderator, unified_document=post.unified_document,
amount=Decimal(str(1000 * (i + 1))), currency="USD",
organization="Org", description="", status=Grant.OPEN,
))

# Fund grants[0] (smallest budget) so it ranks first
applicant = create_random_authenticated_user("applicant")
doc = ResearchhubUnifiedDocument.objects.create(document_type=PREREGISTRATION)
proposal = ResearchhubPost.objects.create(
title="P", created_by=applicant, document_type=PREREGISTRATION, unified_document=doc,
)
GrantApplication.objects.create(grant=grants[0], preregistration_post=proposal, applicant=applicant)
escrow = Escrow.objects.create(
amount_holding=99999, hold_type=Escrow.FUNDRAISE, created_by=applicant,
content_type=ContentType.objects.get_for_model(ResearchhubUnifiedDocument), object_id=doc.id,
)
Fundraise.objects.create(
created_by=applicant, unified_document=doc, escrow=escrow,
status=Fundraise.OPEN, goal_amount=100000,
)

# Act
self.client.force_authenticate(self.user)
response = self.client.get("/api/grant_feed/?ordering=leaderboard")
titles = [r["content_object"]["title"] for r in response.data["results"]]

# Assert — capped at 5, funded grant first, closed/completed excluded
self.assertEqual(len(titles), 5)
self.assertEqual(titles[0], "Grant 0")
self.assertNotIn("Closed Grant", titles)
self.assertNotIn("Completed Grant", titles)
2 changes: 1 addition & 1 deletion src/feed/views/grant_feed_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class GrantFeedViewSet(FeedViewMixin, ModelViewSet):
pagination_class = FeedPagination
filter_backends = [DjangoFilterBackend, FundOrderingFilter]
is_grant_view = True
ordering_fields = ['newest', 'upvotes', 'most_applicants', 'amount_raised']
ordering_fields = ['newest', 'upvotes', 'most_applicants', 'amount_raised', 'leaderboard']
ordering = 'newest' # Default ordering

def get_serializer_context(self):
Expand Down
Loading