| 
4 | 4 | from http import HTTPStatus  | 
5 | 5 | from typing import Literal  | 
6 | 6 | 
 
  | 
7 |  | -from django.conf import settings  | 
8 | 7 | from django.http import HttpRequest  | 
9 |  | -from django.views.decorators.cache import cache_page  | 
10 |  | -from ninja import Field, FilterSchema, Path, Query, Router, Schema  | 
 | 8 | +from ninja import Field, FilterSchema, Path, Query, Schema  | 
11 | 9 | from ninja.decorators import decorate_view  | 
12 |  | -from ninja.pagination import PageNumberPagination, paginate  | 
 | 10 | +from ninja.pagination import RouterPaginated  | 
13 | 11 | from ninja.responses import Response  | 
14 | 12 | 
 
  | 
15 |  | -from apps.owasp.models.chapter import Chapter  | 
 | 13 | +from apps.api.decorators.cache import cache_response  | 
 | 14 | +from apps.owasp.models.chapter import Chapter as ChapterModel  | 
16 | 15 | 
 
  | 
17 |  | -router = Router()  | 
 | 16 | +router = RouterPaginated(tags=["Chapters"])  | 
18 | 17 | 
 
  | 
19 | 18 | 
 
  | 
20 |  | -class ChapterErrorResponse(Schema):  | 
21 |  | -    """Chapter error response schema."""  | 
 | 19 | +class ChapterBase(Schema):  | 
 | 20 | +    """Base schema for Chapter (used in list endpoints)."""  | 
22 | 21 | 
 
  | 
23 |  | -    message: str  | 
 | 22 | +    created_at: datetime  | 
 | 23 | +    key: str  | 
 | 24 | +    name: str  | 
 | 25 | +    updated_at: datetime  | 
24 | 26 | 
 
  | 
 | 27 | +    @staticmethod  | 
 | 28 | +    def resolve_key(obj):  | 
 | 29 | +        """Resolve key."""  | 
 | 30 | +        return obj.nest_key  | 
25 | 31 | 
 
  | 
26 |  | -class ChapterFilterSchema(FilterSchema):  | 
27 |  | -    """Filter schema for Chapter."""  | 
28 | 32 | 
 
  | 
29 |  | -    country: str | None = Field(None, description="Country of the chapter")  | 
30 |  | -    region: str | None = Field(None, description="Region of the chapter")  | 
 | 33 | +class Chapter(ChapterBase):  | 
 | 34 | +    """Schema for Chapter (minimal fields for list display)."""  | 
31 | 35 | 
 
  | 
32 | 36 | 
 
  | 
33 |  | -class ChapterSchema(Schema):  | 
34 |  | -    """Schema for Chapter."""  | 
 | 37 | +class ChapterDetail(ChapterBase):  | 
 | 38 | +    """Detail schema for Chapter (used in single item endpoints)."""  | 
35 | 39 | 
 
  | 
36 | 40 |     country: str  | 
37 |  | -    created_at: datetime  | 
38 |  | -    name: str  | 
39 | 41 |     region: str  | 
40 |  | -    updated_at: datetime  | 
 | 42 | + | 
 | 43 | + | 
 | 44 | +class ChapterError(Schema):  | 
 | 45 | +    """Chapter error schema."""  | 
 | 46 | + | 
 | 47 | +    message: str  | 
 | 48 | + | 
 | 49 | + | 
 | 50 | +class ChapterFilter(FilterSchema):  | 
 | 51 | +    """Filter for Chapter."""  | 
 | 52 | + | 
 | 53 | +    country: str | None = Field(None, description="Country of the chapter")  | 
41 | 54 | 
 
  | 
42 | 55 | 
 
  | 
43 | 56 | @router.get(  | 
44 | 57 |     "/",  | 
45 | 58 |     description="Retrieve a paginated list of OWASP chapters.",  | 
46 | 59 |     operation_id="list_chapters",  | 
47 |  | -    response={200: list[ChapterSchema]},  | 
 | 60 | +    response=list[Chapter],  | 
48 | 61 |     summary="List chapters",  | 
49 |  | -    tags=["Chapters"],  | 
50 | 62 | )  | 
51 |  | -@decorate_view(cache_page(settings.API_CACHE_TIME_SECONDS))  | 
52 |  | -@paginate(PageNumberPagination, page_size=settings.API_PAGE_SIZE)  | 
 | 63 | +@decorate_view(cache_response())  | 
53 | 64 | def list_chapters(  | 
54 | 65 |     request: HttpRequest,  | 
55 |  | -    filters: ChapterFilterSchema = Query(...),  | 
 | 66 | +    filters: ChapterFilter = Query(...),  | 
56 | 67 |     ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(  | 
57 | 68 |         None,  | 
58 | 69 |         description="Ordering field",  | 
59 | 70 |     ),  | 
60 |  | -) -> list[ChapterSchema]:  | 
 | 71 | +) -> list[Chapter]:  | 
61 | 72 |     """Get chapters."""  | 
62 |  | -    return filters.filter(Chapter.active_chapters.order_by(ordering or "-created_at"))  | 
 | 73 | +    return filters.filter(ChapterModel.active_chapters.order_by(ordering or "-created_at"))  | 
63 | 74 | 
 
  | 
64 | 75 | 
 
  | 
65 | 76 | @router.get(  | 
66 | 77 |     "/{str:chapter_id}",  | 
67 | 78 |     description="Retrieve chapter details.",  | 
68 | 79 |     operation_id="get_chapter",  | 
69 | 80 |     response={  | 
70 |  | -        HTTPStatus.NOT_FOUND: ChapterErrorResponse,  | 
71 |  | -        HTTPStatus.OK: ChapterSchema,  | 
 | 81 | +        HTTPStatus.NOT_FOUND: ChapterError,  | 
 | 82 | +        HTTPStatus.OK: ChapterDetail,  | 
72 | 83 |     },  | 
73 | 84 |     summary="Get chapter",  | 
74 |  | -    tags=["Chapters"],  | 
75 | 85 | )  | 
 | 86 | +@decorate_view(cache_response())  | 
76 | 87 | def get_chapter(  | 
77 | 88 |     request: HttpRequest,  | 
78 | 89 |     chapter_id: str = Path(example="London"),  | 
79 |  | -) -> ChapterSchema | ChapterErrorResponse:  | 
 | 90 | +) -> ChapterDetail | ChapterError:  | 
80 | 91 |     """Get chapter."""  | 
81 |  | -    if chapter := Chapter.active_chapters.filter(  | 
 | 92 | +    if chapter := ChapterModel.active_chapters.filter(  | 
82 | 93 |         key__iexact=(  | 
83 | 94 |             chapter_id if chapter_id.startswith("www-chapter-") else f"www-chapter-{chapter_id}"  | 
84 | 95 |         )  | 
 | 
0 commit comments