alanjds / drf-nested-routers

Nested Routers for Django Rest Framework
https://pypi.org/project/drf-nested-routers/
Apache License 2.0
1.66k stars 157 forks source link

Support for DRF 3.15 nested SimpleRouter.use_regex_path = False #342

Open Gibsondz opened 4 months ago

Gibsondz commented 4 months ago

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))
AlexeyBurkov commented 2 months ago

@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.