Skip to content

Commit b1d231f

Browse files
committed
Allow usage of Django 2.x path in SimpleRouter
1 parent de497a9 commit b1d231f

File tree

3 files changed

+128
-10
lines changed

3 files changed

+128
-10
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/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: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from django.urls import NoReverseMatch
2222

2323
from rest_framework import views
24+
from rest_framework.compat import path
2425
from rest_framework.response import Response
2526
from rest_framework.reverse import reverse
2627
from rest_framework.schemas import SchemaGenerator
@@ -80,7 +81,7 @@ def urls(self):
8081

8182

8283
class SimpleRouter(BaseRouter):
83-
84+
8485
routes = [
8586
# List route.
8687
Route(
@@ -124,8 +125,28 @@ class SimpleRouter(BaseRouter):
124125
),
125126
]
126127

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

131152
def get_default_basename(self, viewset):
@@ -214,13 +235,12 @@ def get_lookup_regex(self, viewset, lookup_prefix=''):
214235
215236
https://github.com/alanjds/drf-nested-routers
216237
"""
217-
base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})'
218238
# Use `pk` as default field, unset set. Default regex should not
219239
# consume `.json` style suffixes and should break at '/' boundaries.
220240
lookup_field = getattr(viewset, 'lookup_field', 'pk')
221241
lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field
222-
lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
223-
return base_regex.format(
242+
lookup_value = getattr(viewset, 'lookup_value_regex', self._default_regex)
243+
return self._base_regex.format(
224244
lookup_prefix=lookup_prefix,
225245
lookup_url_kwarg=lookup_url_kwarg,
226246
lookup_value=lookup_value
@@ -230,6 +250,7 @@ def get_urls(self):
230250
"""
231251
Use the registered viewsets to generate a list of URL patterns.
232252
"""
253+
assert self._url_conf is not None, 'SimpleRouter requires Django 2.x when using path'
233254
ret = []
234255

235256
for prefix, viewset, basename in self.registry:
@@ -254,8 +275,12 @@ def get_urls(self):
254275
# controlled by project's urls.py and the router is in an app,
255276
# so a slash in the beginning will (A) cause Django to give
256277
# warnings and (B) generate URLS that will require using '//'.
257-
if not prefix and regex[:2] == '^/':
258-
regex = '^' + regex[2:]
278+
if not prefix:
279+
if self._url_conf is path:
280+
if regex[0] == '/':
281+
regex = regex[1:]
282+
elif regex[:2] == '^/':
283+
regex = '^' + regex[2:]
259284

260285
initkwargs = route.initkwargs.copy()
261286
initkwargs.update({
@@ -265,7 +290,7 @@ def get_urls(self):
265290

266291
view = viewset.as_view(mapping, **initkwargs)
267292
name = route.name.format(basename=basename)
268-
ret.append(url(regex, view, name=name))
293+
ret.append(self._url_conf(regex, view, name=name))
269294

270295
return ret
271296

tests/test_routers.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from collections import namedtuple
22

3+
import django
34
import pytest
45
from django.conf.urls import include, url
56
from django.core.exceptions import ImproperlyConfigured
@@ -8,11 +9,13 @@
89
from django.urls import resolve, reverse
910

1011
from rest_framework import permissions, serializers, viewsets
11-
from rest_framework.compat import get_regex_pattern
12+
from rest_framework.compat import get_regex_pattern, path
1213
from rest_framework.decorators import action
1314
from rest_framework.response import Response
1415
from rest_framework.routers import DefaultRouter, SimpleRouter
15-
from rest_framework.test import APIRequestFactory, URLPatternsTestCase
16+
from rest_framework.test import (
17+
APIClient, APIRequestFactory, URLPatternsTestCase
18+
)
1619
from rest_framework.utils import json
1720

1821
factory = APIRequestFactory()
@@ -77,9 +80,25 @@ def regex_url_path_detail(self, request, *args, **kwargs):
7780
return Response({'pk': pk, 'kwarg': kwarg})
7881

7982

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

99+
notes_path_router = SimpleRouter(use_regex_path=False)
100+
notes_path_router.register('notes', NoteViewSet)
101+
83102
kwarged_notes_router = SimpleRouter()
84103
kwarged_notes_router.register(r'notes', KWargedNoteViewSet)
85104

@@ -92,6 +111,9 @@ def regex_url_path_detail(self, request, *args, **kwargs):
92111
regex_url_path_router = SimpleRouter()
93112
regex_url_path_router.register(r'', RegexUrlPathViewSet, basename='regex')
94113

114+
url_path_router = SimpleRouter(use_regex_path=False)
115+
url_path_router.register('', UrlPathViewSet, basename='path')
116+
95117

96118
class BasicViewSet(viewsets.ViewSet):
97119
def list(self, request, *args, **kwargs):
@@ -463,6 +485,69 @@ def test_regex_url_path_detail(self):
463485
assert json.loads(response.content.decode()) == {'pk': pk, 'kwarg': kwarg}
464486

465487

488+
@pytest.mark.skipif(django.VERSION < (2, 0), reason='Django version < 2.0')
489+
class TestUrlPath(URLPatternsTestCase, TestCase):
490+
client_class = APIClient
491+
urlpatterns = [
492+
path('path/', include(url_path_router.urls)),
493+
path('example/', include(notes_path_router.urls))
494+
] if path else []
495+
496+
def setUp(self):
497+
RouterTestModel.objects.create(uuid='123', text='foo bar')
498+
RouterTestModel.objects.create(uuid='a b', text='baz qux')
499+
500+
def test_create(self):
501+
new_note = {
502+
'uuid': 'foo',
503+
'text': 'example'
504+
}
505+
response = self.client.post('/example/notes/', data=new_note)
506+
assert response.status_code == 201
507+
assert response['location'] == 'http://testserver/example/notes/foo/'
508+
assert response.data == {"url": "http://testserver/example/notes/foo/", "uuid": "foo", "text": "example"}
509+
assert RouterTestModel.objects.filter(uuid='foo').first() is not None
510+
511+
def test_retrieve(self):
512+
response = self.client.get('/example/notes/123/')
513+
assert response.status_code == 200
514+
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
515+
516+
def test_list(self):
517+
response = self.client.get('/example/notes/')
518+
assert response.status_code == 200
519+
assert response.data == [
520+
{"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"},
521+
{"url": "http://testserver/example/notes/a%20b/", "uuid": "a b", "text": "baz qux"},
522+
]
523+
524+
def test_update(self):
525+
updated_note = {
526+
'text': 'foo bar example'
527+
}
528+
response = self.client.patch('/example/notes/123/', data=updated_note)
529+
assert response.status_code == 200
530+
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar example"}
531+
532+
def test_delete(self):
533+
response = self.client.delete('/example/notes/123/')
534+
assert response.status_code == 204
535+
assert RouterTestModel.objects.filter(uuid='123').first() is None
536+
537+
def test_list_extra_action(self):
538+
kwarg = 1234
539+
response = self.client.get('/path/list/{}/'.format(kwarg))
540+
assert response.status_code == 200
541+
assert json.loads(response.content.decode()) == {'kwarg': kwarg}
542+
543+
def test_detail_extra_action(self):
544+
pk = '1'
545+
kwarg = 1234
546+
response = self.client.get('/path/{}/detail/{}/'.format(pk, kwarg))
547+
assert response.status_code == 200
548+
assert json.loads(response.content.decode()) == {'pk': pk, 'kwarg': kwarg}
549+
550+
466551
class TestViewInitkwargs(URLPatternsTestCase, TestCase):
467552
urlpatterns = [
468553
url(r'^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