Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/apps/api/rest/v0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from django.conf import settings
from ninja import NinjaAPI, Swagger
from ninja.pagination import RouterPaginated
from ninja.throttling import AuthRateThrottle

from apps.api.rest.auth.api_key import ApiKey as ApiKey
from apps.api.rest.v0.chapter import router as chapter_router
from apps.api.rest.v0.committee import router as committee_router
from apps.api.rest.v0.event import router as event_router
from apps.api.rest.v0.issue import router as issue_router
Expand All @@ -15,8 +17,6 @@
from apps.api.rest.v0.repository import router as repository_router
from apps.api.rest.v0.sponsor import router as sponsor_router

from .chapter import router as chapter_router

ROUTERS = {
# Chapters.
"/chapters": chapter_router,
Expand All @@ -42,11 +42,12 @@

api_settings = {
"auth": ApiKey(), # The `api_key` param name is based on the ApiKey class name.
"default_router": RouterPaginated(),
"description": "Open Worldwide Application Security Project API",
"docs": Swagger(settings={"persistAuthorization": True}),
"throttle": [AuthRateThrottle("10/s")],
"title": "OWASP Nest",
"version": "0.2.3",
"version": "0.2.4",
}

api_settings_customization = {}
Expand Down
65 changes: 38 additions & 27 deletions backend/apps/api/rest/v0/chapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,78 +7,89 @@
from django.conf import settings
from django.http import HttpRequest
from django.views.decorators.cache import cache_page
from ninja import Field, FilterSchema, Path, Query, Router, Schema
from ninja import Field, FilterSchema, Path, Query, Schema
from ninja.decorators import decorate_view
from ninja.pagination import PageNumberPagination, paginate
from ninja.pagination import RouterPaginated
from ninja.responses import Response

from apps.owasp.models.chapter import Chapter
from apps.owasp.models.chapter import Chapter as ChapterModel

router = Router()
router = RouterPaginated(tags=["Chapters"])


class ChapterErrorResponse(Schema):
"""Chapter error response schema."""
class ChapterBase(Schema):
"""Base schema for Chapter (used in list endpoints)."""

message: str
created_at: datetime
key: str
name: str
updated_at: datetime

@staticmethod
def resolve_key(obj):
"""Resolve key."""
return obj.nest_key

class ChapterFilterSchema(FilterSchema):
"""Filter schema for Chapter."""

country: str | None = Field(None, description="Country of the chapter")
region: str | None = Field(None, description="Region of the chapter")
class Chapter(ChapterBase):
"""Schema for Chapter (minimal fields for list display)."""


class ChapterSchema(Schema):
"""Schema for Chapter."""
class ChapterDetail(ChapterBase):
"""Detail schema for Chapter (used in single item endpoints)."""

country: str
created_at: datetime
name: str
region: str
updated_at: datetime


class ChapterError(Schema):
"""Chapter error schema."""

message: str


class ChapterFilter(FilterSchema):
"""Filter for Chapter."""

country: str | None = Field(None, description="Country of the chapter")


@router.get(
"/",
description="Retrieve a paginated list of OWASP chapters.",
operation_id="list_chapters",
response={200: list[ChapterSchema]},
response=list[Chapter],
summary="List chapters",
tags=["Chapters"],
)
@decorate_view(cache_page(settings.API_CACHE_TIME_SECONDS))
@paginate(PageNumberPagination, page_size=settings.API_PAGE_SIZE)
def list_chapters(
request: HttpRequest,
filters: ChapterFilterSchema = Query(...),
filters: ChapterFilter = Query(...),
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(
None,
description="Ordering field",
),
) -> list[ChapterSchema]:
) -> list[Chapter]:
"""Get chapters."""
return filters.filter(Chapter.active_chapters.order_by(ordering or "-created_at"))
return filters.filter(ChapterModel.active_chapters.order_by(ordering or "-created_at"))


