Skip to content

Commit 1dd2211

Browse files
Implement GraphQL resolvers for project health metrics (#1577)
* Add project_health_metrics node and query * Add health field to the ProjectNode that represents sll ProjectHealthMetrics objects of the project * Add tests * Update filtering and add fields to models * Update filtering * Update tests * Save new boolean values * Add boolean mapping * Add query tests * Merge migrations * Update filtering, add migrations, and update scripts * Update tests and queries * Add test with filters * Update filtering * Update tests * Merge migrations * Revert unnecessary work and apply suggestions * Remove has_no_recent_commits from project * Add missing fields for FE query * Remove project name from the test * Clean migrations * Update code --------- Co-authored-by: Arkadii Yakovets <[email protected]>
1 parent 7d86175 commit 1dd2211

File tree

6 files changed

+153
-6
lines changed

6 files changed

+153
-6
lines changed

backend/apps/owasp/graphql/nodes/project.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from apps.github.graphql.nodes.release import ReleaseNode
1010
from apps.github.graphql.nodes.repository import RepositoryNode
1111
from apps.owasp.graphql.nodes.common import GenericEntityNode
12+
from apps.owasp.graphql.nodes.project_health_metrics import ProjectHealthMetricsNode
1213
from apps.owasp.models.project import Project
14+
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
1315

1416
RECENT_ISSUES_LIMIT = 5
1517
RECENT_RELEASES_LIMIT = 5
@@ -34,6 +36,15 @@
3436
class ProjectNode(GenericEntityNode):
3537
"""Project node."""
3638

39+
@strawberry.field
40+
def health_metrics(self, limit: int = 30) -> list[ProjectHealthMetricsNode]:
41+
"""Resolve project health metrics."""
42+
return ProjectHealthMetrics.objects.filter(
43+
project=self,
44+
).order_by(
45+
"-nest_created_at",
46+
)[:limit]
47+
3748
@strawberry.field
3849
def issues_count(self) -> int:
3950
"""Resolve issues count."""
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""OWASP Project Health Metrics Node."""
2+
3+
import strawberry
4+
import strawberry_django
5+
6+
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
7+
8+
9+
@strawberry_django.type(
10+
ProjectHealthMetrics,
11+
fields=[
12+
"contributors_count",
13+
"forks_count",
14+
"is_funding_requirements_compliant",
15+
"is_leader_requirements_compliant",
16+
"open_issues_count",
17+
"open_pull_requests_count",
18+
"recent_releases_count",
19+
"score",
20+
"stars_count",
21+
"unanswered_issues_count",
22+
"unassigned_issues_count",
23+
],
24+
)
25+
class ProjectHealthMetricsNode:
26+
"""Project health metrics node."""
27+
28+
@strawberry.field
29+
def age_days(self) -> int:
30+
"""Resolve project age in days."""
31+
return self.age_days
32+
33+
@strawberry.field
34+
def last_commit_days(self) -> int:
35+
"""Resolve last commit age in days."""
36+
return self.last_commit_days
37+
38+
@strawberry.field
39+
def last_pull_request_days(self) -> int:
40+
"""Resolve last pull request age in days."""
41+
return self.last_pull_request_days
42+
43+
@strawberry.field
44+
def last_release_days(self) -> int:
45+
"""Resolve last release age in days."""
46+
return self.last_release_days
47+
48+
@strawberry.field
49+
def owasp_page_last_update_days(self) -> int:
50+
"""Resolve OWASP page last update age in days."""
51+
return self.owasp_page_last_update_days

backend/apps/owasp/management/commands/owasp_update_project_health_metrics_scores.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ def handle(self, *args, **options):
6060
metric.score = score
6161
project_health_metrics.append(metric)
6262

63-
ProjectHealthMetrics.bulk_save(project_health_metrics, fields=["score"])
63+
ProjectHealthMetrics.bulk_save(
64+
project_health_metrics,
65+
fields=[
66+
"score",
67+
],
68+
)
6469
self.stdout.write(
6570
self.style.SUCCESS("Updated projects health metrics score successfully.")
6671
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Test cases for ProjectHealthMetricsNode."""
2+
3+
import pytest
4+
5+
from apps.owasp.graphql.nodes.project_health_metrics import ProjectHealthMetricsNode
6+
7+
8+
class TestProjectHealthMetricsNode:
9+
def test_project_health_metrics_node_inheritance(self):
10+
assert hasattr(ProjectHealthMetricsNode, "__strawberry_definition__")
11+
12+
def test_meta_configuration(self):
13+
field_names = {
14+
field.name for field in ProjectHealthMetricsNode.__strawberry_definition__.fields
15+
}
16+
expected_field_names = {
17+
"age_days",
18+
"contributors_count",
19+
"forks_count",
20+
"is_funding_requirements_compliant",
21+
"is_leader_requirements_compliant",
22+
"last_commit_days",
23+
"last_pull_request_days",
24+
"last_release_days",
25+
"open_issues_count",
26+
"open_pull_requests_count",
27+
"owasp_page_last_update_days",
28+
"recent_releases_count",
29+
"score",
30+
"stars_count",
31+
"unanswered_issues_count",
32+
"unassigned_issues_count",
33+
}
34+
assert expected_field_names.issubset(field_names)
35+
36+
def _get_field_by_name(self, name):
37+
return next(
38+
(
39+
f
40+
for f in ProjectHealthMetricsNode.__strawberry_definition__.fields
41+
if f.name == name
42+
),
43+
None,
44+
)
45+
46+
@pytest.mark.parametrize(
47+
("field_name", "expected_type"),
48+
[
49+
("age_days", int),
50+
("contributors_count", int),
51+
("forks_count", int),
52+
("is_funding_requirements_compliant", bool),
53+
("is_leader_requirements_compliant", bool),
54+
("last_commit_days", int),
55+
("last_pull_request_days", int),
56+
("last_release_days", int),
57+
("open_issues_count", int),
58+
("open_pull_requests_count", int),
59+
("owasp_page_last_update_days", int),
60+
("stars_count", int),
61+
("recent_releases_count", int),
62+
("unanswered_issues_count", int),
63+
("unassigned_issues_count", int),
64+
],
65+
)
66+
def test_field_types(self, field_name, expected_type):
67+
field = self._get_field_by_name(field_name)
68+
assert field is not None
69+
assert field.type is expected_type

backend/tests/apps/owasp/graphql/nodes/project_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from apps.github.graphql.nodes.release import ReleaseNode
77
from apps.github.graphql.nodes.repository import RepositoryNode
88
from apps.owasp.graphql.nodes.project import ProjectNode
9+
from apps.owasp.graphql.nodes.project_health_metrics import ProjectHealthMetricsNode
910

1011

1112
class TestProjectNode:
@@ -43,6 +44,11 @@ def _get_field_by_name(self, name):
4344
(f for f in ProjectNode.__strawberry_definition__.fields if f.name == name), None
4445
)
4546

