This commit is contained in:
Ludwig Kraatz 2012-12-07 01:18:59 -08:00
commit 8c5ab053f4
5 changed files with 149 additions and 15 deletions

View File

@ -314,6 +314,7 @@ By default, `HyperlinkedRelatedField` is read-write, although you can change thi
**Arguments**: **Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**. * `view_name` - The view name that should be used as the target of the relationship. **required**.
* `view_namespace` - The namespace of the view, used as the target of the relationship. The default namespace can be set as HyperlinkedModelSerializerOptions attribute. If not set, it's `None`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. * `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
@ -329,6 +330,7 @@ This field is always read-only.
**Arguments**: **Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**. * `view_name` - The view name that should be used as the target of the relationship. **required**.
* `view_namespace` - The namespace of the view, used as the target of the relationship. The default namespace can be set as HyperlinkedModelSerializerOptions attribute. If not set, it's `None`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. * `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. * `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.

View File

@ -20,6 +20,12 @@ from rest_framework.compat import parse_date, parse_datetime
from rest_framework.compat import timezone from rest_framework.compat import timezone
from urlparse import urlparse from urlparse import urlparse
class Empty(object):
"""
Placeholder for unset attributes.
Cannot use `None`, as that may be a valid value.
"""
pass
def is_simple_callable(obj): def is_simple_callable(obj):
""" """
@ -530,6 +536,8 @@ class HyperlinkedRelatedField(RelatedField):
except: except:
raise ValueError("Hyperlinked field requires 'view_name' kwarg") raise ValueError("Hyperlinked field requires 'view_name' kwarg")
self.view_namespace = kwargs.pop('view_namespace', Empty)
self.slug_field = kwargs.pop('slug_field', self.slug_field) self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
@ -538,6 +546,15 @@ class HyperlinkedRelatedField(RelatedField):
self.format = kwargs.pop('format', None) self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def initialize(self, parent, field_name):
super(HyperlinkedRelatedField, self).initialize(parent, field_name)
if self.view_namespace is Empty:
self.view_namespace = getattr(self.parent.opts, 'view_namespace', None)
if self.view_namespace:
self.view_name = '%(namespace)s:%(name)s' % {'namespace': self.view_namespace, 'name': self.view_name}
def get_slug_field(self): def get_slug_field(self):
""" """
Get the name of a slug field to be used to look up by slug. Get the name of a slug field to be used to look up by slug.
@ -564,13 +581,13 @@ class HyperlinkedRelatedField(RelatedField):
kwargs = {self.slug_url_kwarg: slug} kwargs = {self.slug_url_kwarg: slug}
try: try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format) return reverse(view_name, kwargs=kwargs, request=request, format=format)
except: except:
pass pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try: try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format) return reverse(view_name, kwargs=kwargs, request=request, format=format)
except: except:
pass pass
@ -634,9 +651,13 @@ class HyperlinkedIdentityField(Field):
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# TODO: Make view_name mandatory, and have the try:
# HyperlinkedModelSerializer set it on-the-fly self.view_name = kwargs.pop('view_name')
self.view_name = kwargs.pop('view_name', None) except:
raise ValueError("Hyperlinked Identity field requires 'view_name' kwarg")
self.view_namespace = kwargs.pop('view_namespace', Empty)
self.format = kwargs.pop('format', None) self.format = kwargs.pop('format', None)
self.slug_field = kwargs.pop('slug_field', self.slug_field) self.slug_field = kwargs.pop('slug_field', self.slug_field)
@ -646,10 +667,20 @@ class HyperlinkedIdentityField(Field):
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs) super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def initialize(self, parent, field_name):
super(HyperlinkedIdentityField, self).initialize(parent, field_name)
if self.view_namespace is Empty:
self.view_namespace = getattr(self.parent.opts, 'view_namespace', None)
if self.view_namespace:
self.view_name = '%(namespace)s:%(name)s' % {'namespace': self.view_namespace, 'name': self.view_name}
def field_to_native(self, obj, field_name): def field_to_native(self, obj, field_name):
request = self.context.get('request', None) request = self.context.get('request', None)
format = self.format or self.context.get('format', None) format = self.format or self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name view_name = self.view_name
kwargs = {self.pk_url_kwarg: obj.pk} 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)
@ -663,13 +694,13 @@ class HyperlinkedIdentityField(Field):
kwargs = {self.slug_url_kwarg: slug} kwargs = {self.slug_url_kwarg: slug}
try: try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format) return reverse(view_name, kwargs=kwargs, request=request, format=format)
except: except:
pass pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try: try:
return reverse(self.view_name, kwargs=kwargs, request=request, format=format) return reverse(view_name, kwargs=kwargs, request=request, format=format)
except: except:
pass pass

View File

@ -41,7 +41,7 @@ class GenericAPIView(views.APIView):
if serializer_class is None: if serializer_class is None:
class DefaultSerializer(self.model_serializer_class): class DefaultSerializer(self.model_serializer_class):
class Meta: class Meta(self.model_serializer_class.Meta):
model = self.model model = self.model
serializer_class = DefaultSerializer serializer_class = DefaultSerializer

