diff --git a/docs/api_controller/model_controller/02_model_configuration.md b/docs/api_controller/model_controller/02_model_configuration.md index 1fd81b3..d684678 100644 --- a/docs/api_controller/model_controller/02_model_configuration.md +++ b/docs/api_controller/model_controller/02_model_configuration.md @@ -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") @@ -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", @@ -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: diff --git a/ninja_extra/controllers/model/endpoints.py b/ninja_extra/controllers/model/endpoints.py index 74c386c..89e90a3 100644 --- a/ninja_extra/controllers/model/endpoints.py +++ b/ninja_extra/controllers/model/endpoints.py @@ -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 @@ -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, @@ -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, @@ -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) @@ -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, @@ -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, @@ -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) @@ -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, @@ -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, @@ -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) @@ -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, @@ -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, @@ -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) @@ -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, @@ -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, @@ -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, @@ -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) @@ -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, @@ -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, @@ -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) diff --git a/ninja_extra/controllers/model/schemas.py b/ninja_extra/controllers/model/schemas.py index 97ecd3d..07bf9b2 100644 --- a/ninja_extra/controllers/model/schemas.py +++ b/ninja_extra/controllers/model/schemas.py @@ -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 @@ -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: diff --git a/pyproject.toml b/pyproject.toml index 8287f3a..08cf8b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"