@router.get(
"/{str:chapter_id}",
description="Retrieve chapter details.",
operation_id="get_chapter",
response={
HTTPStatus.NOT_FOUND: ChapterErrorResponse,
HTTPStatus.OK: ChapterSchema,
HTTPStatus.NOT_FOUND: ChapterError,
HTTPStatus.OK: ChapterDetail,
},
summary="Get chapter",
tags=["Chapters"],
)
def get_chapter(
request: HttpRequest,
chapter_id: str = Path(example="London"),
) -> ChapterSchema | ChapterErrorResponse:
) -> ChapterDetail | ChapterError:
"""Get chapter."""
if chapter := Chapter.active_chapters.filter(
if chapter := ChapterModel.active_chapters.filter(
key__iexact=(
chapter_id if chapter_id.startswith("www-chapter-") else f"www-chapter-{chapter_id}"
)
Expand Down
56 changes: 34 additions & 22 deletions backend/apps/api/rest/v0/committee.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,81 @@
from django.conf import settings
from django.http import HttpRequest
from django.views.decorators.cache import cache_page
from ninja import Path, Query, Router, Schema
from ninja import Path, Query, Schema
from ninja.decorators import decorate_view
from ninja.pagination import PageNumberPagination, paginate
from ninja.pagination import RouterPaginated
from ninja.responses import Response

from apps.owasp.models.committee import Committee
from apps.owasp.models.committee import Committee as CommitteeModel

router = Router()
router = RouterPaginated(tags=["Committees"])


class CommitteeErrorResponse(Schema):
"""Committee error response schema."""
class CommitteeBase(Schema):
"""Base schema for Committee (used in list endpoints)."""

message: str
created_at: datetime
key: str
name: str
updated_at: datetime

@staticmethod
def resolve_key(obj):
"""Resolve key."""
return obj.nest_key

class CommitteeSchema(Schema):
"""Schema for Committee."""

name: str
class Committee(CommitteeBase):
"""Schema for Committee (minimal fields for list display)."""


class CommitteeDetail(CommitteeBase):
"""Detail schema for Committee (used in single item endpoints)."""

description: str
created_at: datetime
updated_at: datetime


class CommitteeError(Schema):
"""Committee error schema."""

message: str


@router.get(
"/",
description="Retrieve a paginated list of OWASP committees.",
operation_id="list_committees",
response={200: list[CommitteeSchema]},
response=list[Committee],
summary="List committees",
tags=["Committees"],
)
@decorate_view(cache_page(settings.API_CACHE_TIME_SECONDS))
@paginate(PageNumberPagination, page_size=settings.API_PAGE_SIZE)
def list_committees(
request: HttpRequest,
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(
None,
description="Ordering field",
),
) -> list[CommitteeSchema]:
) -> list[Committee]:
"""Get committees."""
return Committee.active_committees.order_by(ordering or "-created_at")
return CommitteeModel.active_committees.order_by(ordering or "-created_at")


@router.get(
"/{str:committee_id}",
description="Retrieve committee details.",
operation_id="get_committee",
response={
HTTPStatus.NOT_FOUND: CommitteeErrorResponse,
HTTPStatus.OK: CommitteeSchema,
HTTPStatus.NOT_FOUND: CommitteeError,
HTTPStatus.OK: CommitteeDetail,
},
summary="Get committee",
tags=["Committees"],
)
def get_chapter(
request: HttpRequest,
committee_id: str = Path(example="project"),
) -> CommitteeSchema | CommitteeErrorResponse:
) -> CommitteeDetail | CommitteeError:
"""Get chapter."""
if committee := Committee.active_committees.filter(
if committee := CommitteeModel.active_committees.filter(
is_active=True,
key__iexact=(
committee_id
Expand Down
65 changes: 51 additions & 14 deletions backend/apps/api/rest/v0/event.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,83 @@
"""Event API."""

from datetime import datetime
from http import HTTPStatus
from typing import Literal

from django.conf import settings
from django.http import HttpRequest
from django.views.decorators.cache import cache_page
from ninja import Query, Router, Schema
from ninja import Path, Query, Schema
from ninja.decorators import decorate_view
from ninja.pagination import PageNumberPagination, paginate
from ninja.pagination import RouterPaginated
from ninja.responses import Response

from apps.owasp.models.event import Event
from apps.owasp.models.event import Event as EventModel

router = Router()
router = RouterPaginated(tags=["Events"])


class EventSchema(Schema):
"""Schema for Event."""
class EventBase(Schema):
"""Base schema for Event (used in list endpoints)."""

description: str
end_date: datetime | None = None
key: str
name: str
end_date: datetime
start_date: datetime
url: str
url: str | None = None


class Event(EventBase):
"""Schema for Event (minimal fields for list display)."""


class EventDetail(EventBase):
"""Detail schema for Event (used in single item endpoints)."""

description: str | None = None


class EventError(Schema):
"""Event error schema."""

message: str


@router.get(
"/",
description="Retrieve a paginated list of OWASP events.",
operation_id="list_events",
summary="List events",
tags=["Events"],
response={200: list[EventSchema]},
response=list[Event],
)
@decorate_view(cache_page(settings.API_CACHE_TIME_SECONDS))
@paginate(PageNumberPagination, page_size=settings.API_PAGE_SIZE)
def list_events(
request: HttpRequest,
ordering: Literal["start_date", "-start_date", "end_date", "-end_date"] | None = Query(
None,
description="Ordering field",
),
) -> list[EventSchema]:
) -> list[Event]:
"""Get all events."""
return Event.objects.order_by(ordering or "-start_date")
return EventModel.objects.order_by(ordering or "-start_date", "-end_date")


@router.get(
"/{str:event_id}",
description="Retrieve an event details.",
operation_id="get_event",
response={
HTTPStatus.NOT_FOUND: EventError,
HTTPStatus.OK: EventDetail,
},
summary="Get event",
)
def get_event(
request: HttpRequest,
event_id: str = Path(..., example="owasp-global-appsec-usa-2025-washington-dc"),
) -> EventDetail | EventError:
"""Get event."""
if event := EventModel.objects.filter(key__iexact=event_id).first():
return event

return Response({"message": "Event not found"}, status=HTTPStatus.NOT_FOUND)
Loading