47+
def test_resolve_health_metrics(self):
48+
field = self._get_field_by_name("health_metrics")
49+
assert field is not None
50+
assert field.type.of_type is ProjectHealthMetricsNode
51+
4652
def test_resolve_issues_count(self):
4753
field = self._get_field_by_name("issues_count")
4854
assert field is not None

backend/tests/apps/owasp/management/commands/owasp_update_project_health_metrics_scores_test.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
99
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements
1010

11-
EXPECTED_SCORE = 52.0
11+
EXPECTED_SCORE = 34.0
1212

1313

1414
class TestUpdateProjectHealthMetricsScoreCommand:
@@ -41,16 +41,16 @@ def test_handle_successful_update(self):
4141
"forks_count": (5, 6),
4242
"last_release_days": (5, 6),
4343
"last_commit_days": (5, 6),
44-
"open_issues_count": (5, 6),
44+
"open_issues_count": (7, 6),
4545
"open_pull_requests_count": (5, 6),
4646
"owasp_page_last_update_days": (5, 6),
4747
"last_pull_request_days": (5, 6),
4848
"recent_releases_count": (5, 6),
4949
"stars_count": (5, 6),
5050
"total_pull_requests_count": (5, 6),
5151
"total_releases_count": (5, 6),
52-
"unanswered_issues_count": (5, 6),
53-
"unassigned_issues_count": (5, 6),
52+
"unanswered_issues_count": (7, 6),
53+
"unassigned_issues_count": (7, 6),
5454
}
5555

5656
# Create mock metrics with test data
@@ -73,7 +73,12 @@ def test_handle_successful_update(self):
7373
self.mock_requirements.assert_called_once()
7474

7575
# Check if score was calculated correctly
76-
self.mock_bulk_save.assert_called_once_with([mock_metric], fields=["score"])
76+
self.mock_bulk_save.assert_called_once_with(
77+
[mock_metric],
78+
fields=[
79+
"score",
80+
],
81+
)
7782
assert mock_metric.score == EXPECTED_SCORE
7883
assert "Updated projects health metrics score successfully." in self.stdout.getvalue()
7984
assert "Updating score for project: Test Project" in self.stdout.getvalue()

0 commit comments

Comments
 (0)