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
101 changes: 99 additions & 2 deletions docs/api_controller/model_controller/02_model_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,54 @@ class EventModelController(ModelControllerBase):

## **Route Configuration**

You can customize individual route behavior using route info dictionaries:
You can customize individual route behavior using route info dictionaries. Each route type (`create_route_info`, `list_route_info`, `find_one_route_info`, `update_route_info`, `patch_route_info`, `delete_route_info`) accepts various configuration parameters.

### **Common Route Parameters**

All route types support these common parameters:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `path` | `str` | varies by route | Custom path for the route |
| `status_code` | `int` | varies by route | HTTP status code for successful responses |
| `auth` | `Any` | NOT_SET | Authentication class or instance |
| `throttle` | `BaseThrottle \| List[BaseThrottle]` | NOT_SET | Throttle class(es) for rate limiting |
| `response` | `Any` | NOT_SET | Custom response configuration |
| `url_name` | `str \| None` | None | Django URL name for the route |
| `description` | `str \| None` | None | Detailed description for OpenAPI docs |
| `operation_id` | `str \| None` | None | Custom operation ID for OpenAPI |
| `summary` | `str \| None` | varies by route | Short summary for OpenAPI docs |
| `tags` | `List[str] \| None` | None | Tags for grouping in OpenAPI docs |
| `deprecated` | `bool \| None` | None | Mark route as deprecated in OpenAPI |
| `by_alias` | `bool` | False | Use schema field aliases in response |
| `exclude_unset` | `bool` | False | Exclude unset fields from response |
| `exclude_defaults` | `bool` | False | Exclude fields with default values |
| `exclude_none` | `bool` | False | Exclude None fields from response |
| `include_in_schema` | `bool` | True | Include route in OpenAPI schema |
| `permissions` | `List[BasePermission]` | None | Permission classes for the route |
| `openapi_extra` | `Dict[str, Any] \| None` | None | Extra OpenAPI schema properties |

### **Route-Specific Parameters**

#### **Create Route (`create_route_info`)**
- `custom_handler`: Custom handler function to override default create logic

#### **Update/Patch Routes (`update_route_info`, `patch_route_info`)**
- `object_getter`: Custom function to retrieve the object
- `custom_handler`: Custom handler function to override default update/patch logic

#### **Find One Route (`find_one_route_info`)**
- `object_getter`: Custom function to retrieve the object

#### **Delete Route (`delete_route_info`)**
- `object_getter`: Custom function to retrieve the object
- `custom_handler`: Custom handler function to override default delete logic

#### **List Route (`list_route_info`)**
- `queryset_getter`: Custom function to retrieve the queryset
- `pagination_response_schema`: Custom pagination response schema

### **Basic Example**

```python
@api_controller("/events")
Expand All @@ -148,7 +195,6 @@ class EventModelController(ModelControllerBase):
"summary": "List all events",
"description": "Retrieves a paginated list of all events",
"tags": ["events"],
"schema_out": CustomListSchema,
},
find_one_route_info={
"summary": "Get event details",
Expand All @@ -158,6 +204,57 @@ class EventModelController(ModelControllerBase):
)
```

### **Advanced Configuration Example**

```python
from ninja_extra import status
from ninja_extra.permissions import IsAuthenticated, IsAdminUser
from ninja_extra.throttling import AnonRateThrottle

@api_controller("/events")
class EventModelController(ModelControllerBase):
model_config = ModelConfig(
model=Event,
create_route_info={
"summary": "Create a new event",
"description": "Creates a new event with the provided data",
"tags": ["events", "management"],
"status_code": status.HTTP_201_CREATED,
"permissions": [IsAuthenticated],
"throttle": AnonRateThrottle(),
"exclude_none": True,
"openapi_extra": {
"requestBody": {
"content": {
"application/json": {
"examples": {
"example1": {
"summary": "Conference event",
"value": {
"title": "Tech Conference 2024",
"start_date": "2024-06-01",
"end_date": "2024-06-03"
}
}
}
}
}
}
}
},
update_route_info={
"summary": "Update event",
"permissions": [IsAuthenticated, IsAdminUser],
"exclude_unset": True,
},
delete_route_info={
"summary": "Delete an event",
"permissions": [IsAdminUser],
"status_code": status.HTTP_204_NO_CONTENT,
}
)
```

