Skip to content

Commit 765d728

Browse files
committed
Allow usage of Django 2.x path in SimpleRouter
1 parent 35c5be6 commit 765d728

File tree

3 files changed

+123
-9
lines changed

3 files changed

+123
-9
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+
By default the URLs created by `SimpleRouter` uses _regexs_ to build urls. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router, in this case [path converters][path-convertes-topic-reference] are used. For example:
177+
178+
router = SimpleRouter(use_regex_path=False)
179+
180+
**Note**: `use_regex_path=False` only works with Django 2.x or above, since this feature was introduced in 2.0.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/4.0/topics/http/urls/#url-namespaces
342348
[include-api-reference]: https://docs.djangoproject.com/en/4.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: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from collections import OrderedDict, namedtuple
1818

1919
from django.core.exceptions import ImproperlyConfigured
20-
from django.urls import NoReverseMatch, re_path
20+
from django.urls import NoReverseMatch, path, re_path
2121

2222
from rest_framework import views
2323
from rest_framework.response import Response
@@ -123,8 +123,28 @@ class SimpleRouter(BaseRouter):
123123
),
124124
]
125125

126-
def __init__(self, trailing_slash=True):
126+
def __init__(self, trailing_slash=True, use_regex_path=True):
127127
self.trailing_slash = '/' if trailing_slash else ''
128+
if use_regex_path:
129+
self._base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})'
130+
self._default_regex = '[^/.]+'
131+
self._url_conf = re_path
132+
else:
133+
self._base_regex = '<{lookup_value}:{lookup_prefix}{lookup_url_kwarg}>'
134+
self._default_regex = 'path'
135+
self._url_conf = path
136+
# remove regex characters from routes
137+
_routes = []
138+
for route in self.routes:
139+
url_param = route.url
140+
if url_param[0] == '^':
141+
url_param = url_param[1:]
142+
if url_param[-1] == '$':
143+
url_param = url_param[:-1]
144+
145+
_routes.append(route._replace(url=url_param))
146+
self.routes = _routes
147+
128148
super().__init__()
129149

