mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 08:14:16 +03:00
Added trailing_slash argument to routers. Closes #905
This commit is contained in:
parent
ffa27b840f
commit
f1251e8c58
|
@ -66,6 +66,13 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
|
||||||
<tr><td>POST</td><td>@action decorated method</td></tr>
|
<tr><td>POST</td><td>@action decorated method</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
By default the URLs created by `SimpleRouter` are appending with a trailing slash.
|
||||||
|
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
|
||||||
|
|
||||||
|
router = SimpleRouter(trailing_slash=False)
|
||||||
|
|
||||||
|
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
|
||||||
|
|
||||||
## DefaultRouter
|
## DefaultRouter
|
||||||
|
|
||||||
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
|
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
|
||||||
|
@ -83,6 +90,10 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
|
||||||
<tr><td>POST</td><td>@action decorated method</td></tr>
|
<tr><td>POST</td><td>@action decorated method</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
As with `SimpleRouter` the trailing slashs on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
|
||||||
|
|
||||||
|
router = DefaultRouter(trailing_slash=False)
|
||||||
|
|
||||||
# Custom Routers
|
# Custom Routers
|
||||||
|
|
||||||
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are strutured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
|
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are strutured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
|
||||||
|
@ -91,7 +102,7 @@ The simplest way to implement a custom router is to subclass one of the existing
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The following example will only route to the `list` and `retrieve` actions, and unlike the routers included by REST framework, it does not use the trailing slash convention.
|
The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention.
|
||||||
|
|
||||||
class ReadOnlyRouter(SimpleRouter):
|
class ReadOnlyRouter(SimpleRouter):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -301,4 +301,5 @@ td, th {
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-color: white;
|
border-color: white;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ from __future__ import unicode_literals
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework.compat import patterns, url
|
from rest_framework.compat import patterns, url
|
||||||
from rest_framework.decorators import api_view
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
|
@ -72,7 +71,7 @@ class SimpleRouter(BaseRouter):
|
||||||
routes = [
|
routes = [
|
||||||
# List route.
|
# List route.
|
||||||
Route(
|
Route(
|
||||||
url=r'^{prefix}/$',
|
url=r'^{prefix}{trailing_slash}$',
|
||||||
mapping={
|
mapping={
|
||||||
'get': 'list',
|
'get': 'list',
|
||||||
'post': 'create'
|
'post': 'create'
|
||||||
|
@ -82,7 +81,7 @@ class SimpleRouter(BaseRouter):
|
||||||
),
|
),
|
||||||
# Detail route.
|
# Detail route.
|
||||||
Route(
|
Route(
|
||||||
url=r'^{prefix}/{lookup}/$',
|
url=r'^{prefix}/{lookup}{trailing_slash}$',
|
||||||
mapping={
|
mapping={
|
||||||
'get': 'retrieve',
|
'get': 'retrieve',
|
||||||
'put': 'update',
|
'put': 'update',
|
||||||
|
@ -95,7 +94,7 @@ class SimpleRouter(BaseRouter):
|
||||||
# Dynamically generated routes.
|
# Dynamically generated routes.
|
||||||
# Generated using @action or @link decorators on methods of the viewset.
|
# Generated using @action or @link decorators on methods of the viewset.
|
||||||
Route(
|
Route(
|
||||||
url=r'^{prefix}/{lookup}/{methodname}/$',
|
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
|
||||||
mapping={
|
mapping={
|
||||||
'{httpmethod}': '{methodname}',
|
'{httpmethod}': '{methodname}',
|
||||||
},
|
},
|
||||||
|
@ -104,6 +103,10 @@ class SimpleRouter(BaseRouter):
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, trailing_slash=True):
|
||||||
|
self.trailing_slash = trailing_slash and '/' or ''
|
||||||
|
super(SimpleRouter, self).__init__()
|
||||||
|
|
||||||
def get_default_base_name(self, viewset):
|
def get_default_base_name(self, viewset):
|
||||||
"""
|
"""
|
||||||
If `base_name` is not specified, attempt to automatically determine
|
If `base_name` is not specified, attempt to automatically determine
|
||||||
|
@ -193,7 +196,11 @@ class SimpleRouter(BaseRouter):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Build the url pattern
|
# Build the url pattern
|
||||||
regex = route.url.format(prefix=prefix, lookup=lookup)
|
regex = route.url.format(
|
||||||
|
prefix=prefix,
|
||||||
|
lookup=lookup,
|
||||||
|
trailing_slash=self.trailing_slash
|
||||||
|
)
|
||||||
view = viewset.as_view(mapping, **route.initkwargs)
|
view = viewset.as_view(mapping, **route.initkwargs)
|
||||||
name = route.name.format(basename=basename)
|
name = route.name.format(basename=basename)
|
||||||
ret.append(url(regex, view, name=name))
|
ret.append(url(regex, view, name=name))
|
||||||
|
|
|
@ -50,7 +50,7 @@ class TestSimpleRouter(TestCase):
|
||||||
route = decorator_routes[i]
|
route = decorator_routes[i]
|
||||||
# check url listing
|
# check url listing
|
||||||
self.assertEqual(route.url,
|
self.assertEqual(route.url,
|
||||||
'^{{prefix}}/{{lookup}}/{0}/$'.format(endpoint))
|
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
|
||||||
# check method to function mapping
|
# check method to function mapping
|
||||||
if endpoint == 'action3':
|
if endpoint == 'action3':
|
||||||
methods_map = ['post', 'delete']
|
methods_map = ['post', 'delete']
|
||||||
|
@ -103,7 +103,7 @@ class TestCustomLookupFields(TestCase):
|
||||||
|
|
||||||
def test_retrieve_lookup_field_list_view(self):
|
def test_retrieve_lookup_field_list_view(self):
|
||||||
response = self.client.get('/notes/')
|
response = self.client.get('/notes/')
|
||||||
self.assertEquals(response.data,
|
self.assertEqual(response.data,
|
||||||
[{
|
[{
|
||||||
"url": "http://testserver/notes/123/",
|
"url": "http://testserver/notes/123/",
|
||||||
"uuid": "123", "text": "foo bar"
|
"uuid": "123", "text": "foo bar"
|
||||||
|
@ -112,10 +112,39 @@ class TestCustomLookupFields(TestCase):
|
||||||
|
|
||||||
def test_retrieve_lookup_field_detail_view(self):
|
def test_retrieve_lookup_field_detail_view(self):
|
||||||
response = self.client.get('/notes/123/')
|
response = self.client.get('/notes/123/')
|
||||||
self.assertEquals(response.data,
|
self.assertEqual(response.data,
|
||||||
{
|
{
|
||||||
"url": "http://testserver/notes/123/",
|
"url": "http://testserver/notes/123/",
|
||||||
"uuid": "123", "text": "foo bar"
|
"uuid": "123", "text": "foo bar"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTrailingSlash(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
model = RouterTestModel
|
||||||
|
|
||||||
|
self.router = SimpleRouter()
|
||||||
|
self.router.register(r'notes', NoteViewSet)
|
||||||
|
self.urls = self.router.urls
|
||||||
|
|
||||||
|
def test_urls_have_trailing_slash_by_default(self):
|
||||||
|
expected = ['^notes/$', '^notes/(?P<pk>[^/]+)/$']
|
||||||
|
for idx in range(len(expected)):
|
||||||
|
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTrailingSlash(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
model = RouterTestModel
|
||||||
|
|
||||||
|
self.router = SimpleRouter(trailing_slash=False)
|
||||||
|
self.router.register(r'notes', NoteViewSet)
|
||||||
|
self.urls = self.router.urls
|
||||||
|
|
||||||
|
def test_urls_can_have_trailing_slash_removed(self):
|
||||||
|
expected = ['^notes$', '^notes/(?P<pk>[^/]+)$']
|
||||||
|
for idx in range(len(expected)):
|
||||||
|
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user