View File

@ -5,6 +5,7 @@ from decimal import Decimal
from django.db import models from django.db import models
from django.forms import widgets from django.forms import widgets
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.core.exceptions import ImproperlyConfigured
from rest_framework.compat import get_concrete_model from rest_framework.compat import get_concrete_model
# Note: We do the following so that users of the framework can use this style: # Note: We do the following so that users of the framework can use this style:
@ -76,10 +77,34 @@ def _get_declared_fields(bases, attrs):
return SortedDict(fields) return SortedDict(fields)
def _get_options_instance(bases, attrs):
options_class = Meta = None
if '_options_class' in attrs:
options_class = attrs['_options_class']
else:
for base in bases:
if hasattr(base, '_options_class'):
options_class = getattr(base, '_options_class')
break
if options_class is None:
raise ImproperlyConfigured, 'A Serializer requires an "_options_class" attribute'
if 'Meta' in attrs:
Meta = attrs['Meta']
else:
for base in bases:
if hasattr(base, 'Meta'):
Meta = getattr(base, 'Meta')
break
if Meta is None:
raise ImproperlyConfigured, 'A Serializer requires an "Meta" attribute'
return options_class(Meta)
class SerializerMetaclass(type): class SerializerMetaclass(type):
def __new__(cls, name, bases, attrs): def __new__(cls, name, bases, attrs):
attrs['base_fields'] = _get_declared_fields(bases, attrs) attrs['base_fields'] = _get_declared_fields(bases, attrs)
attrs['opts'] = _get_options_instance(bases, attrs)
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
@ -102,7 +127,6 @@ class BaseSerializer(Field):
def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs): def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs):
super(BaseSerializer, self).__init__(**kwargs) super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta)
self.parent = None self.parent = None
self.root = None self.root = None
self.partial = partial self.partial = partial
@ -512,6 +536,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
def __init__(self, meta): def __init__(self, meta):
super(HyperlinkedModelSerializerOptions, self).__init__(meta) super(HyperlinkedModelSerializerOptions, self).__init__(meta)
self.view_name = getattr(meta, 'view_name', None) self.view_name = getattr(meta, 'view_name', None)
self.view_namespace = getattr(meta, 'view_namespace', None)
class HyperlinkedModelSerializer(ModelSerializer): class HyperlinkedModelSerializer(ModelSerializer):
@ -520,13 +545,15 @@ class HyperlinkedModelSerializer(ModelSerializer):
_options_class = HyperlinkedModelSerializerOptions _options_class = HyperlinkedModelSerializerOptions
_default_view_name = '%(model_name)s-detail' _default_view_name = '%(model_name)s-detail'
url = HyperlinkedIdentityField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs)
if self.opts.view_name is None: if self.opts.view_name is None:
self.opts.view_name = self._get_default_view_name(self.opts.model) self.opts.view_name = self._get_default_view_name(self.opts.model)
if not 'url' in self.base_fields:
self.base_fields.insert(0, 'url', HyperlinkedIdentityField(view_name=self.opts.view_name, view_namespace=self.opts.view_namespace))
super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs)
def _get_default_view_name(self, model): def _get_default_view_name(self, model):
""" """
Return the view name to use if 'view_name' is not specified in 'Meta' Return the view name to use if 'view_name' is not specified in 'Meta'
@ -551,7 +578,9 @@ class HyperlinkedModelSerializer(ModelSerializer):
queryset = rel._default_manager queryset = rel._default_manager
kwargs = { kwargs = {
'queryset': queryset, 'queryset': queryset,
'view_name': self._get_default_view_name(rel) 'view_name': self._get_default_view_name(rel),
# TODO? offer possibility to init related fields with custom namespaces
'view_namespace': getattr(self.opts, 'view_namespace', None)
} }
if to_many: if to_many:
return ManyHyperlinkedRelatedField(**kwargs) return ManyHyperlinkedRelatedField(**kwargs)

View File