130150
def get_default_basename(self, viewset):
@@ -213,13 +233,12 @@ def get_lookup_regex(self, viewset, lookup_prefix=''):
213233
214234
https://github.com/alanjds/drf-nested-routers
215235
"""
216-
base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})'
217236
# Use `pk` as default field, unset set. Default regex should not
218237
# consume `.json` style suffixes and should break at '/' boundaries.
219238
lookup_field = getattr(viewset, 'lookup_field', 'pk')
220239
lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field
221-
lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
222-
return base_regex.format(
240+
lookup_value = getattr(viewset, 'lookup_value_regex', self._default_regex)
241+
return self._base_regex.format(
223242
lookup_prefix=lookup_prefix,
224243
lookup_url_kwarg=lookup_url_kwarg,
225244
lookup_value=lookup_value
@@ -253,8 +272,12 @@ def get_urls(self):
253272
# controlled by project's urls.py and the router is in an app,
254273
# so a slash in the beginning will (A) cause Django to give
255274
# warnings and (B) generate URLS that will require using '//'.
256-
if not prefix and regex[:2] == '^/':
257-
regex = '^' + regex[2:]
275+
if not prefix:
276+
if self._url_conf is path:
277+
if regex[0] == '/':
278+
regex = regex[1:]
279+
elif regex[:2] == '^/':
280+
regex = '^' + regex[2:]
258281

259282
initkwargs = route.initkwargs.copy()
260283
initkwargs.update({
@@ -264,7 +287,7 @@ def get_urls(self):
264287

265288
view = viewset.as_view(mapping, **initkwargs)
266289
name = route.name.format(basename=basename)
267-
ret.append(re_path(regex, view, name=name))
290+
ret.append(self._url_conf(regex, view, name=name))
268291

269292
return ret
270293

tests/test_routers.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from rest_framework.decorators import action
1111
from rest_framework.response import Response
1212
from rest_framework.routers import DefaultRouter, SimpleRouter
13-
from rest_framework.test import APIRequestFactory, URLPatternsTestCase
13+
from rest_framework.test import (
14+
APIClient, APIRequestFactory, URLPatternsTestCase
15+
)
1416
from rest_framework.utils import json
1517

1618
factory = APIRequestFactory()
@@ -75,9 +77,25 @@ def regex_url_path_detail(self, request, *args, **kwargs):
7577
return Response({'pk': pk, 'kwarg': kwarg})
7678

7779

80+
class UrlPathViewSet(viewsets.ViewSet):
81+
@action(detail=False, url_path='list/<int:kwarg>')
82+
def url_path_list(self, request, *args, **kwargs):
83+
kwarg = self.kwargs.get('kwarg', '')
84+
return Response({'kwarg': kwarg})
85+
86+
@action(detail=True, url_path='detail/<int:kwarg>')
87+
def url_path_detail(self, request, *args, **kwargs):
88+
pk = self.kwargs.get('pk', '')
89+
kwarg = self.kwargs.get('kwarg', '')
90+
return Response({'pk': pk, 'kwarg': kwarg})
91+
92+
7893
notes_router = SimpleRouter()
7994
notes_router.register(r'notes', NoteViewSet)
8095

96+
notes_path_router = SimpleRouter(use_regex_path=False)
97+
notes_path_router.register('notes', NoteViewSet)
98+
8199
kwarged_notes_router = SimpleRouter()
82100
kwarged_notes_router.register(r'notes', KWargedNoteViewSet)
83101

@@ -90,6 +108,9 @@ def regex_url_path_detail(self, request, *args, **kwargs):
90108
regex_url_path_router = SimpleRouter()
91109
regex_url_path_router.register(r'', RegexUrlPathViewSet, basename='regex')
92110

111+
url_path_router = SimpleRouter(use_regex_path=False)
112+
url_path_router.register('', UrlPathViewSet, basename='path')
113+
93114

94115
class BasicViewSet(viewsets.ViewSet):
95116
def list(self, request, *args, **kwargs):
@@ -459,6 +480,68 @@ def test_regex_url_path_detail(self):
459480
assert json.loads(response.content.decode()) == {'pk': pk, 'kwarg': kwarg}
460481

461482

483+
class TestUrlPath(URLPatternsTestCase, TestCase):
484+
client_class = APIClient
485+
urlpatterns = [
486+
path('path/', include(url_path_router.urls)),
487+
path('example/', include(notes_path_router.urls))
488+
]
489+
490+
def setUp(self):
491+
RouterTestModel.objects.create(uuid='123', text='foo bar')
492+
RouterTestModel.objects.create(uuid='a b', text='baz qux')
493+
494+
def test_create(self):
495+
new_note = {
496+
'uuid': 'foo',
497+
'text': 'example'
498+
}
499+
response = self.client.post('/example/notes/', data=new_note)
500+
assert response.status_code == 201
501+
assert response['location'] == 'http://testserver/example/notes/foo/'
502+
assert response.data == {"url": "http://testserver/example/notes/foo/", "uuid": "foo", "text": "example"}
503+
assert RouterTestModel.objects.filter(uuid='foo').exists()
504+
505+
def test_retrieve(self):
506+
response = self.client.get('/example/notes/123/')
507+
assert response.status_code == 200
508+
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
509+
510+
def test_list(self):
511+
response = self.client.get('/example/notes/')
512+
assert response.status_code == 200
513+
assert response.data == [
514+
{"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"},
515+
{"url": "http://testserver/example/notes/a%20b/", "uuid": "a b", "text": "baz qux"},
516+
]
517+
518+
def test_update(self):
519+
updated_note = {
520+
'text': 'foo bar example'
521+
}
522+
response = self.client.patch('/example/notes/123/', data=updated_note)
523+
assert response.status_code == 200
524+
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar example"}
525+
526+
def test_delete(self):
527+
response = self.client.delete('/example/notes/123/')
528+
assert response.status_code == 204
529+
assert not RouterTestModel.objects.filter(uuid='123').exists()
530+
531+
def test_list_extra_action(self):
532+
kwarg = 1234
533+
response = self.client.get('/path/list/{}/'.format(kwarg))
534+
assert response.status_code == 200
535+
assert json.loads(response.content.decode()) == {'kwarg': kwarg}
536+
537+
def test_detail_extra_action(self):
538+
pk = '1'
539+
kwarg = 1234
540+
response = self.client.get('/path/{}/detail/{}/'.format(pk, kwarg))
541+
assert response.status_code == 200
542+
assert json.loads(response.content.decode()) == {'pk': pk, 'kwarg': kwarg}
543+
544+
462545
class TestViewInitkwargs(URLPatternsTestCase, TestCase):
463546
urlpatterns = [
464547
path('example/', include(notes_router.urls)),

0 commit comments

Comments
 (0)