Skip to content

Commit 6de12e5

Browse files
axnsan12carltongibson
authored andcommitted
Fix format_suffix_patterns behavior with Django 2 path() routes (#5691)
* Add failing test for #5672 * Add get_original_route to complement get_regex_pattern * [WIP] Fix path handling * needs more tests * maybe needs some refactoring * Add django 2 variant for all tests and fix trailing slash bug * Add more combinations to mixed path test
1 parent cf3929d commit 6de12e5

File tree

4 files changed

+247
-39
lines changed

4 files changed

+247
-39
lines changed

rest_framework/compat.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
)
3030

3131

32-
def get_regex_pattern(urlpattern):
32+
def get_original_route(urlpattern):
33+
"""
34+
Get the original route/regex that was typed in by the user into the path(), re_path() or url() directive. This
35+
is in contrast with get_regex_pattern below, which for RoutePattern returns the raw regex generated from the path().
36+
"""
3337
if hasattr(urlpattern, 'pattern'):
3438
# Django 2.0
3539
return str(urlpattern.pattern)
@@ -38,6 +42,29 @@ def get_regex_pattern(urlpattern):
3842
return urlpattern.regex.pattern
3943

4044

45+
def get_regex_pattern(urlpattern):
46+
"""
47+
Get the raw regex out of the urlpattern's RegexPattern or RoutePattern. This is always a regular expression,
48+
unlike get_original_route above.
49+
"""
50+
if hasattr(urlpattern, 'pattern'):
51+
# Django 2.0
52+
return urlpattern.pattern.regex.pattern
53+
else:
54+
# Django < 2.0
55+
return urlpattern.regex.pattern
56+
57+
58+
def is_route_pattern(urlpattern):
59+
if hasattr(urlpattern, 'pattern'):
60+
# Django 2.0
61+
from django.urls.resolvers import RoutePattern
62+
return isinstance(urlpattern.pattern, RoutePattern)
63+
else:
64+
# Django < 2.0
65+
return False
66+
67+
4168
def make_url_resolver(regex, urlpatterns):
4269
try:
4370
# Django 2.0
@@ -257,10 +284,11 @@ def md_filter_add_syntax_highlight(md):
257284

258285
# Django 1.x url routing syntax. Remove when dropping Django 1.11 support.
259286
try:
260-
from django.urls import include, path, re_path # noqa
287+
from django.urls import include, path, re_path, register_converter # noqa
261288
except ImportError:
262289
from django.conf.urls import include, url # noqa
263290
path = None
291+
register_converter = None
264292
re_path = url
265293

266294

rest_framework/schemas/generators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from rest_framework import exceptions
1818
from rest_framework.compat import (
19-
URLPattern, URLResolver, coreapi, coreschema, get_regex_pattern
19+
URLPattern, URLResolver, coreapi, coreschema, get_original_route
2020
)
2121
from rest_framework.request import clone_request
2222
from rest_framework.settings import api_settings
@@ -170,7 +170,7 @@ def get_api_endpoints(self, patterns=None, prefix=''):
170170
api_endpoints = []
171171

172172
for pattern in patterns:
173-
path_regex = prefix + get_regex_pattern(pattern)
173+
path_regex = prefix + get_original_route(pattern)
174174
if isinstance(pattern, URLPattern):
175175
path = self.get_path_from_regex(path_regex)
176176
callback = pattern.callback

rest_framework/urlpatterns.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,39 @@
22

33
from django.conf.urls import include, url
44

5-
from rest_framework.compat import URLResolver, get_regex_pattern
5+
from rest_framework.compat import (
6+
URLResolver, get_regex_pattern, is_route_pattern, path, register_converter
7+
)
68
from rest_framework.settings import api_settings
79

810

9-
def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required):
11+
def _get_format_path_converter(suffix_kwarg, allowed):
12+
if allowed:
13+
if len(allowed) == 1:
14+
allowed_pattern = allowed[0]
15+
else:
16+
allowed_pattern = '(?:%s)' % '|'.join(allowed)
17+
suffix_pattern = r"\.%s/?" % allowed_pattern
18+
else:
19+
suffix_pattern = r"\.[a-z0-9]+/?"
20+
21+
class FormatSuffixConverter:
22+
regex = suffix_pattern
23+
24+
def to_python(self, value):
25+
return value.strip('./')
26+
27+
def to_url(self, value):
28+
return '.' + value + '/'
29+
30+
converter_name = 'drf_format_suffix'
31+
if allowed:
32+
converter_name += '_' + '_'.join(allowed)
33+
34+
return converter_name, FormatSuffixConverter
35+
36+
37+
def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_route=None):
1038
ret = []
1139
for urlpattern in urlpatterns:
1240
if isinstance(urlpattern, URLResolver):
@@ -18,8 +46,18 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required):
1846
# Add in the included patterns, after applying the suffixes
1947
patterns = apply_suffix_patterns(urlpattern.url_patterns,
2048
suffix_pattern,
21-
suffix_required)
22-
ret.append(url(regex, include((patterns, app_name), namespace), kwargs))
49+
suffix_required,
50+
suffix_route)
51+
52+
# if the original pattern was a RoutePattern we need to preserve it
53+
if is_route_pattern(urlpattern):
54+
assert path is not None
55+
route = str(urlpattern.pattern)
56+
new_pattern = path(route, include((patterns, app_name), namespace), kwargs)
57+
else:
58+
new_pattern = url(regex, include((patterns, app_name), namespace), kwargs)
59+
60+
ret.append(new_pattern)
2361
else:
2462
# Regular URL pattern
2563
regex = get_regex_pattern(urlpattern).rstrip('$').rstrip('/') + suffix_pattern
@@ -29,7 +67,17 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required):
2967
# Add in both the existing and the new urlpattern
3068
if not suffix_required:
3169
ret.append(urlpattern)
32-
ret.append(url(regex, view, kwargs, name))
70+
71+
# if the original pattern was a RoutePattern we need to preserve it
72+
if is_route_pattern(urlpattern):
73+
assert path is not None
74+
assert suffix_route is not None
75+
route = str(urlpattern.pattern).rstrip('$').rstrip('/') + suffix_route
76+
new_pattern = path(route, view, kwargs, name)
77+
else:
78+
new_pattern = url(regex, view, kwargs, name)
79+
80+
ret.append(new_pattern)
3381

3482
return ret
3583

@@ -60,4 +108,12 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
60108
else:
61109
suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg
62110

63-
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
111+
if path and register_converter:
112+
converter_name, suffix_converter = _get_format_path_converter(suffix_kwarg, allowed)
113+
register_converter(suffix_converter, converter_name)
114+
115+
suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg)
116+
else:
117+
suffix_route = None
118+
119+
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_route)

0 commit comments

Comments
 (0)