## **Async Routes Configuration**

Enable async routes and configure async behavior:
Expand Down
60 changes: 53 additions & 7 deletions ninja_extra/controllers/model/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

from django.db.models import Model as DjangoModel
from django.db.models import QuerySet
from ninja.constants import NOT_SET, NOT_SET_TYPE
from ninja.pagination import PaginationBase
from ninja.params import Body
from ninja.signature import is_async
from ninja.throttling import BaseThrottle
from pydantic import BaseModel as PydanticModel

from ninja_extra import status
Expand Down Expand Up @@ -95,6 +97,9 @@ def create(
schema_out: t.Type[PydanticModel],
path: str = "/",
status_code: int = status.HTTP_201_CREATED,
auth: t.Any = NOT_SET,
throttle: t.Union[BaseThrottle, t.List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
response: t.Any = NOT_SET,
url_name: t.Optional[str] = None,
custom_handler: t.Optional[t.Callable[..., t.Any]] = None,
description: t.Optional[str] = None,
Expand Down Expand Up @@ -129,7 +134,9 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:

return route.post(
working_path,
response={status_code: schema_out},
response=response
if response is not NOT_SET
else {status_code: schema_out},
url_name=url_name,
description=description,
operation_id=operation_id,
Expand All @@ -143,6 +150,8 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
include_in_schema=include_in_schema,
permissions=permissions,
openapi_extra=openapi_extra,
auth=auth,
throttle=throttle,
)(create_item)

return ModelEndpointFunction(view_fun_setup=_setup)
Expand All @@ -155,6 +164,9 @@ def update(
schema_in: t.Type[PydanticModel],
schema_out: t.Type[PydanticModel],
status_code: int = status.HTTP_200_OK,
auth: t.Any = NOT_SET,
throttle: t.Union[BaseThrottle, t.List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
response: t.Any = NOT_SET,
url_name: t.Optional[str] = None,
description: t.Optional[str] = None,
object_getter: t.Optional[t.Callable[..., DjangoModel]] = None,
Expand Down Expand Up @@ -192,7 +204,9 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
)
return route.put(
working_path,
response={status_code: schema_out},
response=response
if response is not NOT_SET
else {status_code: schema_out},
url_name=url_name,
description=description,
operation_id=operation_id,
Expand All @@ -206,6 +220,8 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
include_in_schema=include_in_schema,
permissions=permissions,
openapi_extra=openapi_extra,
auth=auth,
throttle=throttle,
)(update_item)

return ModelEndpointFunction(_setup)
Expand All @@ -218,6 +234,9 @@ def patch(
schema_in: t.Type[PydanticModel],
schema_out: t.Type[PydanticModel],
status_code: int = status.HTTP_200_OK,
auth: t.Any = NOT_SET,
throttle: t.Union[BaseThrottle, t.List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
response: t.Any = NOT_SET,
url_name: t.Optional[str] = None,
description: t.Optional[str] = None,
object_getter: t.Optional[t.Callable[..., DjangoModel]] = None,
Expand Down Expand Up @@ -255,7 +274,9 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
)
return route.patch(
working_path,
response={status_code: schema_out},
response=response
if response is not NOT_SET
else {status_code: schema_out},
url_name=url_name,
description=description,
operation_id=operation_id,
Expand All @@ -269,6 +290,8 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
include_in_schema=include_in_schema,
permissions=permissions,
openapi_extra=openapi_extra,
auth=auth,
throttle=throttle,
)(patch_item)

return ModelEndpointFunction(_setup)
Expand All @@ -280,6 +303,9 @@ def find_one(
lookup_param: str,
schema_out: t.Type[PydanticModel],
status_code: int = status.HTTP_200_OK,
auth: t.Any = NOT_SET,
throttle: t.Union[BaseThrottle, t.List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
response: t.Any = NOT_SET,
url_name: t.Optional[str] = None,
description: t.Optional[str] = None,
object_getter: t.Optional[t.Callable[..., DjangoModel]] = None,
Expand Down Expand Up @@ -313,7 +339,9 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
)
return route.get(
working_path,
response={status_code: schema_out},
response=response
if response is not NOT_SET
else {status_code: schema_out},
url_name=url_name,
description=description,
operation_id=operation_id,
Expand All @@ -327,6 +355,8 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
include_in_schema=include_in_schema,
permissions=permissions,
openapi_extra=openapi_extra,
auth=auth,
throttle=throttle,
)(get_item)

return ModelEndpointFunction(_setup)
Expand All @@ -337,6 +367,9 @@ def list(
schema_out: t.Type[PydanticModel],
path: str = "/",
status_code: int = status.HTTP_200_OK,
auth: t.Any = NOT_SET,
throttle: t.Union[BaseThrottle, t.List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
response: t.Any = NOT_SET,
url_name: t.Optional[str] = None,
description: t.Optional[str] = None,
operation_id: t.Optional[str] = None,
Expand Down Expand Up @@ -378,7 +411,9 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
list_items = paginate(pagination_class, **paginate_kwargs)(list_items)
return route.get(
working_path,
response={
response=response
if response is not NOT_SET
else {
status_code: pagination_response_schema[schema_out] # type:ignore[index]
},
url_name=url_name,
Expand All @@ -394,11 +429,15 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
include_in_schema=include_in_schema,
permissions=permissions,
openapi_extra=openapi_extra,
auth=auth,
throttle=throttle,
)(list_items)

return route.get(
working_path,
response={status_code: t.List[schema_out]}, # type:ignore[valid-type]
response=response
if response is not NOT_SET
else {status_code: t.List[schema_out]}, # type:ignore[valid-type]
url_name=url_name,
description=description,
operation_id=operation_id,
Expand All @@ -412,6 +451,8 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
include_in_schema=include_in_schema,
permissions=permissions,
openapi_extra=openapi_extra,
auth=auth,
throttle=throttle,
)(list_items)

return ModelEndpointFunction(_setup)
Expand All @@ -422,6 +463,9 @@ def delete(
path: str,
lookup_param: str,
status_code: int = status.HTTP_204_NO_CONTENT,
auth: t.Any = NOT_SET,
throttle: t.Union[BaseThrottle, t.List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
response: t.Any = NOT_SET,
url_name: t.Optional[str] = None,
description: t.Optional[str] = None,
object_getter: t.Optional[t.Callable[..., DjangoModel]] = None,
Expand Down Expand Up @@ -460,7 +504,7 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
return route.delete(
working_path,
url_name=url_name,
response={status_code: None},
response=response if response is not NOT_SET else {status_code: None},
description=description,
operation_id=operation_id,
summary=summary,
Expand All @@ -473,6 +517,8 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
include_in_schema=include_in_schema,
permissions=permissions,
openapi_extra=openapi_extra,
auth=auth,
throttle=throttle,
)(delete_item)

return ModelEndpointFunction(_setup)
Expand Down
2 changes: 2 additions & 0 deletions ninja_extra/controllers/model/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.core.exceptions import ImproperlyConfigured
from django.db.models import Model
from ninja import FilterSchema
from ninja.pagination import PaginationBase
from pydantic import BaseModel as PydanticModel
from pydantic import Field, field_validator
Expand Down Expand Up @@ -43,6 +44,7 @@ class ModelPagination(PydanticModel):
klass: t.Type[PaginationBase] = PageNumberPaginationExtra
paginator_kwargs: t.Optional[dict] = None
pagination_schema: t.Type[PydanticModel] = PaginatedResponseSchema
filter_schema: t.Optional[t.Type[FilterSchema]] = None

@field_validator("pagination_schema", mode="before")
def validate_schema(cls, value: t.Any) -> t.Any:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ classifiers = [

requires = [
"Django >= 2.2",
"django-ninja == 1.4.5",
"django-ninja == 1.5.0",
"injector >= 0.19.0",
"asgiref",
"contextlib2"
Expand Down