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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy