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