Skip to content

Commit 9766b3c

Browse files
Piyushrathoreecoderabbitai[bot]rootkasyaarkid15r
authored
Task/badge implementation in frontend (#2273)
* created some initial file and add some changes * Add Badge functionality to UserNode and integrate Badges component - Introduced BadgeNode for GraphQL representation of badges. - Enhanced UserNode with badges and badge count fields. - Updated UserCard to display badge count and integrated Badges component in UserDetailsPage. - Added tests for badge display and functionality across components. - Updated GraphQL queries to include badge data and counts. * updated the backend file for sorting badges * Refactor tests and mock data for badges functionality - Improved formatting and readability in user_test.py by adjusting line breaks. - Updated mockBadgeData.ts to enhance consistency in object formatting. - Simplified Tooltip mock in Badges.test.tsx for better clarity. - Minor adjustments in Badges.tsx for consistent className formatting. * Update frontend/__tests__/unit/components/Badges.test.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update frontend/src/components/Badges.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update frontend/__tests__/unit/pages/UserDetails.test.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add Font Awesome icons to custom dictionary * Make Badge properties readonly * Sort badges by weight before rendering * Use nullish coalescing for user badges * Add aria-label to FontAwesomeIcon for accessibility * Update user.py * Refactor badge count resolver for improved readability and performance; update badge component imports and cleanup * Update backend/apps/github/api/internal/nodes/user.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update frontend/src/components/Badges.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update frontend/src/components/Badges.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add badgeCount field to UserNode and update related queries; refactor badge count test for clarity * frontend: fix Badges non-prefixed and invalid cssClass handling; adjust UserDetails avatar/layout classes to satisfy tests * fix: correct badge relationship reference in UserNode and update tests - Fixed self.badges to self.user_badges in badges() method - Fixed self.badges to self.user_badges in badge_count() method - Updated all test mocks to use user_badges instead of badges - This resolves the incorrect relationship reference for badge filtering * refactor: streamline badge display in UserDetailsPage component * fix: update useParams to use memberKey in UserDetailsPage and adjust test mocks * fix: adjust class names for better layout consistency in UserDetailsPage * Revert "fix: adjust class names for better layout consistency in UserDetailsPage" This reverts commit 5c69863. * Refactor UserNode badge logic and improve Badge component rendering - Updated UserNode to remove unnecessary filters for badge retrieval and count. - Modified BadgeNode to inherit from Node and adjusted fields accordingly. - Enhanced unit tests for UserNode to reflect changes in badge logic. - Simplified Badges component by removing FontAwesome dependencies and using a wrapper for icon rendering. - Improved UserDetailsPage to utilize badgeCount directly from user data. - Adjusted UsersPage to reference badgeCount instead of calculating length. - Updated GraphQL types to ensure BadgeNode extends Node for consistency. * done with changes suggested by kate . * Update frontend/src/components/Badges.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * CodeRabbit: For Font Awesome icons, the CSS classes are backend-driven (e.g., fa-crown for fa-solid). The frontend dynamically extracts the correct prefix for lookup and rendering. This is a critical design for backend compatibility,they are only fa-solid icons so badges.tsx only checks for fa-solid not anyother families. fix the coderabbit issue with syntax. also run make check-test * migration * Update migration timestamp, enhance badge component, and import icon type definition * Refactor badge class key from 'bug_slash' to 'bugSlash' for consistency * Fix issues with badges * Fix make check * Guard against null/undefined cssClass in normalizeCssClass * Add test to verify badge rendering order per backend contract * Fix backend test for getting users * Update code --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: root <[email protected]> Co-authored-by: Kate Golovanova <[email protected]> Co-authored-by: Arkadii Yakovets <[email protected]>
1 parent 8c30dd4 commit 9766b3c

File tree

27 files changed

+958
-160
lines changed

27 files changed

+958
-160
lines changed

backend/apps/core/utils/index.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def get_params_for_index(index_name: str) -> dict:
196196
case "users":
197197
params["attributesToRetrieve"] = [
198198
"idx_avatar_url",
199+
"idx_badge_count",
199200
"idx_bio",
200201
"idx_company",
201202
"idx_created_at",

backend/apps/github/api/internal/nodes/user.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,27 @@
2828
class UserNode:
2929
"""GitHub user node."""
3030

31+
@strawberry.field
32+
def badge_count(self) -> int:
33+
"""Resolve badge count."""
34+
return self.user_badges.filter(is_active=True).count()
35+
3136
@strawberry.field
3237
def badges(self) -> list[BadgeNode]:
3338
"""Return user badges."""
3439
user_badges = (
35-
self.user_badges.select_related("badge")
36-
.filter(is_active=True)
40+
self.user_badges.filter(
41+
is_active=True,
42+
)
43+
.select_related(
44+
"badge",
45+
)
3746
.order_by(
3847
"badge__weight",
3948
"badge__name",
4049
)
4150
)
42-
return [ub.badge for ub in user_badges]
51+
return [user_badge.badge for user_badge in user_badges]
4352

4453
@strawberry.field
4554
def created_at(self) -> float:

backend/apps/github/index/registry/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class UserIndex(IndexBase):
1414

1515
fields = (
1616
"idx_avatar_url",
17+
"idx_badge_count",
1718
"idx_bio",
1819
"idx_company",
1920
"idx_contributions",

backend/apps/github/index/search/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_users(
3232
"attributesToRetrieve": attributes
3333
or [
3434
"idx_avatar_url",
35+
"idx_badge_count",
3536
"idx_bio",
3637
"idx_company",
3738
"idx_contributions",

backend/apps/github/models/mixins/user.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def idx_avatar_url(self) -> str:
2424
"""Return avatar URL for indexing."""
2525
return self.avatar_url
2626

27+
@property
28+
def idx_badge_count(self) -> int:
29+
"""Return badge count for indexing."""
30+
return self.user_badges.filter(is_active=True).count()
31+
2732
@property
2833
def idx_bio(self) -> str:
2934
"""Return bio for indexing."""
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 5.2.6 on 2025-10-09 04:06
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("nest", "0006_delete_apikey"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="badge",
14+
name="css_class",
15+
field=models.CharField(
16+
choices=[
17+
("award", "Award"),
18+
("medal", "Medal"),
19+
("ribbon", "Ribbon"),
20+
("star", "Star"),
21+
("certificate", "Certificate"),
22+
("bug_slash", "Bug Slash"),
23+
],
24+
default="medal",
25+
max_length=255,
26+
verbose_name="CSS Class",
27+
),
28+
),
29+
]

backend/apps/nest/models/badge.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
class Badge(BulkSaveModel, TimestampedModel):
1111
"""Represents a user badge for roles or achievements."""
1212

13+
class BadgeCssClass(models.TextChoices):
14+
AWARD = "award", "Award"
15+
BUG_SLASH = "bug_slash", "Bug Slash"
16+
CERTIFICATE = "certificate", "Certificate"
17+
MEDAL = "medal", "Medal"
18+
RIBBON = "ribbon", "Ribbon"
19+
STAR = "star", "Star"
20+
1321
class Meta:
1422
db_table = "nest_badges"
1523
ordering = ["weight", "name"]
@@ -18,7 +26,8 @@ class Meta:
1826
css_class = models.CharField(
1927
verbose_name="CSS Class",
2028
max_length=255,
21-
default="",
29+
choices=BadgeCssClass.choices,
30+
default=BadgeCssClass.MEDAL,
2231
)
2332
description = models.CharField(
2433
verbose_name="Description",

backend/tests/apps/core/utils/match_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def test_get_params_for_users(self):
114114
"typoTolerance": "min",
115115
"attributesToRetrieve": [
116116
"idx_avatar_url",
117+
"idx_badge_count",
117118
"idx_bio",
118119
"idx_company",
119120
"idx_created_at",

backend/tests/apps/github/api/internal/nodes/user_test.py

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
from apps.github.api.internal.nodes.user import UserNode
8+
from apps.nest.api.internal.nodes.badge import BadgeNode
89

910

1011
class TestUserNode:
@@ -19,6 +20,7 @@ def test_meta_configuration(self):
1920
field_names = {field.name for field in UserNode.__strawberry_definition__.fields}
2021
expected_field_names = {
2122
"avatar_url",
23+
"badge_count",
2224
"badges",
2325
"bio",
2426
"company",
@@ -81,42 +83,110 @@ def test_url_field(self):
8183
result = UserNode.url(mock_user)
8284
assert result == "https://github.com/testuser"
8385

84-
def test_badges_resolver_behavior(self):
85-
"""Unit test verifies the badges method returns badges sorted by weight."""
86-
badge_high = Mock()
87-
badge_high.weight = 1
88-
badge_high.name = "High Priority Badge"
89-
90-
badge_medium = Mock()
91-
badge_medium.weight = 5
92-
badge_medium.name = "Medium Priority Badge"
86+
def test_badge_count_field(self):
87+
"""Test badge_count field resolution."""
88+
mock_user = Mock()
89+
mock_badges_queryset = Mock()
90+
mock_badges_queryset.filter.return_value.count.return_value = 3
91+
mock_user.user_badges = mock_badges_queryset
9392

94-
badge_low = Mock()
95-
badge_low.weight = 10
96-
badge_low.name = "Low Priority Badge"
93+
result = UserNode.badge_count(mock_user)
94+
assert result == 3
95+
mock_badges_queryset.filter.assert_called_once_with(is_active=True)
96+
mock_badges_queryset.filter.return_value.count.assert_called_once()
9797

98-
user_badge_high = Mock()
99-
user_badge_high.badge = badge_high
98+
def test_badges_field_empty(self):
99+
"""Test badges field resolution with no badges."""
100+
mock_user = Mock()
101+
mock_badges_queryset = Mock()
102+
mock_filter = mock_badges_queryset.filter.return_value
103+
mock_select_related = mock_filter.select_related.return_value
104+
mock_select_related.order_by.return_value = []
105+
mock_user.user_badges = mock_badges_queryset
100106

101-
user_badge_medium = Mock()
102-
user_badge_medium.badge = badge_medium
107+
result = UserNode.badges(mock_user)
108+
assert result == []
109+
mock_badges_queryset.filter.assert_called_once_with(is_active=True)
110+
mock_filter.select_related.assert_called_once_with("badge")
111+
mock_select_related.order_by.assert_called_once_with("badge__weight", "badge__name")
103112

104-
user_badge_low = Mock()
105-
user_badge_low.badge = badge_low
113+
def test_badges_field_single_badge(self):
114+
"""Test badges field resolution with single badge."""
115+
mock_user = Mock()
116+
mock_badge = Mock(spec=BadgeNode)
117+
mock_user_badge = Mock()
118+
mock_user_badge.badge = mock_badge
106119

107-
sorted_user_badges = [user_badge_high, user_badge_medium, user_badge_low]
120+
mock_badges_queryset = Mock()
121+
mock_filter = mock_badges_queryset.filter.return_value
122+
mock_select_related = mock_filter.select_related.return_value
123+
mock_select_related.order_by.return_value = [mock_user_badge]
124+
mock_user.user_badges = mock_badges_queryset
108125

126+
result = UserNode.badges(mock_user)
127+
assert result == [mock_badge]
128+
mock_badges_queryset.filter.assert_called_once_with(is_active=True)
129+
mock_filter.select_related.assert_called_once_with("badge")
130+
mock_select_related.order_by.assert_called_once_with("badge__weight", "badge__name")
131+
132+
def test_badges_field_sorted_by_weight_and_name(self):
133+
"""Test badges field resolution with multiple badges sorted by weight and name."""
134+
# Create mock badges with different weights and names
135+
mock_badge_high_weight = Mock(spec=BadgeNode)
136+
mock_badge_high_weight.weight = 100
137+
mock_badge_high_weight.name = "High Weight Badge"
138+
139+
mock_badge_medium_weight_a = Mock(spec=BadgeNode)
140+
mock_badge_medium_weight_a.weight = 50
141+
mock_badge_medium_weight_a.name = "Medium Weight A"
142+
143+
mock_badge_medium_weight_b = Mock(spec=BadgeNode)
144+
mock_badge_medium_weight_b.weight = 50
145+
mock_badge_medium_weight_b.name = "Medium Weight B"
146+
147+
mock_badge_low_weight = Mock(spec=BadgeNode)
148+
mock_badge_low_weight.weight = 10
149+
mock_badge_low_weight.name = "Low Weight Badge"
150+
151+
# Create mock user badges
152+
mock_user_badge_high = Mock()
153+
mock_user_badge_high.badge = mock_badge_high_weight
154+
155+
mock_user_badge_medium_a = Mock()
156+
mock_user_badge_medium_a.badge = mock_badge_medium_weight_a
157+
158+
mock_user_badge_medium_b = Mock()
159+
mock_user_badge_medium_b.badge = mock_badge_medium_weight_b
160+
161+
mock_user_badge_low = Mock()
162+
mock_user_badge_low.badge = mock_badge_low_weight
163+
164+
# Set up the mock queryset to return badges in the expected sorted order
165+
# (lowest weight first, then by name for same weight)
166+
mock_badges_queryset = Mock()
167+
mock_filter = mock_badges_queryset.filter.return_value
168+
mock_select_related = mock_filter.select_related.return_value
169+
mock_select_related.order_by.return_value = [
170+
mock_user_badge_low, # weight 10
171+
mock_user_badge_medium_a, # weight 50, name "Medium Weight A"
172+
mock_user_badge_medium_b, # weight 50, name "Medium Weight B"
173+
mock_user_badge_high, # weight 100
174+
]
109175
mock_user = Mock()
110-
mock_queryset = Mock()
111-
mock_queryset.filter.return_value.order_by.return_value = sorted_user_badges
112-
mock_user.user_badges.select_related.return_value = mock_queryset
176+
mock_user.user_badges = mock_badges_queryset
113177

114178
result = UserNode.badges(mock_user)
115179

116-
mock_user.user_badges.select_related.assert_called_once_with("badge")
117-
mock_queryset.filter.assert_called_once_with(is_active=True)
118-
mock_queryset.filter.return_value.order_by.assert_called_once_with(
119-
"badge__weight", "badge__name"
120-
)
121-
122-
assert result == [ub.badge for ub in sorted_user_badges]
180+
# Verify the badges are returned in the correct order
181+
expected_badges = [
182+
mock_badge_low_weight,
183+
mock_badge_medium_weight_a,
184+
mock_badge_medium_weight_b,
185+
mock_badge_high_weight,
186+
]
187+
assert result == expected_badges
188+
189+
# Verify the queryset was called with correct ordering
190+
mock_badges_queryset.filter.assert_called_once_with(is_active=True)
191+
mock_filter.select_related.assert_called_once_with("badge")
192+
mock_select_related.order_by.assert_called_once_with("badge__weight", "badge__name")

0 commit comments

Comments
 (0)