Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
22 changes: 22 additions & 0 deletions backend/apps/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,28 @@ def truncate(text: str, limit: int, truncate: str = "...") -> str:
return Truncator(text).chars(limit, truncate=truncate)


def normalize_limit(limit: int, max_limit: int = 1000) -> int | None:
Comment thread
arkid15r marked this conversation as resolved.
"""Normalize and validate a limit parameter.

Args:
limit (int): The requested limit.
max_limit (int): The maximum allowed limit. Defaults to 1000.

Returns:
int | None: The normalized limit capped at max_limit, or None if invalid.

"""
try:
limit = int(limit)
except (TypeError, ValueError):
return None

if limit <= 0:
return None

return min(limit, max_limit)


def validate_url(url: str | None) -> bool:
"""Validate that a URL has proper scheme and netloc.

Expand Down
7 changes: 6 additions & 1 deletion backend/apps/github/api/internal/nodes/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import strawberry
import strawberry_django

from apps.common.utils import normalize_limit
from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.api.internal.nodes.milestone import MilestoneNode
from apps.github.api.internal.nodes.organization import OrganizationNode
Expand All @@ -15,6 +16,7 @@
if TYPE_CHECKING:
from apps.owasp.api.internal.nodes.project import ProjectNode

MAX_LIMIT = 1000
RECENT_ISSUES_LIMIT = 5
RECENT_RELEASES_LIMIT = 5

Expand Down Expand Up @@ -69,7 +71,10 @@ def project(
@strawberry_django.field(prefetch_related=["milestones"])
def recent_milestones(self, root: Repository, limit: int = 5) -> list[MilestoneNode]:
"""Resolve recent milestones."""
return root.recent_milestones.order_by("-created_at")[:limit]
if (validated_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return root.recent_milestones.order_by("-created_at")[:validated_limit]

@strawberry_django.field(prefetch_related=["releases"])
def releases(self, root: Repository) -> list[ReleaseNode]:
Expand Down
10 changes: 5 additions & 5 deletions backend/apps/github/api/internal/queries/milestone.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import strawberry_django
from django.db.models import OuterRef, Subquery

from apps.common.utils import normalize_limit
from apps.github.api.internal.nodes.milestone import MilestoneNode
from apps.github.models.generic_issue_model import GenericIssueModel
from apps.github.models.milestone import Milestone
Expand Down Expand Up @@ -76,8 +77,7 @@ def recent_milestones(
id__in=Subquery(latest_milestone_per_author),
)

return (
milestones.order_by("-created_at")[:limit]
if (limit := min(limit, MAX_LIMIT)) > 0
else []
)
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return milestones.order_by("-created_at")[:normalized_limit]
21 changes: 10 additions & 11 deletions backend/apps/owasp/api/internal/nodes/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
import strawberry_django

from apps.common.utils import normalize_limit
from apps.core.utils.index import deep_camelize
from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.api.internal.nodes.milestone import MilestoneNode
Expand Down Expand Up @@ -53,11 +54,10 @@ def health_metrics_list(
self, root: Project, limit: int = 30
) -> list[ProjectHealthMetricsNode]:
"""Resolve project health metrics."""
return (
root.health_metrics.order_by("nest_created_at")[:limit]
if (limit := min(limit, MAX_LIMIT)) > 0
else []
)
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return root.health_metrics.order_by("nest_created_at")[:normalized_limit]

@strawberry_django.field(prefetch_related=["health_metrics"])
def health_metrics_latest(self, root: Project) -> ProjectHealthMetricsNode | None:
Expand Down Expand Up @@ -87,6 +87,9 @@ def recent_issues(self, root: Project) -> list[IssueNode]:
@strawberry_django.field
def recent_milestones(self, root: Project, limit: int = 5) -> list[MilestoneNode]:
"""Resolve recent milestones."""
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return (
Milestone.objects.filter(
repository__in=root.repositories.all(),
Expand All @@ -95,12 +98,8 @@ def recent_milestones(self, root: Project, limit: int = 5) -> list[MilestoneNode
"repository__organization",
"author__owasp_profile",
)
.prefetch_related(
"labels",
)
.order_by("-created_at")[:limit]
if (limit := min(limit, MAX_LIMIT)) > 0
else []
.prefetch_related("labels")
.order_by("-created_at")[:normalized_limit]
)

@strawberry_django.field
Expand Down
10 changes: 5 additions & 5 deletions backend/apps/owasp/api/internal/queries/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import strawberry_django
from django.db.models import Q

from apps.common.utils import normalize_limit
from apps.github.models.user import User as GithubUser
from apps.owasp.api.internal.nodes.project import ProjectNode
from apps.owasp.models.project import Project
Expand Down Expand Up @@ -45,11 +46,10 @@ def recent_projects(self, limit: int = 8) -> list[ProjectNode]:
list[ProjectNode]: A list of recent active projects.

"""
return (
Project.objects.filter(is_active=True).order_by("-created_at")[:limit]
if (limit := min(limit, MAX_RECENT_PROJECTS_LIMIT)) > 0
else []
)
if (normalized_limit := normalize_limit(limit, MAX_RECENT_PROJECTS_LIMIT)) is None:
return []

