Skip to content

Commit 5644467

Browse files
Extend Nest API with Snapshot data
1 parent f203c7c commit 5644467

File tree

5 files changed

+406
-0
lines changed

5 files changed

+406
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from apps.api.rest.v0.project import router as project_router
1717
from apps.api.rest.v0.release import router as release_router
1818
from apps.api.rest.v0.repository import router as repository_router
19+
from apps.api.rest.v0.snapshot import router as snapshot_router
1920
from apps.api.rest.v0.sponsor import router as sponsor_router
2021

2122
ROUTERS = {
@@ -29,6 +30,7 @@
2930
"/projects": project_router,
3031
"/releases": release_router,
3132
"/repositories": repository_router,
33+
"/snapshots": snapshot_router,
3234
"/sponsors": sponsor_router,
3335
}
3436

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
"""Snapshot 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 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.api.rest.v0.chapter import Chapter
15+
from apps.api.rest.v0.issue import Issue
16+
from apps.api.rest.v0.member import Member
17+
from apps.api.rest.v0.project import Project
18+
from apps.api.rest.v0.release import Release
19+
from apps.github.models.issue import Issue as IssueModel
20+
from apps.github.models.release import Release as ReleaseModel
21+
from apps.github.models.user import User as UserModel
22+
from apps.owasp.models.chapter import Chapter as ChapterModel
23+
from apps.owasp.models.project import Project as ProjectModel
24+
from apps.owasp.models.snapshot import Snapshot as SnapshotModel
25+
26+
router = RouterPaginated(tags=["Snapshots"])
27+
28+
29+
class SnapshotBase(Schema):
30+
"""Base schema for Snapshot (used in list endpoints)."""
31+
32+
created_at: datetime
33+
end_at: datetime
34+
key: str
35+
start_at: datetime
36+
title: str
37+
updated_at: datetime
38+
39+
40+
class Snapshot(SnapshotBase):
41+
"""Schema for Snapshot (minimal fields for list display)."""
42+
43+
44+
class SnapshotDetail(SnapshotBase):
45+
"""Detail schema for Snapshot (used in single item endpoints)."""
46+
47+
new_chapters_count: int
48+
new_issues_count: int
49+
new_projects_count: int
50+
new_releases_count: int
51+
new_users_count: int
52+
53+
54+
class SnapshotError(Schema):
55+
"""Snapshot error schema."""
56+
57+
message: str
58+
59+
60+
@router.get(
61+
"/",
62+
description="Retrieve a paginated list of OWASP snapshots.",
63+
operation_id="list_snapshots",
64+
response=list[Snapshot],
65+
summary="List snapshots",
66+
)
67+
@decorate_view(cache_response())
68+
def list_snapshots(
69+
request: HttpRequest,
70+
ordering: Literal[
71+
"created_at", "-created_at", "updated_at", "-updated_at", "start_at", "-start_at"
72+
]
73+
| None = Query(
74+
None,
75+
description="Ordering field",
76+
),
77+
) -> list[Snapshot]:
78+
"""Get all snapshots."""
79+
return SnapshotModel.objects.filter(status=SnapshotModel.Status.COMPLETED).order_by(
80+
ordering or "-created_at"
81+
)
82+
83+
84+
@router.get(
85+
"/{str:snapshot_key}",
86+
description="Retrieve snapshot details.",
87+
operation_id="get_snapshot",
88+
response={
89+
HTTPStatus.NOT_FOUND: SnapshotError,
90+
HTTPStatus.OK: SnapshotDetail,
91+
},
92+
summary="Get snapshot",
93+
)
94+
@decorate_view(cache_response())
95+
def get_snapshot(
96+
request: HttpRequest,
97+
snapshot_key: str = Path(example="2025-02"),
98+
) -> SnapshotDetail | SnapshotError:
99+
"""Get snapshot."""
100+
if snapshot := SnapshotModel.objects.filter(
101+
key__iexact=snapshot_key, status=SnapshotModel.Status.COMPLETED
102+
).first():
103+
return snapshot
104+
105+
return Response({"message": "Snapshot not found"}, status=HTTPStatus.NOT_FOUND)
106+
107+
108+
@router.get(
109+
"/{str:snapshot_key}/chapters/",
110+
description="Retrieve a paginated list of new chapters in a snapshot.",
111+
operation_id="list_snapshot_chapters",
112+
response=list[Chapter],
113+
summary="List new chapters in snapshot",
114+
)
115+
@decorate_view(cache_response())
116+
def list_snapshot_chapters(
117+
request: HttpRequest,
118+
snapshot_key: str = Path(example="2025-02"),
119+
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(
120+
None,
121+
description="Ordering field",
122+
),
123+
) -> list[Chapter]:
124+
"""Get new chapters in snapshot."""
125+
if snapshot := SnapshotModel.objects.filter(
126+
key__iexact=snapshot_key, status=SnapshotModel.Status.COMPLETED
127+
).first():
128+
return snapshot.new_chapters.order_by(ordering or "-created_at")
129+
130+
return ChapterModel.objects.none()
131+
132+
133+
@router.get(
134+
"/{str:snapshot_key}/issues/",
135+
description="Retrieve a paginated list of new issues in a snapshot.",
136+
operation_id="list_snapshot_issues",
137+
response=list[Issue],
138+
summary="List new issues in snapshot",
139+
)
140+
@decorate_view(cache_response())
141+
def list_snapshot_issues(
142+
request: HttpRequest,
143+
snapshot_key: str = Path(example="2025-02"),
144+
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(
145+
None,
146+
description="Ordering field",
147+
),
148+
) -> list[Issue]:
149+
"""Get new issues in snapshot."""
150+
if snapshot := SnapshotModel.objects.filter(
151+
key__iexact=snapshot_key, status=SnapshotModel.Status.COMPLETED
152+
).first():
153+
return snapshot.new_issues.order_by(ordering or "-created_at")
154+
return IssueModel.objects.none()
155+
156+
157+
@router.get(
158+
"/{str:snapshot_key}/projects/",
159+
description="Retrieve a paginated list of new projects in a snapshot.",
160+
operation_id="list_snapshot_projects",
161+
response=list[Project],
162+
summary="List new projects in snapshot",
163+
)
164+
@decorate_view(cache_response())
165+
def list_snapshot_projects(
166+
request: HttpRequest,
167+
snapshot_key: str = Path(example="2025-02"),
168+
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(
169+
None,
170+
description="Ordering field",
171+
),
172+
) -> list[Project]:
173+
"""Get new projects in snapshot."""
174+
if snapshot := SnapshotModel.objects.filter(
175+
key__iexact=snapshot_key, status=SnapshotModel.Status.COMPLETED
176+
).first():
177+
return snapshot.new_projects.order_by(ordering or "-created_at")
178+
return ProjectModel.objects.none()
179+
180+
181+
@router.get(
182+
"/{str:snapshot_key}/releases/",
183+
description="Retrieve a paginated list of new releases in a snapshot.",
184+
operation_id="list_snapshot_releases",
185+
response=list[Release],
186+
summary="List new releases in snapshot",
187+
)
188+
@decorate_view(cache_response())
189+
def list_snapshot_releases(
190+
request: HttpRequest,
191+
snapshot_key: str = Path(example="2025-02"),
192+
ordering: Literal["created_at", "-created_at", "published_at", "-published_at"] | None = Query(
193+
None,
194+
description="Ordering field",
195+
),
196+
) -> list[Release]:
197+
"""Get new releases in snapshot."""
198+
if snapshot := SnapshotModel.objects.filter(
199+
key__iexact=snapshot_key, status=SnapshotModel.Status.COMPLETED
200+
).first():
201+
return snapshot.new_releases.order_by(ordering or "-created_at")
202+
return ReleaseModel.objects.none()
203+
204+
205+
@router.get(
206+
"/{str:snapshot_key}/users/",
207+
description="Retrieve a paginated list of new users in a snapshot.",
208+
operation_id="list_snapshot_users",
209+
response=list[Member],
210+
summary="List new users in snapshot",
211+
)
212+
@decorate_view(cache_response())
213+
def list_snapshot_users(
214+
request: HttpRequest,
215+
snapshot_key: str = Path(example="2025-02"),
216+
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(
217+
None,
218+
description="Ordering field",
219+
),
220+
) -> list[Member]:
221+
"""Get new users in snapshot."""
222+
if snapshot := SnapshotModel.objects.filter(
223+
key__iexact=snapshot_key, status=SnapshotModel.Status.COMPLETED
224+
).first():
225+
return snapshot.new_users.order_by(ordering or "-created_at")
226+
return UserModel.objects.none()

backend/apps/owasp/models/snapshot.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,28 @@ def save(self, *args, **kwargs) -> None:
4545
self.key = now().strftime("%Y-%m")
4646

4747
super().save(*args, **kwargs)
48+
49+
@property
50+
def new_chapters_count(self) -> int:
51+
"""Return the count of new chapters."""
52+
return self.new_chapters.count()
53+
54+
@property
55+
def new_issues_count(self) -> int:
56+
"""Return the count of new issues."""
57+
return self.new_issues.count()
58+
59+
@property
60+
def new_projects_count(self) -> int:
61+
"""Return the count of new projects."""
62+
return self.new_projects.count()
63+
64+
@property
65+
def new_releases_count(self) -> int:
66+
"""Return the count of new releases."""
67+
return self.new_releases.count()
68+
69+
@property
70+
def new_users_count(self) -> int:
71+
"""Return the count of new users."""
72+
return self.new_users.count()

0 commit comments

Comments
 (0)