mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-04 04:20:12 +03:00
Merge aa8908fef2
into 822eb39599
This commit is contained in:
commit
9c20c21161
|
@ -358,8 +358,13 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||||
attributes are not configured to correctly match the URL conf.
|
attributes are not configured to correctly match the URL conf.
|
||||||
"""
|
"""
|
||||||
|
view_kwargs = {}
|
||||||
|
if self.context.get('view'):
|
||||||
|
view_kwargs = self.context['view'].kwargs
|
||||||
|
|
||||||
lookup_field = getattr(obj, self.lookup_field)
|
lookup_field = getattr(obj, self.lookup_field)
|
||||||
kwargs = {self.lookup_field: lookup_field}
|
kwargs = dict(view_kwargs, **{self.lookup_field: lookup_field})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
|
@ -369,7 +374,8 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
# Only try pk if it has been explicitly set.
|
# Only try pk if it has been explicitly set.
|
||||||
# Otherwise, the default `lookup_field = 'pk'` has us covered.
|
# Otherwise, the default `lookup_field = 'pk'` has us covered.
|
||||||
pk = obj.pk
|
pk = obj.pk
|
||||||
kwargs = {self.pk_url_kwarg: pk}
|
kwargs = dict(view_kwargs, **{self.pk_url_kwarg: pk})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
|
@ -378,7 +384,8 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
slug = getattr(obj, self.slug_field, None)
|
slug = getattr(obj, self.slug_field, None)
|
||||||
if slug is not None:
|
if slug is not None:
|
||||||
# Only try slug if it corresponds to an attribute on the object.
|
# Only try slug if it corresponds to an attribute on the object.
|
||||||
kwargs = {self.slug_url_kwarg: slug}
|
kwargs = dict(view_kwargs, **{self.slug_url_kwarg: slug})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
|
ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug':
|
if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug':
|
||||||
|
@ -565,12 +572,18 @@ class HyperlinkedIdentityField(Field):
|
||||||
attributes are not configured to correctly match the URL conf.
|
attributes are not configured to correctly match the URL conf.
|
||||||
"""
|
"""
|
||||||
lookup_field = getattr(obj, self.lookup_field, None)
|
lookup_field = getattr(obj, self.lookup_field, None)
|
||||||
kwargs = {self.lookup_field: lookup_field}
|
|
||||||
|
|
||||||
# Handle unsaved object case
|
# Handle unsaved object case
|
||||||
if lookup_field is None:
|
if lookup_field is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
view_kwargs = {}
|
||||||
|
if self.context.get('view'):
|
||||||
|
view_kwargs = self.context['view'].kwargs
|
||||||
|
|
||||||
|
kwargs = dict(view_kwargs, **{self.lookup_field: lookup_field})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
|
@ -579,7 +592,8 @@ class HyperlinkedIdentityField(Field):
|
||||||
if self.pk_url_kwarg != 'pk':
|
if self.pk_url_kwarg != 'pk':
|
||||||
# Only try pk lookup if it has been explicitly set.
|
# Only try pk lookup if it has been explicitly set.
|
||||||
# Otherwise, the default `lookup_field = 'pk'` has us covered.
|
# Otherwise, the default `lookup_field = 'pk'` has us covered.
|
||||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
kwargs = dict(view_kwargs, **{self.pk_url_kwarg: obj.pk})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
|
@ -588,7 +602,8 @@ class HyperlinkedIdentityField(Field):
|
||||||
slug = getattr(obj, self.slug_field, None)
|
slug = getattr(obj, self.slug_field, None)
|
||||||
if slug:
|
if slug:
|
||||||
# Only use slug lookup if a slug field exists on the model
|
# Only use slug lookup if a slug field exists on the model
|
||||||
kwargs = {self.slug_url_kwarg: slug}
|
kwargs = dict(view_kwargs, **{self.slug_url_kwarg: slug})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.core.urlresolvers import reverse as django_reverse
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
|
|
||||||
|
|
||||||
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
def reverse(viewname, args=None, kwargs=None, request=None, format=None):
|
||||||
"""
|
"""
|
||||||
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
|
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
|
||||||
and returns a fully qualified URL, using the request to get the base URL.
|
and returns a fully qualified URL, using the request to get the base URL.
|
||||||
|
@ -14,7 +14,8 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
|
||||||
if format is not None:
|
if format is not None:
|
||||||
kwargs = kwargs or {}
|
kwargs = kwargs or {}
|
||||||
kwargs['format'] = format
|
kwargs['format'] = format
|
||||||
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
|
|
||||||
|
url = django_reverse(viewname, args=args, kwargs=kwargs)
|
||||||
if request:
|
if request:
|
||||||
return request.build_absolute_uri(url)
|
return request.build_absolute_uri(url)
|
||||||
return url
|
return url
|
||||||
|
|
|
@ -254,10 +254,10 @@ class DefaultRouter(SimpleRouter):
|
||||||
class APIRoot(views.APIView):
|
class APIRoot(views.APIView):
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None, *args, **kwargs):
|
||||||
ret = {}
|
ret = {}
|
||||||
for key, url_name in api_root_dict.items():
|
for key, url_name in api_root_dict.items():
|
||||||
ret[key] = reverse(url_name, request=request, format=format)
|
ret[key] = reverse(url_name, request=request, format=format, kwargs=kwargs)
|
||||||
return Response(ret)
|
return Response(ret)
|
||||||
|
|
||||||
return APIRoot.as_view()
|
return APIRoot.as_view()
|
||||||
|
|
|
@ -98,12 +98,16 @@ urlpatterns = patterns('',
|
||||||
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
|
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
|
||||||
url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
|
url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
|
||||||
url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
|
url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
|
||||||
|
url(r'^(?P<scope>\w[\w-]*)/anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
|
||||||
url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
|
url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
|
||||||
url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
||||||
|
url(r'^(?P<scope>\w[\w-]*)/manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
|
||||||
|
url(r'^(?P<scope>\w[\w-]*)/manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
||||||
url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
|
url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
|
||||||
url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
|
url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
|
||||||
url(r'^comments/(?P<pk>\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
|
url(r'^comments/(?P<pk>\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
|
||||||
url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
||||||
|
url(r'^(?P<scope>\w[\w-]*)/albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
||||||
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
|
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
|
||||||
url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
|
url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
|
||||||
)
|
)
|
||||||
|
@ -172,6 +176,15 @@ class TestManyToManyHyperlinkedView(TestCase):
|
||||||
'http://testserver/anchor/3/',
|
'http://testserver/anchor/3/',
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
self.data_scoped = [{
|
||||||
|
'url': 'http://testserver/scope/manytomany/1/',
|
||||||
|
'rel': [
|
||||||
|
'http://testserver/scope/anchor/1/',
|
||||||
|
'http://testserver/scope/anchor/2/',
|
||||||
|
'http://testserver/scope/anchor/3/',
|
||||||
|
]
|
||||||
|
}]
|
||||||
self.list_view = ManyToManyList.as_view()
|
self.list_view = ManyToManyList.as_view()
|
||||||
self.detail_view = ManyToManyDetail.as_view()
|
self.detail_view = ManyToManyDetail.as_view()
|
||||||
|
|
||||||
|
@ -184,6 +197,16 @@ class TestManyToManyHyperlinkedView(TestCase):
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, self.data)
|
self.assertEqual(response.data, self.data)
|
||||||
|
|
||||||
|
def test_get_list_view_scoped(self):
|
||||||
|
"""
|
||||||
|
Scoped GET requests to ListCreateAPIView should return list of objects
|
||||||
|
with scoped url properties.
|
||||||
|
"""
|
||||||
|
request = factory.get('/scope/manytomany/')
|
||||||
|
response = self.list_view(request, scope='scope')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, self.data_scoped)
|
||||||
|
|
||||||
def test_get_detail_view(self):
|
def test_get_detail_view(self):
|
||||||
"""
|
"""
|
||||||
GET requests to ListCreateAPIView should return list of objects.
|
GET requests to ListCreateAPIView should return list of objects.
|
||||||
|
@ -193,6 +216,16 @@ class TestManyToManyHyperlinkedView(TestCase):
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, self.data[0])
|
self.assertEqual(response.data, self.data[0])
|
||||||
|
|
||||||
|
def test_get_detail_view_scoped(self):
|
||||||
|
"""
|
||||||
|
Scoped GET requests to ListCreateAPIView should return list of objects
|
||||||
|
with scoped url properties
|
||||||
|
"""
|
||||||
|
request = factory.get('/scope/manytomany/1/')
|
||||||
|
response = self.detail_view(request, pk=1, scope='scope')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, self.data_scoped[0])
|
||||||
|
|
||||||
|
|
||||||
class TestHyperlinkedIdentityFieldLookup(TestCase):
|
class TestHyperlinkedIdentityFieldLookup(TestCase):
|
||||||
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||||
|
@ -211,6 +244,11 @@ class TestHyperlinkedIdentityFieldLookup(TestCase):
|
||||||
'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
|
'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
|
||||||
'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
|
'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
|
||||||
}
|
}
|
||||||
|
self.data_scoped = {
|
||||||
|
'foo': {'title': 'foo', 'url': 'http://testserver/scope/albums/foo/'},
|
||||||
|
'bar': {'title': 'bar', 'url': 'http://testserver/can-be/albums/bar/'},
|
||||||
|
'baz': {'title': 'baz', 'url': 'http://testserver/random/albums/baz/'}
|
||||||
|
}
|
||||||
|
|
||||||
def test_lookup_field(self):
|
def test_lookup_field(self):
|
||||||
"""
|
"""
|
||||||
|
@ -223,6 +261,13 @@ class TestHyperlinkedIdentityFieldLookup(TestCase):
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, self.data[album.title])
|
self.assertEqual(response.data, self.data[album.title])
|
||||||
|
|
||||||
|
def test_lookup_field_scoped(self):
|
||||||
|
for album, scope in zip(Album.objects.all(),('scope', 'can-be', 'random')):
|
||||||
|
request = factory.get('/{0}/albums/{1}/'.format(scope, album.title))
|
||||||
|
response = self.detail_view(request, title=album.title, scope=scope)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, self.data_scoped[album.title])
|
||||||
|
|
||||||
|
|
||||||
class TestCreateWithForeignKeys(TestCase):
|
class TestCreateWithForeignKeys(TestCase):
|
||||||
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from rest_framework import serializers, viewsets, permissions
|
from rest_framework import serializers, viewsets, permissions, status
|
||||||
from rest_framework.compat import include, patterns, url
|
from rest_framework.compat import include, patterns, url
|
||||||
from rest_framework.decorators import link, action
|
from rest_framework.decorators import link, action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -164,6 +164,26 @@ class TestNameableRoot(TestCase):
|
||||||
expected = 'nameable-root'
|
expected = 'nameable-root'
|
||||||
self.assertEqual(expected, self.urls[0].name)
|
self.assertEqual(expected, self.urls[0].name)
|
||||||
|
|
||||||
|
class TestScopedRoot(TestCase):
|
||||||
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
model = RouterTestModel
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'notes', NoteViewSet)
|
||||||
|
|
||||||
|
urls = patterns(
|
||||||
|
'',
|
||||||
|
url(r'^(?P<scope>\w[\w-]*)/', include(router.urls)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.view = self.router.get_api_root_view()
|
||||||
|
|
||||||
|
def test_api_root_is_accessible(self):
|
||||||
|
request = factory.get('/scope/') # get the api-root
|
||||||
|
response = self.view(request, scope='scope')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class TestActionKeywordArgs(TestCase):
|
class TestActionKeywordArgs(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user