Repository: encode/django-rest-framework

Issue:
format_suffix_patterns applies them after trailing slash.
According to http://www.django-rest-framework.org/api-guide/format-suffixes the suffices should be used directly after the resource name like http://example.com/api/users.json but format_suffix_patterns applies them after the trailing slash if one is used in the urlpatterns it gets (like the DefaultRouter does)

This results in 404 when trying to use them in the tutorial app for example: http://restframework.herokuapp.com/users.json



Discussion:
Agree that that docs are misleading as to the current behavior there, but not sure if that's a docs fix, or if we need to change the default behavior.

Assuming this is a duplicate of #1319, but not clear to me that it really was properly addressed, or if #1333 (available in the upcoming 2.4.0) only addressed a subset of the problem.

DjangoCon sprint guidance - review current behaviour, summarise what you would expect desired behaviour to be.

My expectations for the behavior would be the following:

```
# should work
example.com/test.json

# should NOT work
example.com/test/.json
```

I have mixed thoughts on the following:
example.com/test.json/

I kind of like the idea of requiring the specification of a data format to be the very end of an address.  This would mean not allowing a trailing slash.  However, others may have use cases that would disagree with this approach.  

Our group specifies content type via the HTTP Accept Header; however, I agree with the support for specifying the data format via an address.  

Lastly, I think the documentation should at least be updated to include samples as to which addresses are expected to work based on the usage of the format_suffix_patterns functionality.  

Seems reasonable.

I've always found that `comments/.json` is just plain weird and confusing.

Expected for me is:

```
http://localhost:8000/comments.json

# perhaps this too
http://localhost:8000/comments.json/
http://localhost:8000/comments/?format=json
```

If behavior doesn't change for now, updating docs might be a good idea.

I agree with this. This is coming from someone completely new to the framework and going through the tutorial, so take that as you wish. However, the tutorial even gives the example that using `format_suffix_patterns` should make it so the API `will be able to handle URLs such as http://example.com/api/items/4.json`

See here: http://www.django-rest-framework.org/tutorial/2-requests-and-responses/#adding-optional-format-suffixes-to-our-urls

Edit: The example HTTP requests later on the same page show the proper request with the trailing forward slash. So maybe this should just be a doc change higher up. However, I would still agree that without a forward slash makes more sense.


Resolving patch:
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -21,7 +21,7 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required):
 
         else:
             # Regular URL pattern
-            regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern
+            regex = urlpattern.regex.pattern.rstrip('$').rstrip('/') + suffix_pattern
             view = urlpattern._callback or urlpattern._callback_str
             kwargs = urlpattern.default_args
             name = urlpattern.name
@@ -55,8 +55,8 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
             allowed_pattern = allowed[0]
         else:
             allowed_pattern = '(%s)' % '|'.join(allowed)
-        suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern)
+        suffix_pattern = r'\.(?P<%s>%s)/?$' % (suffix_kwarg, allowed_pattern)
     else:
-        suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg
+        suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg
 
     return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)


Test patch:
diff --git a/tests/test_urlpatterns.py b/tests/test_urlpatterns.py
--- a/tests/test_urlpatterns.py
+++ b/tests/test_urlpatterns.py
@@ -1,6 +1,6 @@
 from __future__ import unicode_literals
 from collections import namedtuple
-from django.conf.urls import patterns, url, include
+from django.conf.urls import url, include
 from django.core import urlresolvers
 from django.test import TestCase
 from rest_framework.test import APIRequestFactory
@@ -17,7 +17,8 @@ def dummy_view(request, *args, **kwargs):
 
 class FormatSuffixTests(TestCase):
     """
-    Tests `format_suffix_patterns` against different URLPatterns to ensure the URLs still resolve properly, including any captured parameters.
+    Tests `format_suffix_patterns` against different URLPatterns to ensure the
+    URLs still resolve properly, including any captured parameters.
     """
     def _resolve_urlpatterns(self, urlpatterns, test_paths):
         factory = APIRequestFactory()
@@ -35,11 +36,37 @@ def _resolve_urlpatterns(self, urlpatterns, test_paths):
             self.assertEqual(callback_args, test_path.args)
             self.assertEqual(callback_kwargs, test_path.kwargs)
 
+    def test_trailing_slash(self):
+        factory = APIRequestFactory()
+        urlpatterns = format_suffix_patterns([
+            url(r'^test/$', dummy_view),
+        ])
+        resolver = urlresolvers.RegexURLResolver(r'^/', urlpatterns)
+
+        test_paths = [
+            (URLTestPath('/test.api', (), {'format': 'api'}), True),
+            (URLTestPath('/test/.api', (), {'format': 'api'}), False),
+            (URLTestPath('/test.api/', (), {'format': 'api'}), True),
+        ]
+
+        for test_path, expected_resolved in test_paths:
+            request = factory.get(test_path.path)
+            try:
+                callback, callback_args, callback_kwargs = resolver.resolve(request.path_info)
+            except urlresolvers.Resolver404:
+                callback, callback_args, callback_kwargs = (None, None, None)
+            if not expected_resolved:
+                assert callback is None
+                continue
+
+            print(test_path, callback, callback_args, callback_kwargs)
+            assert callback_args == test_path.args
+            assert callback_kwargs == test_path.kwargs
+
     def test_format_suffix(self):
-        urlpatterns = patterns(
-            '',
+        urlpatterns = [
             url(r'^test$', dummy_view),
-        )
+        ]
         test_paths = [
             URLTestPath('/test', (), {}),
             URLTestPath('/test.api', (), {'format': 'api'}),
@@ -48,10 +75,9 @@ def test_format_suffix(self):
         self._resolve_urlpatterns(urlpatterns, test_paths)
 
     def test_default_args(self):
-        urlpatterns = patterns(
-            '',
+        urlpatterns = [
             url(r'^test$', dummy_view, {'foo': 'bar'}),
-        )
+        ]
         test_paths = [
             URLTestPath('/test', (), {'foo': 'bar', }),
             URLTestPath('/test.api', (), {'foo': 'bar', 'format': 'api'}),
@@ -60,14 +86,12 @@ def test_default_args(self):
         self._resolve_urlpatterns(urlpatterns, test_paths)
 
     def test_included_urls(self):
-        nested_patterns = patterns(
-            '',
+        nested_patterns = [
             url(r'^path$', dummy_view)
-        )
-        urlpatterns = patterns(
-            '',
+        ]
+        urlpatterns = [
             url(r'^test/', include(nested_patterns), {'foo': 'bar'}),
-        )
+        ]
         test_paths = [
             URLTestPath('/test/path', (), {'foo': 'bar', }),
             URLTestPath('/test/path.api', (), {'foo': 'bar', 'format': 'api'}),