Skip to content

Commit d4f7ddd

Browse files
committed
Merge branch 'feature/nest-zappa-migration' into pr/rudransh-shrivastava/2431
2 parents 9650e5e + 294c3f4 commit d4f7ddd

File tree

24 files changed

+579
-140
lines changed

24 files changed

+579
-140
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ repos:
1818
files: ^infrastructure/.*\.tf$
1919

2020
- repo: https://github.com/astral-sh/ruff-pre-commit
21-
rev: v0.14.1
21+
rev: v0.14.2
2222
hooks:
2323
- id: ruff
2424
args:

backend/apps/api/rest/v0/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from apps.api.rest.v0.event import router as event_router
1212
from apps.api.rest.v0.issue import router as issue_router
1313
from apps.api.rest.v0.member import router as member_router
14+
from apps.api.rest.v0.milestone import router as milestone_router
1415
from apps.api.rest.v0.organization import router as organization_router
1516
from apps.api.rest.v0.project import router as project_router
1617
from apps.api.rest.v0.release import router as release_router
@@ -23,6 +24,7 @@
2324
"/events": event_router,
2425
"/issues": issue_router,
2526
"/members": member_router,
27+
"/milestones": milestone_router,
2628
"/organizations": organization_router,
2729
"/projects": project_router,
2830
"/releases": release_router,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Milestone API."""
2+
3+
from datetime import datetime
4+
from http import HTTPStatus
5+
from typing import Literal
6+
7+
from django.http import HttpRequest
8+
from ninja import Field, FilterSchema, Path, Query, Schema
9+
from ninja.decorators import decorate_view
10+
from ninja.pagination import RouterPaginated
11+
from ninja.responses import Response
12+
13+
from apps.api.decorators.cache import cache_response
14+
from apps.github.models.generic_issue_model import GenericIssueModel
15+
from apps.github.models.milestone import Milestone as MilestoneModel
16+
17+
router = RouterPaginated(tags=["Milestones"])
18+
19+
20+
class MilestoneBase(Schema):
21+
"""Base schema for Milestone (used in list endpoints)."""
22+
23+
created_at: datetime
24+
number: int
25+
state: GenericIssueModel.State
26+
title: str
27+
updated_at: datetime
28+
url: str
29+
30+
31+
class Milestone(MilestoneBase):
32+
"""Schema for Milestone (minimal fields for list display)."""
33+
34+
35+
class MilestoneDetail(MilestoneBase):
36+
"""Detail schema for Milestone (used in single item endpoints)."""
37+
38+
body: str
39+
closed_issues_count: int
40+
due_on: datetime | None
41+
open_issues_count: int
42+
43+
44+
class MilestoneError(Schema):
45+
"""Milestone error schema."""
46+
47+
message: str
48+
49+
50+
class MilestoneFilter(FilterSchema):
51+
"""Filter for Milestone."""
52+
53+
organization: str | None = Field(
54+
None,
55+
description="Organization that milestones belong to (filtered by repository owner)",
56+
example="OWASP",
57+
)
58+
repository: str | None = Field(
59+
None,
60+
description="Repository that milestones belong to",
61+
example="Nest",
62+
)
63+
state: GenericIssueModel.State | None = Field(
64+
None,
65+
description="Milestone state",
66+
)
67+
68+
69+
@router.get(
70+
"/",
71+
description="Retrieve a paginated list of GitHub milestones.",
72+
operation_id="list_milestones",
73+
response=list[Milestone],
74+
summary="List milestones",
75+
)
76+
@decorate_view(cache_response())
77+
def list_milestones(
78+
request: HttpRequest,
79+
filters: MilestoneFilter = Query(...),
80+
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = None,
81+
) -> list[Milestone]:
82+
"""Get all milestones."""
83+
milestones = MilestoneModel.objects.select_related("repository", "repository__organization")
84+
85+
if filters.organization:
86+
milestones = milestones.filter(
87+
repository__organization__login__iexact=filters.organization
88+
)
89+
if filters.repository:
90+
milestones = milestones.filter(repository__name__iexact=filters.repository)
91+
if filters.state:
92+
milestones = milestones.filter(state=filters.state)
93+
94+
return milestones.order_by(ordering or "-created_at", "-updated_at")
95+
96+
97+
@router.get(
98+
"/{str:organization_id}/{str:repository_id}/{int:milestone_id}",
99+
description=(
100+
"Retrieve a specific GitHub milestone by organization, repository, and milestone number."
101+
),
102+
operation_id="get_milestone",
103+
response={
104+
HTTPStatus.NOT_FOUND: MilestoneError,
105+
HTTPStatus.OK: MilestoneDetail,
106+
},
107+
summary="Get milestone",
108+
)
109+
@decorate_view(cache_response())
110+
def get_milestone(
111+
request: HttpRequest,
112+
organization_id: str = Path(example="OWASP"),
113+
repository_id: str = Path(example="Nest"),
114+
milestone_id: int = Path(example=1),
115+
) -> MilestoneDetail | MilestoneError:
116+
"""Get milestone."""
117+
try:
118+
return MilestoneModel.objects.get(
119+
repository__organization__login__iexact=organization_id,
120+
repository__name__iexact=repository_id,
121+
number=milestone_id,
122+
)
123+
except MilestoneModel.DoesNotExist:
124+
return Response({"message": "Milestone not found"}, status=HTTPStatus.NOT_FOUND)

backend/apps/github/admin/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class MemberProfileInline(admin.StackedInline):
1414
verbose_name_plural = "OWASP Member Profile"
1515
fields = (
1616
"owasp_slack_id",
17+
"linkedin_page_id",
1718
"first_contribution_at",
1819
"is_owasp_board_member",
1920
"is_former_owasp_staff",

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ def issues_count(self) -> int:
8888
"""Resolve issues count."""
8989
return self.idx_issues_count
9090

91+
@strawberry.field
92+
def linkedin_page_id(self) -> str:
93+
"""Resolve LinkedIn page ID."""
94+
if hasattr(self, "owasp_profile") and self.owasp_profile.linkedin_page_id:
95+
return self.owasp_profile.linkedin_page_id
96+
return ""
97+
9198
@strawberry.field
9299
def releases_count(self) -> int:
93100
"""Resolve releases count."""

backend/apps/owasp/admin/member_profile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class MemberProfileAdmin(admin.ModelAdmin):
4040
{
4141
"fields": (
4242
"github_user",
43+
"linkedin_page_id",
4344
"owasp_slack_id",
4445
)
4546
},
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 5.2.7 on 2025-10-25 00:24
2+
3+
import django.core.validators
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("owasp", "0064_memberprofile_is_gsoc_mentor_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="memberprofile",
15+
name="linkedin_page_id",
16+
field=models.CharField(
17+
blank=True,
18+
default="",
19+
help_text="LinkedIn username or custom URL ID (e.g., 'john-doe-123')",
20+
max_length=100,
21+
validators=[
22+
django.core.validators.RegexValidator(
23+
code="invalid_linkedin_id",
24+
message="LinkedIn ID must be 3-100 characters and contain only letters, numbers, and hyphens",
25+
regex="^[a-zA-Z0-9\\-]{3,100}$",
26+
)
27+
],
28+
verbose_name="LinkedIn Page ID",
29+
),
30+
),
31+
]

backend/apps/owasp/models/member_profile.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from django.core.validators import RegexValidator
56
from django.db import models
67

78
from apps.common.models import TimestampedModel
@@ -52,6 +53,23 @@ class Meta:
5253
verbose_name="Is GSoC Mentor",
5354
help_text="Whether the member is a Google Summer of Code mentor",
5455
)
56+
linkedin_page_id = models.CharField(
57+
max_length=100,
58+
blank=True,
59+
default="",
60+
validators=[
61+
RegexValidator(
62+
regex=r"^[a-zA-Z0-9\-]{3,100}$",
63+
message=(
64+
"LinkedIn ID must be 3-100 characters and contain only "
65+
"letters, numbers, and hyphens"
66+
),
67+
code="invalid_linkedin_id",
68+
),
69+
],
70+
verbose_name="LinkedIn Page ID",
71+
help_text="LinkedIn username or custom URL ID (e.g., 'john-doe-123')",
72+
)
5573

5674
def __str__(self) -> str:
5775
"""Return human-readable representation."""

backend/apps/owasp/models/post.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import UTC, datetime
44

55
from django.db import models
6+
from django.utils import timezone
67
from django.utils.dateparse import parse_datetime
78

89
from apps.common.models import BulkSaveModel, TimestampedModel
@@ -36,7 +37,11 @@ def bulk_save(posts, fields=None) -> None: # type: ignore[override]
3637
@staticmethod
3738
def recent_posts():
3839
"""Get recent posts."""
39-
return Post.objects.order_by("-published_at")
40+
return Post.objects.filter(
41+
published_at__lte=timezone.now(),
42+
).order_by(
43+
"-published_at",
44+
)
4045

4146
@staticmethod
4247
def update_data(data, *, save: bool = True) -> "Post":

backend/poetry.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)