Repository: alanjds/drf-nested-routers

Issue:
Support for DRF 3.15 nested SimpleRouter.use_regex_path = False
I know that 3.15 is not part of the the supported versions of DRF yet but I thought this would be useful to bring up. This is my first time doing something like this so please let me know if it's off base.

Ran into this when I was looking to use a custom url converter and attempting to apply the converter to nested URLs using NestedSimpleRouter

When use_regex_path=False on SimpleRouter it will strip the regex ^ and $ characters from the router routes causing the following line to not properly pre-pend the parent_prefix to the route.

**DRF 3.15 regex strip:**
```
        if use_regex_path:
            self._base_pattern = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})'
            self._default_value_pattern = '[^/.]+'
            self._url_conf = re_path
        else:
            self._base_pattern = '<{lookup_value}:{lookup_prefix}{lookup_url_kwarg}>'
            self._default_value_pattern = 'str'
            self._url_conf = path
            # remove regex characters from routes
            _routes = []
            for route in self.routes:
                url_param = route.url
                if url_param[0] == '^':
                    url_param = url_param[1:]
                if url_param[-1] == '$':
                    url_param = url_param[:-1]
```

**NestedMixin's route replacement for regex ^ character** 
`route_contents['url'] = route.url.replace('^', '^' + escaped_parent_regex)`

The '^' does not exist in this case and the router fails silently not pre-pending any parent to the route.

Example to reproduce is to use DRF 3.15 and set up the following urls.py:

```
# urls.py
from rest_framework_nested import routers
from views import DomainViewSet, NameserverViewSet
(...)

router = routers.SimpleRouter(use_regex_path=False)
router.register(r'domains', DomainViewSet, )

domains_router = routers.NestedSimpleRouter(router, r'domains', lookup='domain', use_regex_path=False)
domains_router.register(r'nameservers', NameserverViewSet, basename='domain-nameservers')

urlpatterns = [
    path(r'', include(router.urls)),
    path(r'', include(domains_router.urls)),
]
```

I made quick and dirty fix locally by doing something like below:

```
class NestedMixin(object):
    def __init__(self, parent_router, parent_prefix, *args, **kwargs):
        self.parent_router = parent_router
        self.parent_prefix = parent_prefix
        self.nest_count = getattr(parent_router, 'nest_count', 0) + 1
        self.nest_prefix = kwargs.pop('lookup', 'nested_%i' % self.nest_count) + '_'
        self.use_regex_path = kwargs.get('use_regex_path', True)
```

```
        for route in self.routes:
            route_contents = route._asdict()

            # This will get passed through .format in a little bit, so we need
            # to escape it
            escaped_parent_regex = self.parent_regex.replace('{', '{{').replace('}', '}}')

            if self.use_regex_path:
                route_contents['url'] = route.url.replace('^', '^' + escaped_parent_regex)
            else:
                route_contents['url'] = escaped_parent_regex + route_contents['url']
            nested_routes.append(type(route)(**route_contents))
```

Hints from issue comments:
@AlexeyBurkov: @Gibsondz, thanks a lot.

Faced the same issue.
Solution looks nice enough. Works for me.

I'd recommend forking the repo and creating a pull request with the solution (and mention the issue in it), that would be easier to understand, imho.@alanjds: Thanks @Gibsondz for the contribution and @AlexeyBurkov for the suggestion to convert this to a PR.

Lets have this fixed 👍 @alanjds: Released on PyPI with version 0.94.2: https://pypi.org/project/drf-nested-routers/0.94.2/

Solution patch:
diff --git a/README.md b/README.md
index adbb865d..485075d1 100644
--- a/README.md
+++ b/README.md
@@ -17,11 +17,11 @@ exist without a domain, so you need it "nested" inside the domain.
 
 ## Requirements & Compatibility
 
-- Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13)
+- Python (3.9, 3.10, 3.11, 3.12, 3.13)
 - Django (4.2, 5.0, 5.1)
 - Django REST Framework (3.14, 3.15)
 
-It may work with lower versions, but since the release **0.93.5** is no more tested on CI for Python 3.6 or lower.<br/>
+It may work with lower versions, but since the release **0.94.2** is no more tested on CI for Python 3.8 or lower.<br/>
 And since **0.92.1** is no more tested on CI for Pythons 2.7 to 3.5, Django 1.11 to 2.1 or DRF 3.6 to 3.10.<br/>
 Before that, the release **0.90.2** was tested also with DRF 2.4.3 up to 3.7.
 
diff --git a/requirements.txt b/requirements.txt
index e3a1797a..e746566b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,5 +3,5 @@
 
 # Minimum Django and REST framework version
 Django>=4.2
-djangorestframework>=3.14.0
+djangorestframework>=3.15.0
 
