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
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:
"""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 (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

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

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

from apps.common.utils import normalize_limit
from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.models.issue import Issue

Expand Down Expand Up @@ -61,4 +62,7 @@ def recent_issues(
.order_by("-created_at")
)

return queryset[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else []
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return queryset[:normalized_limit]
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]
6 changes: 5 additions & 1 deletion backend/apps/github/api/internal/queries/pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.db.models import F, Window
from django.db.models.functions import Rank

from apps.common.utils import normalize_limit
from apps.github.api.internal.nodes.pull_request import PullRequestNode
from apps.github.models.pull_request import PullRequest
from apps.owasp.models.project import Project
Expand Down Expand Up @@ -82,4 +83,7 @@ def recent_pull_requests(
.order_by("-created_at")
)

return queryset[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else []
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return queryset[:normalized_limit]
6 changes: 5 additions & 1 deletion backend/apps/github/api/internal/queries/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.db.models import F, Window
from django.db.models.functions import Rank

from apps.common.utils import normalize_limit
from apps.github.api.internal.nodes.release import ReleaseNode
from apps.github.models.release import Release

Expand Down Expand Up @@ -65,4 +66,7 @@ def recent_releases(
.order_by("-published_at")
)

return queryset[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else []
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return queryset[:normalized_limit]
16 changes: 7 additions & 9 deletions backend/apps/github/api/internal/queries/repository.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.github.api.internal.nodes.repository import RepositoryNode
from apps.github.models.repository import Repository

Expand Down Expand Up @@ -54,12 +55,9 @@ def repositories(
list[RepositoryNode]: A list of repositories.

"""
return (
(
Repository.objects.filter(
organization__login__iexact=organization,
).order_by("-stars_count")[:limit]
)
if (limit := min(limit, MAX_LIMIT)) > 0
else []
)
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

return Repository.objects.filter(
organization__login__iexact=organization,
).order_by("-stars_count")[:normalized_limit]
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.github.api.internal.nodes.repository_contributor import RepositoryContributorNode
from apps.github.models.repository_contributor import RepositoryContributor

Expand Down Expand Up @@ -42,15 +43,15 @@ def top_contributors(
list: List of top contributors with their details.

"""
if (limit := min(limit, MAX_LIMIT)) <= 0:
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

top_contributors = RepositoryContributor.get_top_contributors(
chapter=chapter,
committee=committee,
excluded_usernames=excluded_usernames,
has_full_name=has_full_name,
limit=limit,
limit=normalized_limit,
organization=organization,
project=project,
repository=repository,
Expand Down
13 changes: 11 additions & 2 deletions backend/apps/mentorship/api/internal/nodes/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import strawberry

from apps.common.utils import normalize_limit
from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.api.internal.nodes.pull_request import PullRequestNode
from apps.github.api.internal.nodes.user import UserNode
Expand All @@ -16,6 +17,8 @@
from apps.mentorship.models.issue_user_interest import IssueUserInterest
from apps.mentorship.models.task import Task

MAX_LIMIT = 1000


@strawberry.type
class ModuleNode:
Expand Down Expand Up @@ -79,14 +82,17 @@ def issues(
self, limit: int = 20, offset: int = 0, label: str | None = None
) -> list[IssueNode]:
"""Return paginated issues linked to this module, optionally filtered by label."""
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

queryset = self.issues.select_related("repository", "author").prefetch_related(
"assignees", "labels"
)

if label and label != "all":
queryset = queryset.filter(labels__name=label)

return list(queryset.order_by("-updated_at")[offset : offset + limit])
return list(queryset.order_by("-updated_at")[offset : offset + normalized_limit])

@strawberry.field
def issues_count(self, label: str | None = None) -> int:
Expand Down Expand Up @@ -163,12 +169,15 @@ def task_assigned_at(self, issue_number: int) -> datetime | None:
@strawberry.field
def recent_pull_requests(self, limit: int = 5) -> list[PullRequestNode]:
"""Return recent pull requests linked to issues in this module."""
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

issue_ids = self.issues.values_list("id", flat=True)
return list(
PullRequest.objects.filter(related_issues__id__in=issue_ids)
.select_related("author")
.distinct()
.order_by("-created_at")[:limit]
.order_by("-created_at")[:normalized_limit]
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import strawberry
from django.db.models import Prefetch

from apps.common.utils import normalize_limit
from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.models import Label
from apps.github.models.user import User as GithubUser
Expand All @@ -21,6 +22,7 @@
from apps.github.api.internal.nodes.issue import IssueNode

logger = logging.getLogger(__name__)
MAX_LIMIT = 1000


@strawberry.type
Expand Down Expand Up @@ -97,6 +99,9 @@ def get_mentee_module_issues(
offset: int = 0,
) -> list[IssueNode]:
"""Get issues assigned to a mentee in a specific module."""
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

try:
module = Module.objects.only("id").get(key=module_key, program__key=program_key)

Expand All @@ -122,7 +127,7 @@ def get_mentee_module_issues(
)
.order_by("-created_at")
)
issues = issues_qs[offset : offset + limit]
issues = issues_qs[offset : offset + normalized_limit]

return list(issues)

Expand Down
13 changes: 10 additions & 3 deletions backend/apps/mentorship/api/internal/queries/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import strawberry
from django.db.models import Q

from apps.common.utils import normalize_limit
from apps.mentorship.api.internal.nodes.program import PaginatedPrograms, ProgramNode
from apps.mentorship.models import Program
from apps.mentorship.models.mentor import Mentor
from apps.nest.api.internal.permissions import IsAuthenticated

PAGE_SIZE = 25
MAX_LIMIT = 1000
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -47,6 +49,9 @@ def my_programs(
logger.warning("Mentor for user '%s' not found.", user.username)
return PaginatedPrograms(programs=[], total_pages=0, current_page=page)

if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
normalized_limit = PAGE_SIZE

queryset = (
Program.objects.prefetch_related(
"admins__github_user", "modules__mentors__github_user"
Expand All @@ -59,11 +64,13 @@ def my_programs(
queryset = queryset.filter(name__icontains=search)

total_count = queryset.count()
total_pages = max(1, (total_count + limit - 1) // limit)
total_pages = max(1, (total_count + normalized_limit - 1) // normalized_limit)
page = max(1, min(page, total_pages))
offset = (page - 1) * limit
offset = (page - 1) * normalized_limit

paginated_programs = queryset.order_by("-nest_created_at")[offset : offset + limit]
paginated_programs = queryset.order_by("-nest_created_at")[
offset : offset + normalized_limit
]

results = []
mentor_id = mentor.id
Expand Down
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/board_of_directors.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.owasp.api.internal.nodes.board_of_directors import BoardOfDirectorsNode
from apps.owasp.models.board_of_directors import BoardOfDirectors

Expand Down Expand Up @@ -40,8 +41,7 @@ def boards_of_directors(self, limit: int = 10) -> list[BoardOfDirectorsNode]:
List of BoardOfDirectorsNode objects.

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

return BoardOfDirectors.objects.order_by("-year")[:normalized_limit]
Loading