Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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