Skip to content

Commit 42f1e38

Browse files
feat: Implement GraphQL support for badges
- Add BadgeNode GraphQL type with fields: id, name, description, weight, css_class - Add badges query to return all badges ordered by weight and name - Add badges field to UserNode to expose user badges sorted by weight - Add comprehensive test coverage for BadgeQueries and UserNode badges field - Integrate BadgeQueries into NestQuery schema Fixes #1764
1 parent eade724 commit 42f1e38

File tree

6 files changed

+124
-1
lines changed

6 files changed

+124
-1
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import strawberry_django
55

66
from apps.github.models.user import User
7+
from apps.nest.api.internal.nodes.badge import BadgeNode
78

89

910
@strawberry_django.type(
@@ -51,3 +52,17 @@ def updated_at(self) -> float:
5152
def url(self) -> str:
5253
"""Resolve URL."""
5354
return self.url
55+
56+
@strawberry.field
57+
def badges(self) -> list[BadgeNode]:
58+
"""Return active badges for the user, ordered by badge weight and name."""
59+
# related_name on UserBadge is "badges"; prefetch badge and filter active
60+
user_badges = (
61+
self.badges.select_related("badge")
62+
.filter(is_active=True)
63+
.order_by(
64+
"badge__weight",
65+
"badge__name",
66+
)
67+
)
68+
return [ub.badge for ub in user_badges]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""GraphQL node for Badge model."""
2+
3+
import strawberry
4+
import strawberry_django
5+
6+
from apps.nest.models.badge import Badge
7+
8+
9+
@strawberry_django.type(
10+
Badge,
11+
fields=[
12+
"id",
13+
"name",
14+
"description",
15+
"weight",
16+
"css_class",
17+
],
18+
)
19+
class BadgeNode(strawberry.relay.Node):
20+
"""GraphQL node for Badge model."""
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import strawberry
22

33
from apps.nest.api.internal.queries.api_key import ApiKeyQueries
4+
from apps.nest.api.internal.queries.badge import BadgeQueries
45

56

67
@strawberry.type
7-
class NestQuery(ApiKeyQueries):
8+
class NestQuery(ApiKeyQueries, BadgeQueries):
89
"""Nest query."""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""GraphQL queries for Badge model."""
2+
3+
import strawberry
4+
5+
from apps.nest.api.internal.nodes.badge import BadgeNode
6+
from apps.nest.models.badge import Badge
7+
8+
9+
@strawberry.type
10+
class BadgeQueries:
11+
"""GraphQL query class for badges."""
12+
13+
@strawberry.field
14+
def badges(self) -> list[BadgeNode]:
15+
"""Return all badges ordered by weight and name."""
16+
return list(Badge.objects.all().order_by("weight", "name"))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Tests for badges field on GitHub UserNode."""
2+
3+
from unittest.mock import MagicMock
4+
5+
from apps.github.api.internal.nodes.user import UserNode
6+
7+
8+
class TestUserNodeBadgesField:
9+
"""Test cases for badges field on UserNode."""
10+
11+
def test_badges_resolution_orders_and_filters_active(self):
12+
"""Badges resolution should filter active and order by badge weight/name."""
13+
# Build a lightweight object with required attributes and methods
14+
user = MagicMock()
15+
user_badge_qs = MagicMock()
16+
user.badges.select_related.return_value = user_badge_qs
17+
18+
# Mock chained calls: filter(...).order_by(...) -> [ub1]
19+
ordered_qs = MagicMock()
20+
ordered_qs.__iter__.return_value = iter([])
21+
ub1 = MagicMock()
22+
ub1.badge = MagicMock()
23+
ordered_qs.__iter__.return_value = iter([ub1])
24+
user_badge_qs.filter.return_value = ordered_qs
25+
ordered_qs.order_by.return_value = [ub1]
26+
27+
# Use the resolver through the class to keep Strawberry decorators intact
28+
result = UserNode.badges(user) # pass instance as self
29+
30+
user.badges.select_related.assert_called_once_with("badge")
31+
user_badge_qs.filter.assert_called_once_with(is_active=True)
32+
ordered_qs.order_by.assert_called_once_with("badge__weight", "badge__name")
33+
assert result == [ub1.badge]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Tests for Badge GraphQL queries and nodes."""
2+
3+
from unittest.mock import MagicMock, patch
4+
5+
from apps.nest.api.internal.nodes.badge import BadgeNode
6+
from apps.nest.api.internal.queries.badge import BadgeQueries
7+
from apps.nest.models.badge import Badge
8+
9+
10+
class TestBadgeQueries:
11+
"""Test cases for BadgeQueries class."""
12+
13+
def test_has_strawberry_definition(self):
14+
"""BadgeQueries should be a valid Strawberry type with 'badges' field."""
15+
assert hasattr(BadgeQueries, "__strawberry_definition__")
16+
field_names = [f.name for f in BadgeQueries.__strawberry_definition__.fields]
17+
assert "badges" in field_names
18+
19+
def test_badges_field_configuration(self):
20+
"""'badges' field should return a list of BadgeNode."""
21+
field = next(
22+
f for f in BadgeQueries.__strawberry_definition__.fields if f.name == "badges"
23+
)
24+
assert field.type.of_type is BadgeNode or field.type is BadgeNode
25+
26+
@patch("apps.nest.models.badge.Badge.objects.all")
27+
def test_badges_resolution(self, mock_all):
28+
"""Resolver should return badges ordered by weight and name."""
29+
mock_badge = MagicMock(spec=Badge)
30+
mock_qs = MagicMock()
31+
mock_qs.order_by.return_value = [mock_badge]
32+
mock_all.return_value = mock_qs
33+
34+
result = BadgeQueries().badges()
35+
36+
assert result == [mock_badge]
37+
mock_all.assert_called_once()
38+
mock_qs.order_by.assert_called_once_with("weight", "name")

0 commit comments

Comments
 (0)