diff --git a/rest_framework_nested/routers.py b/rest_framework_nested/routers.py
index ae826e67..f2ee60b1 100644
--- a/rest_framework_nested/routers.py
+++ b/rest_framework_nested/routers.py
@@ -59,6 +59,7 @@ class NestedMixin:
         self.parent_prefix = parent_prefix
         self.nest_count = getattr(parent_router, 'nest_count', 0) + 1
         self.nest_prefix = kwargs.pop('lookup', f'nested_{self.nest_count}') + '_'
+        self.use_regex_path = kwargs.get('use_regex_path', True)
 
         super().__init__(*args, **kwargs)
 
@@ -111,7 +112,11 @@ class NestedMixin:
             # to escape it
             escaped_parent_regex = self.parent_regex.replace('{', '{{').replace('}', '}}')
 
-            route_contents['url'] = route.url.replace('^', '^' + escaped_parent_regex)
+            if self.use_regex_path:
+                route_contents['url'] = route.url.replace('^', '^' + escaped_parent_regex)
+            else:
+                route_contents['url'] = escaped_parent_regex + route_contents['url']
+
             nested_routes.append(type(route)(**route_contents))
 
         self.routes = nested_routes
diff --git a/setup.py b/setup.py
index 46be47f2..909c2462 100644
--- a/setup.py
+++ b/setup.py
@@ -76,7 +76,7 @@ setup(
     packages=get_packages(package),
     package_data=get_package_data(package),
     install_requires=[
-        'djangorestframework>=3.14.0',
+        'djangorestframework>=3.15.0',
         'Django>=4.2',
     ],
     python_requires=">=3.8",
diff --git a/tox.ini b/tox.ini
index e6d1fe17..81aca510 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,14 +1,13 @@
 [tox]
 # see https://docs.djangoproject.com/en/5.1/faq/install/#what-python-version-can-i-use-with-django
 envlist =
-       py{38,39,310,311,312}-django4.2-drf{3.14,3.15}
+       py{39,310,311,312}-django4.2-drf{3.14,3.15}
        py{310,311,312}-django5.0-drf{3.14,3.15}
        py{310,311,312,313}-django5.1-drf{3.14,3.15}
        py312-mypy
 
 [gh-actions]
 python =
-       3.8: py38
        3.9: py39
        3.10: py310
        3.11: py311


Test patch:
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 51ba983a..ab14558f 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -51,6 +51,11 @@ class AViewSet(ModelViewSet):
     queryset = QS(A)
 
 
+class ANonRegexViewSet(ModelViewSet):
+    model = A
+    queryset = QS(A)
+
+
 class BViewSet(ModelViewSet):
     model = B
     queryset = QS(B)
@@ -90,6 +95,35 @@ class TestNestedSimpleRouter(TestCase):
         self.assertEqual(get_regex_pattern(urls[1]), '^a/(?P<a_pk>[0-9a-f]{32})/b/(?P<b_pk>[^/.]+)/c/(?P<pk>[^/.]+)/$')
 
 
+class TestNonRegexNestedSimpleRouter(TestCase):
+    def setUp(self):
+        self.router = SimpleRouter(use_regex_path=False)
+        self.router.register(r'a', ANonRegexViewSet)
+        self.a_router = NestedSimpleRouter(self.router, r'a', lookup='a', use_regex_path=False)
+        self.a_router.register(r'b', BViewSet)
+        self.b_router = NestedSimpleRouter(self.a_router, r'b', lookup='b', use_regex_path=False)
+        self.b_router.register(r'c', CViewSet)
+
+    def test_non_regex_recursive_nested_simple_routers(self):
+        self.assertFalse(hasattr(self.router, 'parent_regex'))
+        urls = self.router.urls
+
+        self.assertEqual(pattern_from_url(urls[0]), 'a/')
+        self.assertEqual(pattern_from_url(urls[1]), 'a/<str:pk>/')
+
+        self.assertEqual(self.a_router.parent_regex, 'a/<str:a_pk>/')
+        urls = self.a_router.urls
+        self.assertEqual(len(urls), 2)
+        self.assertEqual(pattern_from_url(urls[0]), 'a/<str:a_pk>/b/')
+        self.assertEqual(pattern_from_url(urls[1]), 'a/<str:a_pk>/b/<str:pk>/')
+
+        self.assertEqual(self.b_router.parent_regex, 'a/<str:a_pk>/b/<str:b_pk>/')
+        urls = self.b_router.urls
+        self.assertEqual(len(urls), 2)
+        self.assertEqual(pattern_from_url(urls[0]), 'a/<str:a_pk>/b/<str:b_pk>/c/')
+        self.assertEqual(pattern_from_url(urls[1]), 'a/<str:a_pk>/b/<str:b_pk>/c/<str:pk>/')
+
+
 class TestEmptyPrefix(TestCase):
     def setUp(self):
         self.router = SimpleRouter()