Repository: encode/django-rest-framework

Issue:
Nested NamespaceVersioning
## Description

This both fixes the misleading error message for `NamespaceVersioning` and adds proper (at least according to me) nested namespaces handling.

https://github.com/tomchristie/django-rest-framework/issues/4009



Discussion:
:+1: 

This breaks my URLs scheme that have another namespace before the versions one, as seem:

``` python
# urls.py (root)
urlpatterns = [
    url(r'^api/', include('api.urls', namespace='api')),
]
```

``` python
# api/urls.py (app)
api_v1_patterns = [
    url(r'^', include(router.urls)),
    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

urlpatterns = [
    url(r'^v1/', include(api_v1_patterns, namespace='v1')),
]
```

Actual master works. With this PR applied it breaks.

...maybe in conjunction with #3816 it works, but I had not tested.

@alanjds it wouldn't work with master if you had any extra namespace _after_ the version, which imho is they way they should be used

I guess I'll have to come up with some idea to handle both before and after cases.


Resolving patch:
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -112,16 +112,19 @@ class NamespaceVersioning(BaseVersioning):
     Host: example.com
     Accept: application/json
     """
-    invalid_version_message = _('Invalid version in URL path.')
+    invalid_version_message = _('Invalid version in URL path. Does not match any version namespace.')
 
     def determine_version(self, request, *args, **kwargs):
         resolver_match = getattr(request, 'resolver_match', None)
         if (resolver_match is None or not resolver_match.namespace):
             return self.default_version
-        version = resolver_match.namespace
-        if not self.is_allowed_version(version):
-            raise exceptions.NotFound(self.invalid_version_message)
-        return version
+
+        # Allow for possibly nested namespaces.
+        possible_versions = resolver_match.namespace.split(':')
+        for version in possible_versions:
+            if self.is_allowed_version(version):
+                return version
+        raise exceptions.NotFound(self.invalid_version_message)
 
     def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
         if request.version is not None:


Test patch:
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -85,6 +85,7 @@ def test_query_param_versioning(self):
         response = view(request)
         assert response.data == {'version': None}
 
+    @override_settings(ALLOWED_HOSTS=['*'])
     def test_host_name_versioning(self):
         scheme = versioning.HostNameVersioning
         view = RequestVersionView.as_view(versioning_class=scheme)
@@ -173,6 +174,7 @@ def test_reverse_query_param_versioning(self):
         response = view(request)
         assert response.data == {'url': 'http://testserver/another/'}
 
+    @override_settings(ALLOWED_HOSTS=['*'])
     def test_reverse_host_name_versioning(self):
         scheme = versioning.HostNameVersioning
         view = ReverseView.as_view(versioning_class=scheme)
@@ -223,6 +225,7 @@ def test_invalid_query_param_versioning(self):
         response = view(request)
         assert response.status_code == status.HTTP_404_NOT_FOUND
 
+    @override_settings(ALLOWED_HOSTS=['*'])
     def test_invalid_host_name_versioning(self):
         scheme = versioning.HostNameVersioning
         view = RequestInvalidVersionView.as_view(versioning_class=scheme)
@@ -293,8 +296,12 @@ def test_bug_2489(self):
 
 
 class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase):
+    nested = [
+        url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='nested'),
+    ]
     included = [
         url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'),
+        url(r'^nested/', include(nested, namespace='nested-namespace'))
     ]
 
     urlpatterns = [
@@ -322,6 +329,10 @@ def test_api_url_is_properly_reversed_with_v2(self):
         field = self._create_field('namespaced', 'v2')
         assert field.to_representation(PKOnlyObject(5)) == 'http://testserver/v2/namespaced/5/'
 
+    def test_api_url_is_properly_reversed_with_nested(self):
+        field = self._create_field('nested', 'v1:nested-namespace')
+        assert field.to_representation(PKOnlyObject(3)) == 'http://testserver/v1/nested/namespaced/3/'
+
     def test_non_api_url_is_properly_reversed_regardless_of_the_version(self):
         """
         Regression test for #2711