Skip to content

Commit 9391d76

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

File tree

3 files changed

+260
-0
lines changed

3 files changed

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

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)