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
4 changes: 3 additions & 1 deletion docs/api_controller/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ The fourth argument, `permissions`, is a list of all permissions that should be

The fifth argument, `auto_import`, defaults to true, which automatically adds your controller to auto import list.

The fifth argument, `urls_namespace`, defaults to `None`, or if set will nest the routes of this controller under their own namespace.
The sixth argument, `urls_namespace`, defaults to `Controller Class Name`, or if set will nest the routes of this controller under their own namespace.

The seventh argument, `append_unique_op_id`, defaults to true, which appends a unique operation id suffix to the controller's routes to avoid conflicts.

for example:

Expand Down
14 changes: 13 additions & 1 deletion ninja_extra/controllers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ def __init__(
permissions: Optional[List[BasePermissionType]] = None,
auto_import: bool = True,
urls_namespace: Optional[str] = None,
append_unique_op_id: bool = True
) -> None:
self.prefix = prefix
# Optional controller-level URL namespace. Applied to all route paths.
Expand All @@ -413,6 +414,7 @@ def __init__(

self.tags = tags # type: ignore
self.throttle = throttle
self.append_unique_op_id = append_unique_op_id

self.auto_import: bool = auto_import # set to false and it would be ignored when api.auto_discover is called
# `controller_class` target class that the APIController wraps
Expand Down Expand Up @@ -597,7 +599,13 @@ def _add_operation_from_route_function(self, route_function: RouteFunction) -> N
controller_name = (
str(self.controller_class.__name__).lower().replace("controller", "")
)
route_function.route.route_params.operation_id = f"{controller_name}_{route_function.route.view_func.__name__}_{str(uuid.uuid4())[:8]}"
route_function.route.route_params.operation_id = (
f"{controller_name}_{route_function.route.view_func.__name__}"
)
if self.append_unique_op_id:
route_function.route.route_params.operation_id += (
f"_{uuid.uuid4().hex[:8]}"
)

if (
self.auth
Expand Down Expand Up @@ -687,6 +695,7 @@ def api_controller(
permissions: Optional[List[BasePermissionType]] = None,
auto_import: bool = True,
urls_namespace: Optional[str] = None,
append_unique_op_id: bool = True,
) -> Callable[
[Union[Type, Type[T]]], Union[Type[ControllerBase], Type[T]]
]: # pragma: no cover
Expand All @@ -701,6 +710,7 @@ def api_controller(
permissions: Optional[List[BasePermissionType]] = None,
auto_import: bool = True,
urls_namespace: Optional[str] = None,
append_unique_op_id: bool = True,
) -> Union[ControllerClassType, Callable[[ControllerClassType], ControllerClassType]]:
if isinstance(prefix_or_class, type):
return APIController(
Expand All @@ -710,6 +720,7 @@ def api_controller(
permissions=permissions,
auto_import=auto_import,
throttle=throttle,
append_unique_op_id=append_unique_op_id,
urls_namespace=urls_namespace,
)(prefix_or_class)

Expand All @@ -721,6 +732,7 @@ def _decorator(cls: ControllerClassType) -> ControllerClassType:
permissions=permissions,
auto_import=auto_import,
throttle=throttle,
append_unique_op_id=append_unique_op_id,
urls_namespace=urls_namespace,
)(cls)

Expand Down
51 changes: 51 additions & 0 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ class DisableAutoImportController:
auto_import = False # disable auto_import of the controller


@api_controller
class SomeControllerWithSingleRoute:
@http_get("/example")
def example(self):
pass


@api_controller(append_unique_op_id=False)
class SomeControllerWithoutUniqueSuffix:
@http_get("/example")
def example(self):
pass


class TestAPIController:
def test_api_controller_as_decorator(self):
controller_type = api_controller("prefix", tags="new_tag", auth=FakeAuth())(
Expand Down Expand Up @@ -144,6 +158,43 @@ def test_controller_should_have_path_operation_list(self):
assert operation.methods == route_function.route.route_params.methods
assert operation.operation_id == route_function.route.route_params.operation_id

def test_controller_should_append_unique_op_id_to_operation_id(self):
_api_controller = SomeControllerWithSingleRoute.get_api_controller()
controller_name = (
str(_api_controller.controller_class.__name__)
.lower()
.replace("controller", "")
)
route_view_func_name: RouteFunction = get_route_function(
SomeControllerWithRoute().example
).route.view_func.__name__

operation_id = (
_api_controller._path_operations.get("/example").operations[0].operation_id
)
raw_operation_id = "_".join(operation_id.split("_")[:-1])
op_id_postfix = operation_id.split("_")[-1]

assert raw_operation_id == f"{controller_name}_{route_view_func_name}"
assert len(op_id_postfix) == 8

def test_controller_should_not_add_unique_suffix_following_params(self):
_api_controller = SomeControllerWithoutUniqueSuffix.get_api_controller()
controller_name = (
str(_api_controller.controller_class.__name__)
.lower()
.replace("controller", "")
)
route_view_func_name: RouteFunction = get_route_function(
SomeControllerWithRoute().example
).route.view_func.__name__

operation_id = (
_api_controller._path_operations.get("/example").operations[0].operation_id
)

assert operation_id == f"{controller_name}_{route_view_func_name}"

def test_get_route_function_should_return_instance_route_definitions(self):
for route_definition in get_route_functions(SomeControllerWithRoute):
assert isinstance(route_definition, RouteFunction)
Expand Down