diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b09487f38..cadc2d2b6 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -344,12 +344,6 @@ except ImportError: return datetime.datetime(**kw) -# empty does not exist in 1.3x -if django.VERSION >= (1, 4): - from django.utils.functional import empty -else: - empty = object() - # Markdown is optional try: import markdown diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 969800b00..c1d534046 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -20,6 +20,12 @@ from rest_framework.compat import parse_date, parse_datetime from rest_framework.compat import timezone 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): """ @@ -530,7 +536,7 @@ class HyperlinkedRelatedField(RelatedField): except: raise ValueError("Hyperlinked field requires 'view_name' kwarg") - self.view_namespace = kwargs.pop('view_namespace', None) + self.view_namespace = kwargs.pop('view_namespace', Empty) self.slug_field = kwargs.pop('slug_field', self.slug_field) default_slug_kwarg = self.slug_url_kwarg or self.slug_field @@ -540,6 +546,15 @@ class HyperlinkedRelatedField(RelatedField): self.format = kwargs.pop('format', None) 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): """ Get the name of a slug field to be used to look up by slug. @@ -547,10 +562,7 @@ class HyperlinkedRelatedField(RelatedField): return self.slug_field def to_native(self, obj): - view_namespace = self.view_namespace view_name = self.view_name - if view_namespace: - view_name = '%(namespace)s:%(name)s' % {'namespace': view_namespace, 'name': view_name} request = self.context.get('request', None) format = self.format or self.context.get('format', None) @@ -645,7 +657,7 @@ class HyperlinkedIdentityField(Field): except: raise ValueError("Hyperlinked Identity field requires 'view_name' kwarg") - self.view_namespace = kwargs.pop('view_namespace', None) + self.view_namespace = kwargs.pop('view_namespace', Empty) self.format = kwargs.pop('format', None) @@ -654,15 +666,22 @@ class HyperlinkedIdentityField(Field): self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) - 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): request = self.context.get('request', None) format = self.format or self.context.get('format', None) - view_namespace = self.view_namespace view_name = self.view_name - if view_namespace: - view_name = '%(namespace)s:%(name)s' % {'namespace': view_namespace, 'name': view_name} kwargs = {self.pk_url_kwarg: obj.pk} try: diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 060e3aa0f..0bb9aae52 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -6,7 +6,7 @@ from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict from django.core.exceptions import ImproperlyConfigured -from rest_framework.compat import get_concrete_model, empty +from rest_framework.compat import get_concrete_model # Note: We do the following so that users of the framework can use this style: # @@ -69,7 +69,7 @@ def _get_declared_fields(bases, attrs): return SortedDict(fields) def _get_options_instance(bases, attrs): - options_class = Meta = empty + options_class = Meta = None if '_options_class' in attrs: options_class = attrs['_options_class'] else: @@ -77,7 +77,7 @@ def _get_options_instance(bases, attrs): if hasattr(base, '_options_class'): options_class = getattr(base, '_options_class') break - if options_class is empty: + if options_class is None: raise ImproperlyConfigured, 'A Serializer requires an "_options_class" attribute' if 'Meta' in attrs: @@ -87,7 +87,7 @@ def _get_options_instance(bases, attrs): if hasattr(base, 'Meta'): Meta = getattr(base, 'Meta') break - if Meta is empty: + if Meta is None: raise ImproperlyConfigured, 'A Serializer requires an "Meta" attribute' return options_class(Meta) diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index b91628804..6f97cfb39 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -16,6 +16,16 @@ class BlogPostCommentSerializer(serializers.ModelSerializer): model = BlogPostComment 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): description = serializers.CharField() @@ -65,6 +75,14 @@ class BlogPostCommentDetail(generics.RetrieveAPIView): model = BlogPostComment 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): model = BlogPost @@ -86,6 +104,8 @@ class OptionalRelationDetail(generics.RetrieveAPIView): anchor_urls = patterns('', url(r'^(?P\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), url(r'^manytomany/(?P\d+)/$', NamespacedManyToManyDetail.as_view(), name='manytomanymodel-detail'), + url(r'^comments/$', NamespacedBlogPostCommentListCreate.as_view(), name='blogpostcomment-list'), + url(r'^comments/(?P\d+)/$', NamespacedBlogPostCommentDetail.as_view(), name='blogpostcomment-detail'), ) urlpatterns = patterns('', @@ -216,6 +236,7 @@ class TestCreateWithForeignKeys(TestCase): """ self.post = BlogPost.objects.create(title="Test post") self.create_view = BlogPostCommentListCreate.as_view() + self.create_namespaced_view = NamespacedBlogPostCommentListCreate.as_view() def test_create_comment(self): @@ -231,6 +252,26 @@ class TestCreateWithForeignKeys(TestCase): self.assertEqual(self.post.blogpostcomment_set.count(), 1) 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): urls = 'rest_framework.tests.hyperlinkedserializers'