return Project.objects.filter(is_active=True).order_by("-created_at")[:normalized_limit]

@strawberry_django.field
def search_projects(self, query: str) -> list[ProjectNode]:
Expand Down
43 changes: 43 additions & 0 deletions backend/tests/apps/common/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
join_values,
natural_date,
natural_number,
normalize_limit,
round_down,
validate_url,
)
Expand Down Expand Up @@ -196,3 +197,45 @@ def test_validate_url(self, url, expected):
"""Test the validate_url function."""
result = validate_url(url)
assert result == expected

@pytest.mark.parametrize(
("limit", "max_limit", "expected"),
[
(5, 1000, 5),
(100, 1000, 100),
(1000, 1000, 1000),
(1500, 1000, 1000),
(999, 1000, 999),
(0, 1000, None),
(-5, 1000, None),
(5, 10, 5),
(15, 10, 10),
(100, 50, 50),
(1, 1, 1),
],
)
def test_normalize_limit(self, limit, max_limit, expected):
"""Test the normalize_limit function with valid integers."""
assert normalize_limit(limit, max_limit) == expected

@pytest.mark.parametrize(
("limit", "max_limit"),
[
("invalid", 1000),
("5.5", 1000),
(None, 1000),
([], 1000),
({}, 1000),
],
)
def test_normalize_limit_invalid_types(self, limit, max_limit):
"""Test the normalize_limit function with invalid types."""
assert normalize_limit(limit, max_limit) is None

def test_normalize_limit_default_max_limit(self):
"""Test the normalize_limit function with default max_limit."""
assert normalize_limit(500) == 500
assert normalize_limit(1000) == 1000
assert normalize_limit(1500) == 1000
assert normalize_limit(-1) is None
assert normalize_limit(0) is None
23 changes: 23 additions & 0 deletions frontend/__tests__/mockData/mockAboutData.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,85 @@
export const mockAboutData = {
project: {
id: 'project-nest',
name: 'OWASP Nest',
contributorsCount: 1200,
issuesCount: 40,
forksCount: 60,
starsCount: 890,
summary: 'A community-first platform for OWASP collaboration',
recentMilestones: [
{
id: 'milestone-1',
title: 'NestBot title',
body: 'NestBot Idea',
url: 'http/github.com/milestones/5',
progress: 58,
state: 'open',
},
{
id: 'milestone-2',
title: 'Contribution Hub title',
body: 'Contribution Hub Idea',
url: 'http/github.com/milestones/8',
progress: 75,
state: 'open',
},
{
id: 'milestone-3',
title: 'Project Dashboard title',
body: 'Project Dashboard Idea',
url: 'http/github.com/milestones/10',
progress: 80,
state: 'open',
},
{
id: 'milestone-4',
title: 'Milestone 4',
body: 'Milestone 4 Idea',
url: 'http/github.com/milestones/11',
progress: 20,
state: 'open',
},
{
id: 'milestone-5',
title: 'Milestone 5',
body: 'Milestone 5 Idea',
url: 'http/github.com/milestones/12',
progress: 40,
state: 'open',
},
],
},
topContributors: Array.from({ length: 15 }, (_, i) => ({
id: `contributor-${i + 1}`,
avatarUrl: `https://avatars.githubusercontent.com/avatar${i + 1}.jpg`,
login: `contributor${i + 1}`,
name: `Contributor ${i + 1}`,
})),
users: {
arkid15r: {
id: 'user-arkid15r',
avatarUrl: 'https://avatars.githubusercontent.com/u/2201626?v=4',
login: 'arkid15r',
name: 'Arkadii Yakovets',
badgeCount: 0,
badges: [],
},
kasya: {
id: 'user-kasya',
avatarUrl: 'https://avatars.githubusercontent.com/u/5873153?v=4',
login: 'kasya',
name: 'Kate Golovanova',
badgeCount: 0,
badges: [],
},
mamicidal: {
id: 'user-mamicidal',
avatarUrl: 'https://avatars.githubusercontent.com/u/112129498?v=4',
login: 'mamicidal',
name: 'Starr Brown',
badgeCount: 0,
badges: [],
},
},
}