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
13 changes: 10 additions & 3 deletions backend/contributions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
METRICS_POINTS_EXCLUDED_TYPE_SLUGS = [
'builder-welcome',
'builder',
'validator-waitlist',
'validator',
'community-link-x',
'community-link-discord',
]
Expand Down Expand Up @@ -302,9 +304,14 @@ def get_queryset(self):
contribution_type__slug__in=['builder-welcome', 'validator-waitlist']
)

# Optionally restrict to contributions whose type is submittable.
# Used by the public contributions explorer to hide system-generated
# contributions (badges, journey rewards) from non-submittable types.
# Optionally remove journey/social-link onboarding records so dashboard
# contribution lists match member metrics.
exclude_onboarding = self.request.query_params.get('exclude_onboarding')
if exclude_onboarding and exclude_onboarding.lower() == 'true':
queryset = queryset.exclude(
contribution_type__slug__in=METRICS_POINTS_EXCLUDED_TYPE_SLUGS
)

submittable_only = self.request.query_params.get('submittable_only')
if submittable_only and submittable_only.lower() == 'true':
queryset = queryset.filter(contribution_type__is_submittable=True)
Expand Down
42 changes: 42 additions & 0 deletions backend/creators/migrations/0002_backfill_community_members.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.db import migrations


def backfill_community_members(apps, schema_editor):
Creator = apps.get_model('creators', 'Creator')
Contribution = apps.get_model('contributions', 'Contribution')
PoapClaim = apps.get_model('poaps', 'PoapClaim')

user_ids = set(
Contribution.objects.filter(
contribution_type__category__slug='community',
).values_list('user_id', flat=True).distinct()
)
user_ids.update(
PoapClaim.objects.filter(
user__isnull=False,
).values_list('user_id', flat=True).distinct()
)

existing_user_ids = set(
Creator.objects.filter(user_id__in=user_ids).values_list('user_id', flat=True)
)
missing_user_ids = user_ids - existing_user_ids

Creator.objects.bulk_create(
[Creator(user_id=user_id) for user_id in missing_user_ids],
ignore_conflicts=True,
batch_size=500,
)


class Migration(migrations.Migration):

dependencies = [
('creators', '0001_initial'),
('contributions', '0061_featuredcontent_hero_placements'),
('poaps', '0001_initial'),
]

operations = [
migrations.RunPython(backfill_community_members, migrations.RunPython.noop),
]
26 changes: 26 additions & 0 deletions backend/creators/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from creators.models import Creator


def ensure_creator_status(user):
if not user or not getattr(user, 'pk', None):
return None

creator, _ = Creator.objects.get_or_create(user=user)
return creator


def ensure_creator_status_for_users(user_ids):
user_ids = {user_id for user_id in user_ids if user_id}
if not user_ids:
return

existing_user_ids = set(
Creator.objects.filter(user_id__in=user_ids).values_list('user_id', flat=True)
)
missing_user_ids = user_ids - existing_user_ids
if missing_user_ids:
Creator.objects.bulk_create(
[Creator(user_id=user_id) for user_id in missing_user_ids],
ignore_conflicts=True,
batch_size=500,
)
8 changes: 8 additions & 0 deletions backend/leaderboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,14 @@ def update_leaderboard_on_contribution(sender, instance, created, **kwargs):
logger.debug(f"Contribution saved: {instance.points} points × {instance.multiplier_at_creation} = "
f"{instance.frozen_global_points} global points")

if (
instance.contribution_type
and instance.contribution_type.category
and instance.contribution_type.category.slug == 'community'
):
from creators.utils import ensure_creator_status
ensure_creator_status(instance.user)

# Update the user's leaderboard entries
update_user_leaderboard_entries(instance.user)

Expand Down
103 changes: 102 additions & 1 deletion backend/leaderboard/tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
Category,
Contribution,
ContributionType,
Mission,
SubmittedContribution,
)
from leaderboard.models import ReferralPoints
from creators.models import Creator
from leaderboard.models import GlobalLeaderboardMultiplier, ReferralPoints
from poaps.models import PoapClaim, PoapDrop
from users.models import User


Expand All @@ -33,6 +36,15 @@ def setUp(self):
slug='builder-submission',
category=self.builder_category
)
self.community_link_x_type, _ = ContributionType.objects.get_or_create(
slug='community-link-x',
defaults={
'name': 'Community Link X',
'category': self.community_category,
'min_points': 0,
'max_points': 100,
},
)

def _create_user(self, email, address, visible=True):
return User.objects.create_user(
Expand Down Expand Up @@ -124,3 +136,92 @@ def test_community_member_count_uses_accepted_community_contributions(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['community_member_count'], 2)
self.assertEqual(response.data['creator_count'], 2)
self.assertEqual(response.data['builder_count'], 1)

def test_poap_claim_grants_role_but_does_not_count_as_member_metric(self):
poap_user = self._create_user(
'poap@example.com',
'0x0000000000000000000000000000000000000007'
)
drop = PoapDrop.objects.create(
title='Community Call',
slug='community-call',
event_start_at=timezone.now(),
status=PoapDrop.STATUS_ACTIVE,
)

PoapClaim.objects.create(
drop=drop,
user=poap_user,
claim_method=PoapClaim.CLAIM_ADMIN,
)

response = self.client.get('/api/v1/leaderboard/stats/')

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['community_member_count'], 0)
self.assertTrue(Creator.objects.filter(user=poap_user).exists())

def test_community_social_link_contributions_do_not_count_as_members_or_activity(self):
social_user = self._create_user(
'social@example.com',
'0x0000000000000000000000000000000000000009'
)
Contribution.objects.create(
user=social_user,
contribution_type=self.community_link_x_type,
points=20,
frozen_global_points=20,
contribution_date=timezone.now()
)

response = self.client.get('/api/v1/leaderboard/stats/', {'type': 'community'})

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['community_member_count'], 0)
self.assertEqual(response.data['participant_count'], 0)
self.assertEqual(response.data['contribution_count'], 0)

def test_mission_backed_non_submittable_community_contribution_is_reflected(self):
contributor = self._create_user(
'mission-community@example.com',
'0x0000000000000000000000000000000000000008'
)
mission_type = ContributionType.objects.create(
name='Community Mission Host',
slug='community-mission-host',
category=self.community_category,
min_points=1,
max_points=100,
is_submittable=False,
)
GlobalLeaderboardMultiplier.objects.create(
contribution_type=mission_type,
multiplier_value=1,
valid_from=timezone.now() - timezone.timedelta(days=30),
)
mission = Mission.objects.create(
name='Community Mission',
description='Mission-backed community work',
contribution_type=mission_type,
)

contribution = Contribution.objects.create(
user=contributor,
contribution_type=mission_type,
mission=mission,
points=25,
contribution_date=timezone.now(),
)

stats_response = self.client.get('/api/v1/leaderboard/stats/', {'type': 'community'})
monthly_response = self.client.get('/api/v1/leaderboard/monthly/', {'type': 'community'})

self.assertEqual(stats_response.status_code, 200)
self.assertEqual(stats_response.data['community_member_count'], 1)
self.assertEqual(stats_response.data['contribution_count'], 1)
self.assertTrue(Creator.objects.filter(user=contributor).exists())

self.assertEqual(monthly_response.status_code, 200)
self.assertEqual(monthly_response.data[0]['user'], contributor.id)
self.assertEqual(monthly_response.data[0]['total_points'], contribution.frozen_global_points)
Loading
Loading