diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index c87b9299a..fc66d8704 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -339,7 +339,8 @@ class HyperlinkedRelatedField(RelatedField):
self.view_name, request
)
except AttributeError:
- expected_viewname = self.view_name
+ # by default, expect the 'rest_framework' namespace
+ expected_viewname = 'rest_framework:' + self.view_name
if match.view_name != expected_viewname:
self.fail('incorrect_match')
diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py
index e9cf737f1..7eba38f9a 100644
--- a/rest_framework/reverse.py
+++ b/rest_framework/reverse.py
@@ -34,10 +34,29 @@ def preserve_builtin_query_params(url, request=None):
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
"""
+ Extends `django.urls.reverse` with behavior specific to rest framework.
+
+ The `viewname` will be prepended with the 'rest_framework' application
+ namespace if no namspace is included in the `viewname` argument. The
+ framework fundamentally assumes that the router urls will be included with
+ the 'rest_framework' namespace, so ensure that your root url patterns are
+ configured accordingly. Assuming you use the default router, you can check
+ this with:
+
+ from django.urls import reverse
+
+ reverse('rest_framework:api-root')
+
If versioning is being used then we pass any `reverse` calls through
to the versioning scheme instance, so that the resulting URL
can be modified if needed.
+
+ Optionally takes a `request` object (see `_reverse` for details).
"""
+ # prepend the 'rest_framework' application namespace
+ if ':' not in viewname:
+ viewname = 'rest_framework:' + viewname
+
scheme = getattr(request, 'versioning_scheme', None)
if scheme is not None:
try:
@@ -56,14 +75,33 @@ def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extr
"""
Same as `django.urls.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
+
+ Additionally, the request is used to determine the `current_app` instance.
"""
if format is not None:
kwargs = kwargs or {}
kwargs['format'] = format
+ if request:
+ extra.setdefault('current_app', current_app(request))
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
if request:
return request.build_absolute_uri(url)
return url
+def current_app(request):
+ """
+ Get the current app for the request.
+
+ This code is copied from the URL tag.
+ """
+ try:
+ return request.current_app
+ except AttributeError:
+ try:
+ return request.resolver_match.namespace
+ except AttributeError:
+ return None
+
+
reverse_lazy = lazy(reverse, six.text_type)
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 281bbde8a..55c9ca422 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -101,6 +101,10 @@ class BaseRouter(object):
self._urls = self.get_urls()
return self._urls
+ @property
+ def urlpatterns(self):
+ return (self.urls, 'rest_framework')
+
class SimpleRouter(BaseRouter):
@@ -305,10 +309,7 @@ class APIRootView(views.APIView):
def get(self, request, *args, **kwargs):
# Return a plain {"name": "hyperlink"} response.
ret = OrderedDict()
- namespace = request.resolver_match.namespace
for key, url_name in self.api_root_dict.items():
- if namespace:
- url_name = namespace + ':' + url_name
try:
ret[key] = reverse(
url_name,
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index 2a2459b37..abf7d4d74 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -96,7 +96,7 @@ def optional_login(request):
Include a login snippet if REST framework's login view is in the URLconf.
"""
try:
- login_url = reverse('rest_framework:login')
+ login_url = reverse('rest_framework_auth:login')
except NoReverseMatch:
return ''
@@ -112,7 +112,7 @@ def optional_docs_login(request):
Include a login snippet if REST framework's login view is in the URLconf.
"""
try:
- login_url = reverse('rest_framework:login')
+ login_url = reverse('rest_framework_auth:login')
except NoReverseMatch:
return 'log in'
@@ -128,7 +128,7 @@ def optional_logout(request, user):
Include a logout snippet if REST framework's logout view is in the URLconf.
"""
try:
- logout_url = reverse('rest_framework:logout')
+ logout_url = reverse('rest_framework_auth:logout')
except NoReverseMatch:
snippet = format_html('
{user}', user=escape(user))
return mark_safe(snippet)
diff --git a/rest_framework/urls.py b/rest_framework/urls.py
index 80fce5dc4..8d40727c4 100644
--- a/rest_framework/urls.py
+++ b/rest_framework/urls.py
@@ -27,7 +27,7 @@ else:
logout = views.LogoutView.as_view()
-app_name = 'rest_framework'
+app_name = 'rest_framework_auth'
urlpatterns = [
url(r'^login/$', login, login_kwargs, name='login'),
url(r'^logout/$', logout, name='logout'),
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index 5c9a7ade1..384f1eb75 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -135,6 +135,13 @@ class NamespaceVersioning(BaseVersioning):
)
def get_versioned_viewname(self, viewname, request):
+ """
+ The incoming `viewname` should be prefixed with the 'rest_framework'
+ application namespace. We want to replace this with the version
+ instance namespace.
+ """
+ if viewname.startswith('rest_framework:'):
+ viewname = viewname[len('rest_framework:'):]
return request.version + ':' + viewname
diff --git a/tests/test_lazy_hyperlinks.py b/tests/test_lazy_hyperlinks.py
index cf3ee735f..a502ce946 100644
--- a/tests/test_lazy_hyperlinks.py
+++ b/tests/test_lazy_hyperlinks.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url
+from django.conf.urls import include, url
from django.db import models
from django.test import TestCase, override_settings
@@ -28,10 +28,14 @@ def dummy_view(request):
pass
-urlpatterns = [
+patterns = [
url(r'^example/(?P[0-9]+)/$', dummy_view, name='example-detail'),
]
+urlpatterns = [
+ url(r'^', include((patterns, 'rest_framework')))
+]
+
@override_settings(ROOT_URLCONF='tests.test_lazy_hyperlinks')
class TestLazyHyperlinkNames(TestCase):
diff --git a/tests/test_relations.py b/tests/test_relations.py
index fd3256e89..b686bea17 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -2,7 +2,7 @@ import uuid
import pytest
from _pytest.monkeypatch import MonkeyPatch
-from django.conf.urls import url
+from django.conf.urls import include, url
from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
from django.utils.datastructures import MultiValueDict
@@ -146,7 +146,9 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase):
@override_settings(ROOT_URLCONF=[
- url(r'^example/(?P.+)/$', lambda: None, name='example'),
+ url(r'^', include(
+ ([url(r'^example/(?P.+)/$', lambda: None, name='example')], 'rest_framework')
+ )),
])
class TestHyperlinkedRelatedField(APISimpleTestCase):
def setUp(self):
diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py
index 887a6f423..45faf90a2 100644
--- a/tests/test_relations_hyperlink.py
+++ b/tests/test_relations_hyperlink.py
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
-from django.conf.urls import url
+from django.conf.urls import include, url
from django.test import TestCase, override_settings
from rest_framework import serializers
@@ -18,7 +18,7 @@ def dummy_view(request, pk):
pass
-urlpatterns = [
+patterns = [
url(r'^dummyurl/(?P[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
@@ -29,6 +29,10 @@ urlpatterns = [
url(r'^nullableonetoonesource/(?P[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'),
]
+urlpatterns = [
+ url(r'^', include((patterns, 'rest_framework')))
+]
+
# ManyToMany
class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer):
@@ -449,3 +453,39 @@ class HyperlinkedNullableOneToOneTests(TestCase):
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
]
assert serializer.data == expected
+
+
+class HyperlinkedNamespaceTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ new_target = ForeignKeyTarget(name='target-2')
+ new_target.save()
+ for idx in range(1, 4):
+ source = ForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def results(self):
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+ expected = [
+ {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
+ {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+ {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
+ ]
+ with self.assertNumQueries(1):
+ assert serializer.data == expected
+
+ def test_foreign_key_retrieve_no_namespace(self):
+ url_conf = [
+ url(r'^', include((patterns, 'rest_framework'), namespace=None))
+ ]
+ with override_settings(ROOT_URLCONF=url_conf):
+ self.results()
+
+ def test_foreign_key_retrieve_namespace(self):
+ url_conf = [
+ url(r'^', include((patterns, 'rest_framework'), namespace='api'))
+ ]
+ with override_settings(ROOT_URLCONF=url_conf):
+ self.results()
diff --git a/tests/test_reverse.py b/tests/test_reverse.py
index 145b1a54f..673f46002 100644
--- a/tests/test_reverse.py
+++ b/tests/test_reverse.py
@@ -1,21 +1,34 @@
from __future__ import unicode_literals
-from django.conf.urls import url
+from django.conf.urls import include, url
+from django.http import HttpResponse
from django.test import TestCase, override_settings
from django.urls import NoReverseMatch
-from rest_framework.reverse import reverse
+from rest_framework.reverse import current_app, reverse
from rest_framework.test import APIRequestFactory
factory = APIRequestFactory()
-def null_view(request):
- pass
+def mock_view(request):
+ return HttpResponse('')
+
+
+apppatterns = ([
+ url(r'^home$', mock_view, name='home'),
+], 'app')
+
+apipatterns = ([
+ url(r'^root$', mock_view, name='root'),
+], 'rest_framework')
urlpatterns = [
- url(r'^view$', null_view, name='view'),
+ url(r'^view$', mock_view, name='view'),
+ url(r'^app2/', include(apppatterns, namespace='app2')),
+ url(r'^app1/', include(apppatterns, namespace='app1')),
+ url(r'^api/', include(apipatterns)),
]
@@ -28,7 +41,7 @@ class MockVersioningScheme(object):
if self.raise_error:
raise NoReverseMatch()
- return 'http://scheme-reversed/view'
+ return 'http://scheme-reversed/api/root'
@override_settings(ROOT_URLCONF='tests.test_reverse')
@@ -36,21 +49,116 @@ class ReverseTests(TestCase):
"""
Tests for fully qualified URLs when using `reverse`.
"""
+
+ def test_reverse_non_drf_view(self):
+ request = factory.get('/api/root')
+
+ # DRF reverse should not match non-DRF views
+ with self.assertRaises(NoReverseMatch):
+ reverse('view', request=request)
+
def test_reversed_urls_are_fully_qualified(self):
- request = factory.get('/view')
- url = reverse('view', request=request)
- assert url == 'http://testserver/view'
+ request = factory.get('/api/root')
+
+ url = reverse('root', request=request)
+ assert url == 'http://testserver/api/root'
def test_reverse_with_versioning_scheme(self):
- request = factory.get('/view')
+ request = factory.get('/api/root')
request.versioning_scheme = MockVersioningScheme()
- url = reverse('view', request=request)
- assert url == 'http://scheme-reversed/view'
+ url = reverse('root', request=request)
+ assert url == 'http://scheme-reversed/api/root'
def test_reverse_with_versioning_scheme_fallback_to_default_on_error(self):
- request = factory.get('/view')
+ request = factory.get('/api/root')
request.versioning_scheme = MockVersioningScheme(raise_error=True)
- url = reverse('view', request=request)
- assert url == 'http://testserver/view'
+ url = reverse('root', request=request)
+ assert url == 'http://testserver/api/root'
+
+
+@override_settings(ROOT_URLCONF='tests.test_reverse')
+class NamespaceTests(TestCase):
+ """
+ Ensure reverse can handle namespaces.
+
+ Note: It's necessary to use self.client() here, as the
+ RequestFactory does not setup the resolver_match.
+ """
+
+ def request(self, url):
+ return self.client.get(url).wsgi_request
+
+ def test_default_namespace(self):
+ url = reverse('root')
+ assert url == '/api/root'
+
+ def test_application_namespace(self):
+ url = reverse('app:home')
+ assert url == '/app1/home'
+
+ # instance namespace provided by current_app
+ url = reverse('app:home', current_app='app2')
+ assert url == '/app2/home'
+
+ def test_instance_namespace(self):
+ url = reverse('app1:home')
+ assert url == '/app1/home'
+
+ url = reverse('app2:home')
+ assert url == '/app2/home'
+
+ def test_application_namespace_with_request(self):
+ # request's current app affects result
+ request1 = self.request('/app1/home')
+ request2 = self.request('/app2/home')
+
+ # sanity check
+ assert current_app(request1) == 'app1'
+ assert current_app(request2) == 'app2'
+
+ assert reverse('app:home', request=request1) == 'http://testserver/app1/home'
+ assert reverse('app:home', request=request2) == 'http://testserver/app2/home'
+
+ def test_instance_namespace_with_request(self):
+ # request's current app is not relevant
+ request1 = self.request('/app1/home')
+ request2 = self.request('/app2/home')
+
+ # sanity check
+ assert current_app(request1) == 'app1'
+ assert current_app(request2) == 'app2'
+
+ assert reverse('app1:home', request=request1) == 'http://testserver/app1/home'
+ assert reverse('app2:home', request=request1) == 'http://testserver/app2/home'
+ assert reverse('app1:home', request=request2) == 'http://testserver/app1/home'
+ assert reverse('app2:home', request=request2) == 'http://testserver/app2/home'
+
+
+@override_settings(ROOT_URLCONF='tests.test_reverse')
+class CurrentAppTests(TestCase):
+ """
+ Test current_app() function.
+
+ Note: It's necessary to use self.client() here, as the
+ RequestFactory does not setup the resolver_match.
+ """
+
+ def request(self, url):
+ return self.client.get(url).wsgi_request
+
+ def test_no_namespace(self):
+ request = self.request('/view')
+ assert current_app(request) == ''
+
+ def test_namespace(self):
+ request = self.request('/app1/home')
+ assert current_app(request) == 'app1'
+
+ request = self.request('/app2/home')
+ assert current_app(request) == 'app2'
+
+ def test_factory_incompatibility(self):
+ request = factory.get('/app1/home')
+ assert current_app(request) is None
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 36255f48f..e1dafa9bd 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -145,13 +145,17 @@ class TestSimpleRouter(TestCase):
class TestRootView(URLPatternsTestCase, TestCase):
urlpatterns = [
- url(r'^non-namespaced/', include(namespaced_router.urls)),
- url(r'^namespaced/', include((namespaced_router.urls, 'namespaced'), namespace='namespaced')),
+ url(r'^non-namespaced/', include(namespaced_router.urlpatterns)),
+ url(r'^namespaced1/', include(namespaced_router.urlpatterns, namespace='namespaced1')),
+ url(r'^namespaced2/', include(namespaced_router.urlpatterns, namespace='namespaced2')),
]
def test_retrieve_namespaced_root(self):
- response = self.client.get('/namespaced/')
- assert response.data == {"example": "http://testserver/namespaced/example/"}
+ response = self.client.get('/namespaced1/')
+ assert response.data == {"example": "http://testserver/namespaced1/example/"}
+
+ response = self.client.get('/namespaced2/')
+ assert response.data == {"example": "http://testserver/namespaced2/example/"}
def test_retrieve_non_namespaced_root(self):
response = self.client.get('/non-namespaced/')
@@ -163,8 +167,8 @@ class TestCustomLookupFields(URLPatternsTestCase, TestCase):
Ensure that custom lookup fields are correctly routed.
"""
urlpatterns = [
- url(r'^example/', include(notes_router.urls)),
- url(r'^example2/', include(kwarged_notes_router.urls)),
+ url(r'^example/', include(notes_router.urlpatterns)),
+ url(r'^example2/', include(kwarged_notes_router.urlpatterns)),
]
def setUp(self):
@@ -221,8 +225,8 @@ class TestLookupUrlKwargs(URLPatternsTestCase, TestCase):
Setup a deep lookup_field, but map it to a simple URL kwarg.
"""
urlpatterns = [
- url(r'^example/', include(notes_router.urls)),
- url(r'^example2/', include(kwarged_notes_router.urls)),
+ url(r'^example/', include(notes_router.urlpatterns)),
+ url(r'^example2/', include(kwarged_notes_router.urlpatterns)),
]
def setUp(self):
@@ -410,7 +414,7 @@ class TestDynamicListAndDetailRouter(TestCase):
class TestEmptyPrefix(URLPatternsTestCase, TestCase):
urlpatterns = [
- url(r'^empty-prefix/', include(empty_prefix_router.urls)),
+ url(r'^empty-prefix/', include(empty_prefix_router.urlpatterns)),
]
def test_empty_prefix_list(self):
@@ -427,7 +431,7 @@ class TestEmptyPrefix(URLPatternsTestCase, TestCase):
class TestRegexUrlPath(URLPatternsTestCase, TestCase):
urlpatterns = [
- url(r'^regex/', include(regex_url_path_router.urls)),
+ url(r'^regex/', include(regex_url_path_router.urlpatterns)),
]
def test_regex_url_path_list(self):
diff --git a/tests/test_urlconf.py b/tests/test_urlconf.py
new file mode 100644
index 000000000..6d3e02e42
--- /dev/null
+++ b/tests/test_urlconf.py
@@ -0,0 +1,127 @@
+"""
+Test rest framework assumptions about the configuration of the root urlconf.
+"""
+from django.conf.urls import include, url
+
+from rest_framework import routers, viewsets
+from rest_framework.reverse import NoReverseMatch, reverse
+from rest_framework.test import APITestCase, URLPatternsTestCase
+
+
+class MockViewSet(viewsets.ViewSet):
+ def list(self, request, *args, **kwargs):
+ pass
+
+
+router = routers.DefaultRouter(trailing_slash=False)
+router.register(r'example', MockViewSet, base_name='example')
+
+
+class TestUrlsAppNameRequired(APITestCase, URLPatternsTestCase):
+ urlpatterns = [
+ url(r'^api/', include(router.urls)),
+ ]
+
+ def test_reverse(self):
+ """
+ The 'rest_framework' namespace must be present.
+ """
+ with self.assertRaises(NoReverseMatch):
+ reverse('example-list')
+
+
+class TestUrlpatternsAppName(APITestCase, URLPatternsTestCase):
+ urlpatterns = [
+ url(r'^api/', include(router.urlpatterns)),
+ ]
+
+ def test_reverse(self):
+ self.assertEqual(reverse('example-list'), '/api/example')
+
+ def test_app_name_reverse(self):
+ self.assertEqual(reverse('rest_framework:example-list'), '/api/example')
+
+
+class TestUrlpatternsNamespace(APITestCase, URLPatternsTestCase):
+ urlpatterns = [
+ url(r'^api/v1/', include(router.urlpatterns, namespace='v1')),
+ url(r'^api/v2/', include(router.urlpatterns, namespace='v2')),
+ ]
+
+ def test_reverse(self):
+ self.assertEqual(reverse('example-list'), '/api/v2/example')
+
+ def test_app_name_reverse(self):
+ self.assertEqual(reverse('rest_framework:example-list'), '/api/v2/example')
+
+ def test_namespace_reverse(self):
+ self.assertEqual(reverse('v1:example-list'), '/api/v1/example')
+ self.assertEqual(reverse('v2:example-list'), '/api/v2/example')
+
+
+class TestAppUrlpatternsAppName(APITestCase, URLPatternsTestCase):
+ apppatterns = ([
+ url(r'^api/', include(router.urlpatterns)),
+ ], 'api')
+
+ urlpatterns = [
+ url(r'^', include(apppatterns)),
+ ]
+
+ def test_reverse(self):
+ """
+ Nesting the router.urlpatterns in an app with an app_name will
+ break url resolution.
+ """
+ with self.assertRaises(NoReverseMatch):
+ reverse('example-list')
+
+
+class TestAppUrlpatterns(APITestCase, URLPatternsTestCase):
+ apppatterns = ([
+ url(r'^api/', include(router.urlpatterns)),
+ ], None)
+
+ urlpatterns = [
+ url(r'^', include(apppatterns)),
+ ]
+
+ def test_reverse(self):
+ self.assertEqual(reverse('example-list'), '/api/example')
+
+
+class TestAppUrlsAppName(APITestCase, URLPatternsTestCase):
+ apppatterns = ([
+ url(r'^api/', include(router.urls)),
+ ], 'rest_framework')
+
+ urlpatterns = [
+ url(r'^', include(apppatterns)),
+ ]
+
+ def test_reverse(self):
+ self.assertEqual(reverse('example-list'), '/api/example')
+
+ def test_app_name_reverse(self):
+ self.assertEqual(reverse('rest_framework:example-list'), '/api/example')
+
+
+class TestAppUrlsNamespace(APITestCase, URLPatternsTestCase):
+ apppatterns = ([
+ url(r'^', include(router.urls)),
+ ], 'rest_framework')
+
+ urlpatterns = [
+ url(r'^api/v1/', include(apppatterns, namespace='v1')),
+ url(r'^api/v2/', include(apppatterns, namespace='v2')),
+ ]
+
+ def test_reverse(self):
+ self.assertEqual(reverse('example-list'), '/api/v2/example')
+
+ def test_app_name_reverse(self):
+ self.assertEqual(reverse('rest_framework:example-list'), '/api/v2/example')
+
+ def test_namespace_reverse(self):
+ self.assertEqual(reverse('v1:example-list'), '/api/v1/example')
+ self.assertEqual(reverse('v2:example-list'), '/api/v2/example')
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
index 7e650e275..3a19cdaac 100644
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -143,17 +143,21 @@ class TestRequestVersion:
class TestURLReversing(URLPatternsTestCase, APITestCase):
- included = [
+ versioned = [
url(r'^namespaced/$', dummy_view, name='another'),
url(r'^example/(?P\d+)/$', dummy_pk_view, name='example-detail')
]
- urlpatterns = [
- url(r'^v1/', include((included, 'v1'), namespace='v1')),
+ included = [
url(r'^another/$', dummy_view, name='another'),
url(r'^(?P[v1|v2]+)/another/$', dummy_view, name='another'),
]
+ urlpatterns = [
+ url(r'^v1/', include((versioned, 'rest_framework'), namespace='v1')),
+ url(r'^', include((included, 'rest_framework'))),
+ ]
+
def test_reverse_unversioned(self):
view = ReverseView.as_view()
@@ -314,8 +318,8 @@ class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase):
]
urlpatterns = [
- url(r'^v1/', include((included, 'v1'), namespace='v1')),
- url(r'^v2/', include((included, 'v2'), namespace='v2'))
+ url(r'^v1/', include((included, 'rest_framework'), namespace='v1')),
+ url(r'^v2/', include((included, 'rest_framework'), namespace='v2'))
]
def setUp(self):
@@ -344,17 +348,21 @@ class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase,
nested = [
url(r'^namespaced/(?P\d+)/$', dummy_pk_view, name='nested'),
]
- included = [
+ versioned = [
url(r'^namespaced/(?P\d+)/$', dummy_pk_view, name='namespaced'),
url(r'^nested/', include((nested, 'nested-namespace'), namespace='nested-namespace'))
]
- urlpatterns = [
- url(r'^v1/', include((included, 'restframeworkv1'), namespace='v1')),
- url(r'^v2/', include((included, 'restframeworkv2'), namespace='v2')),
+ included = [
url(r'^non-api/(?P\d+)/$', dummy_pk_view, name='non-api-view')
]
+ urlpatterns = [
+ url(r'^v1/', include((versioned, 'rest_framework'), namespace='v1')),
+ url(r'^v2/', include((versioned, 'rest_framework'), namespace='v2')),
+ url(r'^', include((included, 'rest_framework'))),
+ ]
+
def _create_field(self, view_name, version):
request = factory.get("/")
request.versioning_scheme = NamespaceVersioning()
@@ -381,6 +389,7 @@ class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase,
def test_non_api_url_is_properly_reversed_regardless_of_the_version(self):
"""
Regression test for #2711
+ Note: non-api-views still need to be included in the 'rest_framework' namespace
"""
field = self._create_field('non-api-view', 'v1')
assert field.to_representation(PKOnlyObject(10)) == 'http://testserver/non-api/10/'
diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py
index 25feb0f37..30bb03144 100644
--- a/tests/test_viewsets.py
+++ b/tests/test_viewsets.py
@@ -62,7 +62,7 @@ router.register(r'actions-alt', ActionViewSet, base_name='actions-alt')
urlpatterns = [
- url(r'^api/', include(router.urls)),
+ url(r'^api/', include(router.urlpatterns)),
]