Skip to content

Commit a29275f

Browse files
committed
Add SimplePathRouter
1 parent 372f4fd commit a29275f

File tree

3 files changed

+219
-46
lines changed

3 files changed

+219
-46
lines changed

docs/api-guide/routers.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ The router will match lookup values containing any characters except slashes and
173173
lookup_field = 'my_model_id'
174174
lookup_value_regex = '[0-9a-f]{32}'
175175

176+
## SimplePathRouter
177+
178+
This router is similar to `SimpleRouter` as above, but instead of _regexs_ it uses [path converters][path-convertes-topic-reference] to build urls.
179+
180+
**Note**: this router is available only with Django 2.x or above, since this feature was introduced in 2.0. See [release note][simplified-routing-release-note]
181+
176182
## DefaultRouter
177183

178184
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
@@ -340,3 +346,5 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions
340346
[drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
341347
[url-namespace-docs]: https://docs.djangoproject.com/en/1.11/topics/http/urls/#url-namespaces
342348
[include-api-reference]: https://docs.djangoproject.com/en/2.0/ref/urls/#include
349+
[simplified-routing-release-note]: https://docs.djangoproject.com/en/2.0/releases/2.0/#simplified-url-routing-syntax
350+
[path-convertes-topic-reference]: https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters

rest_framework/routers.py

Lines changed: 169 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from django.utils.deprecation import RenameMethodsBase
2424

2525
from rest_framework import RemovedInDRF311Warning, views
26+
from rest_framework.compat import path
2627
from rest_framework.response import Response
2728
from rest_framework.reverse import reverse
2829
from rest_framework.schemas import SchemaGenerator
@@ -99,50 +100,10 @@ def urls(self):
99100
return self._urls
100101

101102

102-
class SimpleRouter(BaseRouter):
103-
104-
routes = [
105-
# List route.
106-
Route(
107-
url=r'^{prefix}{trailing_slash}$',
108-
mapping={
109-
'get': 'list',
110-
'post': 'create'
111-
},
112-
name='{basename}-list',
113-
detail=False,
114-
initkwargs={'suffix': 'List'}
115-
),
116-
# Dynamically generated list routes. Generated using
117-
# @action(detail=False) decorator on methods of the viewset.
118-
DynamicRoute(
119-
url=r'^{prefix}/{url_path}{trailing_slash}$',
120-
name='{basename}-{url_name}',
121-
detail=False,
122-
initkwargs={}
123-
),
124-
# Detail route.
125-
Route(
126-
url=r'^{prefix}/{lookup}{trailing_slash}$',
127-
mapping={
128-
'get': 'retrieve',
129-
'put': 'update',
130-
'patch': 'partial_update',
131-
'delete': 'destroy'
132-
},
133-
name='{basename}-detail',
134-
detail=True,
135-
initkwargs={'suffix': 'Instance'}
136-
),
137-
# Dynamically generated detail routes. Generated using
138-
# @action(detail=True) decorator on methods of the viewset.
139-
DynamicRoute(
140-
url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
141-
name='{basename}-{url_name}',
142-
detail=True,
143-
initkwargs={}
144-
),
145-
]
103+
class AbstractSimpleRouter(BaseRouter):
104+
"""
105+
Base class for SimpleRouter and SimplePathRouter.
106+
"""
146107

147108
def __init__(self, trailing_slash=True):
148109
self.trailing_slash = '/' if trailing_slash else ''
@@ -223,6 +184,52 @@ def get_method_map(self, viewset, method_map):
223184
bound_methods[method] = action
224185
return bound_methods
225186

187+
188+
class SimpleRouter(AbstractSimpleRouter):
189+
190+
routes = [
191+
# List route.
192+
Route(
193+
url=r'^{prefix}{trailing_slash}$',
194+
mapping={
195+
'get': 'list',
196+
'post': 'create'
197+
},
198+
name='{basename}-list',
199+
detail=False,
200+
initkwargs={'suffix': 'List'}
201+
),
202+
# Dynamically generated list routes. Generated using
203+
# @action(detail=False) decorator on methods of the viewset.
204+
DynamicRoute(
205+
url=r'^{prefix}/{url_path}{trailing_slash}$',
206+
name='{basename}-{url_name}',
207+
detail=False,
208+
initkwargs={}
209+
),
210+
# Detail route.
211+
Route(
212+
url=r'^{prefix}/{lookup}{trailing_slash}$',
213+
mapping={
214+
'get': 'retrieve',
215+
'put': 'update',
216+
'patch': 'partial_update',
217+
'delete': 'destroy'
218+
},
219+
name='{basename}-detail',
220+
detail=True,
221+
initkwargs={'suffix': 'Instance'}
222+
),
223+
# Dynamically generated detail routes. Generated using
224+
# @action(detail=True) decorator on methods of the viewset.
225+
DynamicRoute(
226+
url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
227+
name='{basename}-{url_name}',
228+
detail=True,
229+
initkwargs={}
230+
),
231+
]
232+
226233
def get_lookup_regex(self, viewset, lookup_prefix=''):
227234
"""
228235
Given a viewset, return the portion of URL regex that is used
@@ -290,6 +297,124 @@ def get_urls(self):
290297
return ret
291298

292299

300+
if path is not None:
301+
class SimplePathRouter(AbstractSimpleRouter):
302+
"""
303+
Router which uses Django 2.x path to build urls
304+
"""
305+
306+
routes = [
307+
# List route.
308+
Route(
309+
url='{prefix}{trailing_slash}',
310+
mapping={
311+
'get': 'list',
312+
'post': 'create'
313+
},
314+
name='{basename}-list',
315+
detail=False,
316+
initkwargs={'suffix': 'List'}
317+
),
318+
# Dynamically generated list routes. Generated using
319+
# @action(detail=False) decorator on methods of the viewset.
320+
DynamicRoute(
321+
url='{prefix}/{url_path}{trailing_slash}',
322+
name='{basename}-{url_name}',
323+
detail=False,
324+
initkwargs={}
325+
),
326+
# Detail route.
327+
Route(
328+
url='{prefix}/{lookup}{trailing_slash}',
329+
mapping={
330+
'get': 'retrieve',
331+
'put': 'update',
332+
'patch': 'partial_update',
333+
'delete': 'destroy'
334+
},
335+
name='{basename}-detail',
336+
detail=True,
337+
initkwargs={'suffix': 'Instance'}
338+
),
339+
# Dynamically generated detail routes. Generated using
340+
# @action(detail=True) decorator on methods of the viewset.
341+
DynamicRoute(
342+
url='{prefix}/{lookup}/{url_path}{trailing_slash}',
343+
name='{basename}-{url_name}',
344+
detail=True,
345+
initkwargs={}
346+
),
347+
]
348+
349+
def get_lookup_path(self, viewset, lookup_prefix=''):
350+
"""
351+
Given a viewset, return the portion of URL path that is used
352+
to match against a single instance.
353+
354+
Note that lookup_prefix is not used directly inside REST rest_framework
355+
itself, but is required in order to nicely support nested router
356+
implementations, such as drf-nested-routers.
357+
358+
https://github.com/alanjds/drf-nested-routers
359+
"""
360+
base_regex = '<{lookup_converter}:{lookup_prefix}{lookup_url_kwarg}>'
361+
# Use `pk` as default field, unset set. Default regex should not
362+
# consume `.json` style suffixes and should break at '/' boundaries.
363+
lookup_field = getattr(viewset, 'lookup_field', 'pk')
364+
lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field
365+
lookup_converter = getattr(viewset, 'lookup_converter', 'path')
366+
return base_regex.format(
367+
lookup_prefix=lookup_prefix,
368+
lookup_url_kwarg=lookup_url_kwarg,
369+
lookup_converter=lookup_converter
370+
)
371+
372+
def get_urls(self):
373+
"""
374+
Use the registered viewsets to generate a list of URL patterns.
375+
"""
376+
ret = []
377+
378+
for prefix, viewset, basename in self.registry:
379+
lookup = self.get_lookup_path(viewset)
380+
routes = self.get_routes(viewset)
381+
382+
for route in routes:
383+
384+
# Only actions which actually exist on the viewset will be bound
385+
mapping = self.get_method_map(viewset, route.mapping)
386+
if not mapping:
387+
continue
388+
389+
# Build the url pattern
390+
url_path = route.url.format(
391+
prefix=prefix,
392+
lookup=lookup,
393+
trailing_slash=self.trailing_slash
394+
)
395+
396+
# If there is no prefix, the first part of the url is probably
397+
# controlled by project's urls.py and the router is in an app,
398+
# so a slash in the beginning will (A) cause Django to give
399+
# warnings and (B) generate URLS that will require using '//'.
400+
if not prefix and url_path[0] == '/':
401+
url_path = url_path[1:]
402+
403+
initkwargs = route.initkwargs.copy()
404+
initkwargs.update({
405+
'basename': basename,
406+
'detail': route.detail,
407+
})
408+
409+
view = viewset.as_view(mapping, **initkwargs)
410+
name = route.name.format(basename=basename)
411+
ret.append(path(url_path, view, name=name))
412+
413+
return ret
414+
else:
415+
SimplePathRouter = None
416+
417+
293418
class APIRootView(views.APIView):
294419
"""
295420
The default basic root view for DefaultRouter

tests/test_routers.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import unittest
12
import warnings
23
from collections import namedtuple
34

@@ -11,10 +12,12 @@
1112
from rest_framework import (
1213
RemovedInDRF311Warning, permissions, serializers, viewsets
1314
)
14-
from rest_framework.compat import get_regex_pattern
15+
from rest_framework.compat import get_regex_pattern, path
1516
from rest_framework.decorators import action
1617
from rest_framework.response import Response
17-
from rest_framework.routers import DefaultRouter, SimpleRouter
18+
from rest_framework.routers import (
19+
DefaultRouter, SimplePathRouter, SimpleRouter
20+
)
1821
from rest_framework.test import APIRequestFactory, URLPatternsTestCase
1922
from rest_framework.utils import json
2023

@@ -80,6 +83,19 @@ def regex_url_path_detail(self, request, *args, **kwargs):
8083
return Response({'pk': pk, 'kwarg': kwarg})
8184

8285

86+
class UrlPathViewSet(viewsets.ViewSet):
87+
@action(detail=False, url_path='list/<int:kwarg>')
88+
def url_path_list(self, request, *args, **kwargs):
89+
kwarg = self.kwargs.get('kwarg', '')
90+
return Response({'kwarg': kwarg})
91+
92+
@action(detail=True, url_path='detail/<int:kwarg>')
93+
def url_path_detail(self, request, *args, **kwargs):
94+
pk = self.kwargs.get('pk', '')
95+
kwarg = self.kwargs.get('kwarg', '')
96+
return Response({'pk': pk, 'kwarg': kwarg})
97+
98+
8399
notes_router = SimpleRouter()
84100
notes_router.register(r'notes', NoteViewSet)
85101

@@ -95,6 +111,10 @@ def regex_url_path_detail(self, request, *args, **kwargs):
95111
regex_url_path_router = SimpleRouter()
96112
regex_url_path_router.register(r'', RegexUrlPathViewSet, basename='regex')
97113

114+
if path is not None:
115+
url_path_router = SimplePathRouter()
116+
url_path_router.register('', UrlPathViewSet, basename='path')
117+
98118

99119
class BasicViewSet(viewsets.ViewSet):
100120
def list(self, request, *args, **kwargs):
@@ -466,6 +486,26 @@ def test_regex_url_path_detail(self):
466486
assert json.loads(response.content.decode()) == {'pk': pk, 'kwarg': kwarg}
467487

468488

489+
@unittest.skipUnless(path, 'django.urls.path is not defined')
490+
class TestUrlPath(URLPatternsTestCase, TestCase):
491+
urlpatterns = [
492+
path('path/', include(url_path_router.urls))
493+
] if path else []
494+
495+
def test_url_path_list(self):
496+
kwarg = 1234
497+
response = self.client.get('/path/list/{}/'.format(kwarg))
498+
assert response.status_code == 200
499+
assert json.loads(response.content.decode()) == {'kwarg': kwarg}
500+
501+
def test_url_path_detail(self):
502+
pk = '1'
503+
kwarg = 1234
504+
response = self.client.get('/path/{}/detail/{}/'.format(pk, kwarg))
505+
assert response.status_code == 200
506+
assert json.loads(response.content.decode()) == {'pk': pk, 'kwarg': kwarg}
507+
508+
469509
class TestViewInitkwargs(URLPatternsTestCase, TestCase):
470510
urlpatterns = [
471511
url(r'^example/', include(notes_router.urls)),

0 commit comments

Comments
 (0)