@ -1,4 +1,4 @@
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from rest_framework import generics, status, serializers from rest_framework import generics, status, serializers
@ -16,6 +16,16 @@ class BlogPostCommentSerializer(serializers.ModelSerializer):
model = BlogPostComment model = BlogPostComment
fields = ('text', 'blog_post_url', 'url') fields = ('text', 'blog_post_url', 'url')
class NamespacedBlogPostCommentSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
text = serializers.CharField()
blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail', view_namespace = None)
class Meta:
model = BlogPostComment
view_namespace = 'namespacetests'
fields = ('text', 'blog_post_url', 'url')
class PhotoSerializer(serializers.Serializer): class PhotoSerializer(serializers.Serializer):
description = serializers.CharField() description = serializers.CharField()
@ -49,6 +59,13 @@ class ManyToManyDetail(generics.RetrieveAPIView):
model = ManyToManyModel model = ManyToManyModel
model_serializer_class = serializers.HyperlinkedModelSerializer model_serializer_class = serializers.HyperlinkedModelSerializer
class NamespacedManyToManySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
view_namespace = 'namespacetests'
class NamespacedManyToManyDetail(generics.RetrieveAPIView):
model = ManyToManyModel
model_serializer_class = NamespacedManyToManySerializer
class BlogPostCommentListCreate(generics.ListCreateAPIView): class BlogPostCommentListCreate(generics.ListCreateAPIView):
model = BlogPostComment model = BlogPostComment
@ -58,6 +75,14 @@ class BlogPostCommentDetail(generics.RetrieveAPIView):
model = BlogPostComment model = BlogPostComment
serializer_class = BlogPostCommentSerializer serializer_class = BlogPostCommentSerializer
class NamespacedBlogPostCommentListCreate(generics.ListCreateAPIView):
model = BlogPostComment
serializer_class = NamespacedBlogPostCommentSerializer
class NamespacedBlogPostCommentDetail(generics.RetrieveAPIView):
model = BlogPostComment
serializer_class = NamespacedBlogPostCommentSerializer
class BlogPostDetail(generics.RetrieveAPIView): class BlogPostDetail(generics.RetrieveAPIView):
model = BlogPost model = BlogPost
@ -76,7 +101,15 @@ class OptionalRelationDetail(generics.RetrieveAPIView):
model_serializer_class = serializers.HyperlinkedModelSerializer model_serializer_class = serializers.HyperlinkedModelSerializer
anchor_urls = patterns('',
url(r'^(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
url(r'^manytomany/(?P<pk>\d+)/$', NamespacedManyToManyDetail.as_view(), name='manytomanymodel-detail'),
url(r'^comments/$', NamespacedBlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
url(r'^comments/(?P<pk>\d+)/$', NamespacedBlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
)
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^other/namespace/', include(anchor_urls, namespace='namespacetests', app_name='namespacetests')),
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'),
@ -156,6 +189,15 @@ class TestManyToManyHyperlinkedView(TestCase):
}] }]
self.list_view = ManyToManyList.as_view() self.list_view = ManyToManyList.as_view()
self.detail_view = ManyToManyDetail.as_view() self.detail_view = ManyToManyDetail.as_view()
self.namespaced_data = [{
'url': 'http://testserver/other/namespace/manytomany/1/',
'rel': [
'http://testserver/other/namespace/1/',
'http://testserver/other/namespace/2/',
'http://testserver/other/namespace/3/',
]
}]
self.namespaced_detail_view = NamespacedManyToManyDetail.as_view()
def test_get_list_view(self): def test_get_list_view(self):
""" """
@ -175,6 +217,15 @@ class TestManyToManyHyperlinkedView(TestCase):
self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data[0]) self.assertEquals(response.data, self.data[0])
def test_get_detail_namespaced_view(self):
"""
GET requests to a View in a namespace should succeed.
"""
request = factory.get('/other/namespace/manytomany/1/')
response = self.namespaced_detail_view(request, pk=1).render()
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.namespaced_data[0])
class TestCreateWithForeignKeys(TestCase): class TestCreateWithForeignKeys(TestCase):
urls = 'rest_framework.tests.hyperlinkedserializers' urls = 'rest_framework.tests.hyperlinkedserializers'
@ -185,6 +236,7 @@ class TestCreateWithForeignKeys(TestCase):
""" """
self.post = BlogPost.objects.create(title="Test post") self.post = BlogPost.objects.create(title="Test post")
self.create_view = BlogPostCommentListCreate.as_view() self.create_view = BlogPostCommentListCreate.as_view()
self.create_namespaced_view = NamespacedBlogPostCommentListCreate.as_view()
def test_create_comment(self): def test_create_comment(self):
@ -200,6 +252,26 @@ class TestCreateWithForeignKeys(TestCase):
self.assertEqual(self.post.blogpostcomment_set.count(), 1) self.assertEqual(self.post.blogpostcomment_set.count(), 1)
self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment') self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
def test_create_namespaced_comment(self):
data = {
'text': 'A test comment',
'blog_post_url': 'http://testserver/posts/1/'
}
response_data = {
'url': 'http://testserver/other/namespace/comments/1/',
'text': 'A test comment',
'blog_post_url': 'http://testserver/posts/1/'
}
request = factory.post('/other/namespace/comments/', data=data)
response = self.create_namespaced_view(request).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response['Location'], 'http://testserver/other/namespace/comments/1/', msg='Not specified Namespace should be inherited from parents Options')
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
self.assertEquals(response.data, response_data)
class TestCreateWithForeignKeysAndCustomSlug(TestCase): class TestCreateWithForeignKeysAndCustomSlug(TestCase):
urls = 'rest_framework.tests.hyperlinkedserializers' urls = 'rest_framework.tests.hyperlinkedserializers'