Skip to content

Commit 4896478

Browse files
authored
Merge pull request #311 from anentropic/issue-310
implement urls namespace for APIController
2 parents 594184a + c2b72e9 commit 4896478

File tree

5 files changed

+65
-6
lines changed

5 files changed

+65
-6
lines changed

docs/api_controller/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ The fourth argument, `permissions`, is a list of all permissions that should be
4343

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

46+
The fifth argument, `urls_namespace`, defaults to `None`, or if set will nest the routes of this controller under their own namespace.
47+
4648
for example:
4749

4850
```python

ninja_extra/controllers/base.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from django.db.models import Model, QuerySet
2929
from django.http import HttpResponse
30-
from django.urls import URLPattern
30+
from django.urls import URLPattern, URLResolver, include
3131
from django.urls import path as django_path
3232
from injector import inject, is_decorated_with_inject
3333
from ninja import NinjaAPI, Router
@@ -403,8 +403,11 @@ def __init__(
403403
tags: Union[Optional[List[str]], str] = None,
404404
permissions: Optional[List[BasePermissionType]] = None,
405405
auto_import: bool = True,
406+
urls_namespace: Optional[str] = None,
406407
) -> None:
407408
self.prefix = prefix
409+
# Optional controller-level URL namespace. Applied to all route paths.
410+
self.urls_namespace = urls_namespace or None
408411
# `auth` primarily defines APIController route function global authentication method.
409412
self.auth: Optional[AuthBase] = auth
410413

@@ -553,16 +556,34 @@ def add_controller_route_function(self, route_function: RouteFunction) -> None:
553556
get_function_name(route_function.route.view_func)
554557
] = route_function
555558

556-
def urls_paths(self, prefix: str) -> Iterator[URLPattern]:
559+
def urls_paths(self, prefix: str) -> Iterator[Union[URLPattern, URLResolver]]:
560+
namespaced_patterns: List[URLPattern] = []
561+
557562
for path, path_view in self.path_operations.items():
558563
path = path.replace("{", "<").replace("}", ">")
559564
route = "/".join([i for i in (prefix, path) if i])
560565
# to skip lot of checks we simply treat double slash as a mistake:
561566
route = normalize_path(route)
562567
route = route.lstrip("/")
568+
563569
for op in path_view.operations:
564570
op = cast(Operation, op)
565-
yield django_path(route, path_view.get_view(), name=op.url_name)
571+
view = path_view.get_view()
572+
if op.url_name:
573+
pattern = django_path(route, view, name=op.url_name)
574+
else:
575+
pattern = django_path(route, view)
576+
577+
if self.urls_namespace:
578+
namespaced_patterns.append(pattern)
579+
else:
580+
yield pattern
581+
582+
if self.urls_namespace and namespaced_patterns:
583+
yield django_path(
584+
"",
585+
include((namespaced_patterns, self.urls_namespace), namespace=self.urls_namespace),
586+
)
566587

567588
def __repr__(self) -> str: # pragma: no cover
568589
return f"<controller - {self.controller_class.__name__}>"
@@ -590,6 +611,8 @@ def _add_operation_from_route_function(self, route_function: RouteFunction) -> N
590611
f"endpoint={get_function_name(route_function.route.view_func)}"
591612
)
592613
data = route_function.route.route_params.dict()
614+
if not data.get("url_name"):
615+
data["url_name"] = get_function_name(route_function.route.view_func)
593616
route_function.operation = self.add_api_operation(
594617
view_func=route_function.as_view, **data
595618
)
@@ -663,19 +686,21 @@ def api_controller(
663686
tags: Union[Optional[List[str]], str] = None,
664687
permissions: Optional[List[BasePermissionType]] = None,
665688
auto_import: bool = True,
689+
urls_namespace: Optional[str] = None,
666690
) -> Callable[
667691
[Union[Type, Type[T]]], Union[Type[ControllerBase], Type[T]]
668692
]: # pragma: no cover
669693
...
670694

671695

672696
def api_controller(
673-
prefix_or_class: Union[str, Union[ControllerClassType, Type]] = "",
697+
prefix_or_class: Union[str, ControllerClassType] = "",
674698
auth: Any = NOT_SET,
675699
throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
676700
tags: Union[Optional[List[str]], str] = None,
677701
permissions: Optional[List[BasePermissionType]] = None,
678702
auto_import: bool = True,
703+
urls_namespace: Optional[str] = None,
679704
) -> Union[ControllerClassType, Callable[[ControllerClassType], ControllerClassType]]:
680705
if isinstance(prefix_or_class, type):
681706
return APIController(
@@ -685,6 +710,7 @@ def api_controller(
685710
permissions=permissions,
686711
auto_import=auto_import,
687712
throttle=throttle,
713+
urls_namespace=urls_namespace,
688714
)(prefix_or_class)
689715

690716
def _decorator(cls: ControllerClassType) -> ControllerClassType:
@@ -695,6 +721,7 @@ def _decorator(cls: ControllerClassType) -> ControllerClassType:
695721
permissions=permissions,
696722
auto_import=auto_import,
697723
throttle=throttle,
724+
urls_namespace=urls_namespace,
698725
)(cls)
699726

700727
return _decorator

tests/controllers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,14 @@ def get_event(self, event_id: int = Path(..., alias="id")) -> EventSchema:
5555
def get_event_from_orm(self, id: int) -> EventSchema:
5656
event = get_object_or_404(Event, id=id)
5757
return EventSchema.from_orm(event)
58+
59+
60+
@api_controller("inventory-items", urls_namespace="inventory")
61+
class NamespacedController:
62+
@http_get("", url_name="inventory-item-list")
63+
def list_items(self):
64+
return [{"id": 1, "name": "sample"}]
65+
66+
@http_get("/{int:item_id}", url_name="inventory-item-detail")
67+
def get_item(self, item_id: int):
68+
return {"id": item_id, "name": f"sample-{item_id}"}

tests/test_controller.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import django
44
import pytest
55
from django.contrib.auth.models import Group
6+
from django.urls import reverse
67

78
from ninja_extra import (
89
NinjaExtraAPI,
@@ -308,3 +309,21 @@ async def example(self):
308309
example_route_function.operation.auth_callbacks[0],
309310
AsyncFakeAuth,
310311
)
312+
313+
314+
def test_namespaced_controller_list(client):
315+
response = client.get("/api/inventory-items")
316+
assert response.status_code == 200
317+
assert response.json() == [{"id": 1, "name": "sample"}]
318+
assert reverse("api-1.0.0:inventory:inventory-item-list") == "/api/inventory-items"
319+
320+
321+
def test_namespaced_controller_detail(client):
322+
response = client.get("/api/inventory-items/5")
323+
assert response.status_code == 200
324+
assert response.json() == {"id": 5, "name": "sample-5"}
325+
assert reverse("api-1.0.0:inventory:inventory-item-detail", kwargs={"item_id": 5}) == "/api/inventory-items/5"
326+
327+
328+
def test_default_url_name(client):
329+
assert reverse("api-1.0.0:get_event", kwargs={"id": 5}) == "/api/events/5"

tests/urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
from ninja_extra import NinjaExtraAPI
55

6-
from .controllers import EventController
6+
from .controllers import EventController, NamespacedController
77

88
api = NinjaExtraAPI()
9-
api.register_controllers(EventController)
9+
api.register_controllers(EventController, NamespacedController)
1010

1111

1212
urlpatterns = [

0 commit comments

Comments
 (0)