From acaaa0384cb822a96c7ce19a6d45a730642b09bf Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 13:25:08 +0100 Subject: [PATCH 01/10] HyperlinkedIdentityField now requires View-Name HyperlinkedModelSerializer sets View-Name on the Fly * serializers opt class need to be set by serializers Metaclass --- rest_framework/fields.py | 8 +++++--- rest_framework/serializers.py | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 482a3d485..9aa1d5906 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -634,9 +634,11 @@ class HyperlinkedIdentityField(Field): slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden def __init__(self, *args, **kwargs): - # TODO: Make view_name mandatory, and have the - # HyperlinkedModelSerializer set it on-the-fly - self.view_name = kwargs.pop('view_name', None) + try: + self.view_name = kwargs.pop('view_name') + except: + raise ValueError("Hyperlinked Identity field requires 'view_name' kwarg") + self.format = kwargs.pop('format', None) self.slug_field = kwargs.pop('slug_field', self.slug_field) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4519ab053..80437952f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -5,6 +5,8 @@ from decimal import Decimal from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict +from django.utils.functional import empty +from django.core.exceptions import ImproperlyConfigured from rest_framework.compat import get_concrete_model # Note: We do the following so that users of the framework can use this style: @@ -67,10 +69,34 @@ def _get_declared_fields(bases, attrs): return SortedDict(fields) +def _get_options_instance(bases, attrs): + options_class = Meta = empty + 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 empty: + 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 empty: + raise ImproperlyConfigured, 'A Serializer requires an "Meta" attribute' + + return options_class(Meta) class SerializerMetaclass(type): def __new__(cls, name, 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) @@ -93,7 +119,6 @@ class BaseSerializer(Field): def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs): super(BaseSerializer, self).__init__(**kwargs) - self.opts = self._options_class(self.Meta) self.parent = None self.root = None self.partial = partial @@ -503,12 +528,13 @@ class HyperlinkedModelSerializer(ModelSerializer): _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' - url = HyperlinkedIdentityField() - def __init__(self, *args, **kwargs): - super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs) if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) + + self.base_fields.insert(0, 'url', HyperlinkedIdentityField(view_name=self.opts.view_name)) + + super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs) def _get_default_view_name(self, model): """ From 5a9aa344151a589436d643bfcfc563f09c9a7270 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 13:40:07 +0100 Subject: [PATCH 02/10] compat for django 1.3x fixed empty was introduced Django > 1.3 --- rest_framework/compat.py | 7 +++++++ rest_framework/serializers.py | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 09b763681..b09487f38 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -343,6 +343,13 @@ except ImportError: kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) 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/serializers.py b/rest_framework/serializers.py index 80437952f..80eefbf08 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -5,9 +5,8 @@ from decimal import Decimal from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict -from django.utils.functional import empty from django.core.exceptions import ImproperlyConfigured -from rest_framework.compat import get_concrete_model +from rest_framework.compat import get_concrete_model, empty # Note: We do the following so that users of the framework can use this style: # From 0a8f1d8ca9d01ecdffe158959b9b2aa37e3484ba Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 13:51:13 +0100 Subject: [PATCH 03/10] Hyperlinked Fields now support namespaced views --- rest_framework/fields.py | 22 +++++++++++++++++----- rest_framework/serializers.py | 3 ++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9aa1d5906..a7652db0c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -529,6 +529,8 @@ class HyperlinkedRelatedField(RelatedField): self.view_name = kwargs.pop('view_name') except: raise ValueError("Hyperlinked field requires 'view_name' kwarg") + + self.view_namespace = kwargs.pop('view_namespace', None) self.slug_field = kwargs.pop('slug_field', self.slug_field) default_slug_kwarg = self.slug_url_kwarg or self.slug_field @@ -545,7 +547,11 @@ 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)' % {'namespace': view_namespace, 'name':view_name} + request = self.context.get('request', None) format = self.format or self.context.get('format', None) pk = getattr(obj, 'pk', None) @@ -564,13 +570,13 @@ class HyperlinkedRelatedField(RelatedField): kwargs = {self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(view_name, kwargs=kwargs, request=request, format=format) except: pass kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(view_name, kwargs=kwargs, request=request, format=format) except: pass @@ -639,6 +645,8 @@ class HyperlinkedIdentityField(Field): except: raise ValueError("Hyperlinked Identity field requires 'view_name' kwarg") + self.view_namespace = kwargs.pop('view_namespace', None) + self.format = kwargs.pop('format', None) self.slug_field = kwargs.pop('slug_field', self.slug_field) @@ -651,7 +659,11 @@ class HyperlinkedIdentityField(Field): def field_to_native(self, obj, field_name): request = self.context.get('request', None) format = self.format or self.context.get('format', None) - view_name = self.view_name or self.parent.opts.view_name + view_namespace = self.view_namespace + view_name = self.view_name + if view_namespace: + view_name = '%(namespace)s:%(name)' % {'namespace': view_namespace, 'name':view_name} + kwargs = {self.pk_url_kwarg: obj.pk} try: return reverse(view_name, kwargs=kwargs, request=request, format=format) @@ -665,13 +677,13 @@ class HyperlinkedIdentityField(Field): kwargs = {self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(view_name, kwargs=kwargs, request=request, format=format) except: pass kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(view_name, kwargs=kwargs, request=request, format=format) except: pass diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 80eefbf08..a15b4ef67 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -519,6 +519,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): def __init__(self, meta): super(HyperlinkedModelSerializerOptions, self).__init__(meta) self.view_name = getattr(meta, 'view_name', None) + self.view_namespace = getattr(meta, 'view_namespace', None) class HyperlinkedModelSerializer(ModelSerializer): @@ -531,7 +532,7 @@ class HyperlinkedModelSerializer(ModelSerializer): if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) - self.base_fields.insert(0, 'url', HyperlinkedIdentityField(view_name=self.opts.view_name)) + 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) From 47a1f05ec01147a85cbdde2f3cda8ee3c22d5bc6 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 14:06:01 +0100 Subject: [PATCH 04/10] documentation added and default value from option class The namespace default is now the 'view_namespace' attr of a Serializer Option class --- docs/api-guide/fields.md | 2 ++ rest_framework/fields.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 1d4c34cb9..6bc572fa4 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -314,6 +314,7 @@ By default, `HyperlinkedRelatedField` is read-write, although you can change thi **Arguments**: * `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. * `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'`. @@ -329,6 +330,7 @@ This field is always read-only. **Arguments**: * `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. * `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`. diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a7652db0c..97b60a004 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -547,7 +547,7 @@ class HyperlinkedRelatedField(RelatedField): return self.slug_field def to_native(self, obj): - view_namespace = self.view_namespace + view_namespace = self.view_namespace or getattr(self.parent.opts, 'view_namespace', None) view_name = self.view_name if view_namespace: view_name = '%(namespace)s:%(name)' % {'namespace': view_namespace, 'name':view_name} @@ -659,7 +659,7 @@ class HyperlinkedIdentityField(Field): 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_namespace = self.view_namespace or getattr(self.parent.opts, 'view_namespace', None) view_name = self.view_name if view_namespace: view_name = '%(namespace)s:%(name)' % {'namespace': view_namespace, 'name':view_name} From 28a5b73c250e344d2853869f7710fc383daee8ae Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 15:16:31 +0100 Subject: [PATCH 05/10] test written and some code fixed --- rest_framework/fields.py | 4 +-- rest_framework/generics.py | 2 +- .../tests/hyperlinkedserializers.py | 33 ++++++++++++++++++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 97b60a004..1dd666a3c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -550,7 +550,7 @@ class HyperlinkedRelatedField(RelatedField): view_namespace = self.view_namespace or getattr(self.parent.opts, 'view_namespace', None) view_name = self.view_name if view_namespace: - view_name = '%(namespace)s:%(name)' % {'namespace': view_namespace, 'name':view_name} + 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) @@ -662,7 +662,7 @@ class HyperlinkedIdentityField(Field): view_namespace = self.view_namespace or getattr(self.parent.opts, 'view_namespace', None) view_name = self.view_name if view_namespace: - view_name = '%(namespace)s:%(name)' % {'namespace': view_namespace, 'name':view_name} + 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/generics.py b/rest_framework/generics.py index dd8dfcf8d..736af752b 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -41,7 +41,7 @@ class GenericAPIView(views.APIView): if serializer_class is None: class DefaultSerializer(self.model_serializer_class): - class Meta: + class Meta(self.model_serializer_class.Meta): model = self.model serializer_class = DefaultSerializer diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index d7effce70..b91628804 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -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.client import RequestFactory from rest_framework import generics, status, serializers @@ -49,6 +49,13 @@ class ManyToManyDetail(generics.RetrieveAPIView): model = ManyToManyModel 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): model = BlogPostComment @@ -76,7 +83,13 @@ class OptionalRelationDetail(generics.RetrieveAPIView): model_serializer_class = serializers.HyperlinkedModelSerializer +anchor_urls = patterns('', + url(r'^(?P\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), + url(r'^manytomany/(?P\d+)/$', NamespacedManyToManyDetail.as_view(), name='manytomanymodel-detail'), +) + 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/(?P\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'), url(r'^anchor/(?P\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), @@ -156,6 +169,15 @@ class TestManyToManyHyperlinkedView(TestCase): }] self.list_view = ManyToManyList.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): """ @@ -175,6 +197,15 @@ class TestManyToManyHyperlinkedView(TestCase): self.assertEquals(response.status_code, status.HTTP_200_OK) 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): urls = 'rest_framework.tests.hyperlinkedserializers' From f84f256caa92d39fb31e773844851f1bc182ce0b Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 15:44:25 +0100 Subject: [PATCH 06/10] initializing related fields with namespace allows to drop self.parent.opts.view_namespace access in to_native --- rest_framework/fields.py | 4 ++-- rest_framework/serializers.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1dd666a3c..969800b00 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -547,7 +547,7 @@ class HyperlinkedRelatedField(RelatedField): return self.slug_field def to_native(self, obj): - view_namespace = self.view_namespace or getattr(self.parent.opts, 'view_namespace', 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} @@ -659,7 +659,7 @@ class HyperlinkedIdentityField(Field): 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 or getattr(self.parent.opts, 'view_namespace', 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} diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a15b4ef67..b14ce477b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -560,7 +560,9 @@ class HyperlinkedModelSerializer(ModelSerializer): queryset = rel._default_manager kwargs = { '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: return ManyHyperlinkedRelatedField(**kwargs) From 6f56b6279fe6ba838a4a6cf543e88132085cd811 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 16:09:49 +0100 Subject: [PATCH 07/10] check before inserting 'url' in HyperlinkedmodelSerializer --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b14ce477b..060e3aa0f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -532,7 +532,8 @@ class HyperlinkedModelSerializer(ModelSerializer): if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) - self.base_fields.insert(0, 'url', HyperlinkedIdentityField(view_name=self.opts.view_name, view_namespace=self.opts.view_namespace)) + 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) From 2771b596d174d08350a63516986bb8c7beccc2c1 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 17:09:54 +0100 Subject: [PATCH 08/10] added tests, and fixed bugs changed whole empty stuff - was just wrong or useless --- rest_framework/compat.py | 6 --- rest_framework/fields.py | 37 +++++++++++++---- rest_framework/serializers.py | 8 ++-- .../tests/hyperlinkedserializers.py | 41 +++++++++++++++++++ 4 files changed, 73 insertions(+), 19 deletions(-) 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' From e40b8ee715356da51a8302294df91b94b3ce989d Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 17:25:25 +0100 Subject: [PATCH 09/10] polishing --- rest_framework/compat.py | 1 - rest_framework/fields.py | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index cadc2d2b6..09b763681 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -343,7 +343,6 @@ except ImportError: kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) return datetime.datetime(**kw) - # Markdown is optional try: import markdown diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c1d534046..8217dc3bf 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -562,8 +562,7 @@ class HyperlinkedRelatedField(RelatedField): return self.slug_field def to_native(self, obj): - view_name = self.view_name - + view_name = self.view_name request = self.context.get('request', None) format = self.format or self.context.get('format', None) pk = getattr(obj, 'pk', None) @@ -666,7 +665,7 @@ 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) @@ -681,8 +680,7 @@ class HyperlinkedIdentityField(Field): def field_to_native(self, obj, field_name): request = self.context.get('request', None) format = self.format or self.context.get('format', None) - view_name = self.view_name - + view_name = self.view_name kwargs = {self.pk_url_kwarg: obj.pk} try: return reverse(view_name, kwargs=kwargs, request=request, format=format) From 88d1aa24bc6d27732a71df5ea5944ca39fc89dd6 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 4 Dec 2012 17:33:54 +0100 Subject: [PATCH 10/10] removed last extra whitespaces.. --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 8217dc3bf..62b29cfeb 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -562,7 +562,7 @@ class HyperlinkedRelatedField(RelatedField): return self.slug_field def to_native(self, obj): - view_name = self.view_name + view_name = self.view_name request = self.context.get('request', None) format = self.format or self.context.get('format', None) pk = getattr(obj, 'pk', None) @@ -680,7 +680,7 @@ class HyperlinkedIdentityField(Field): def field_to_native(self, obj, field_name): request = self.context.get('request', None) format = self.format or self.context.get('format', None) - view_name = self.view_name + view_name = self.view_name kwargs = {self.pk_url_kwarg: obj.pk} try: return reverse(view_name, kwargs=kwargs, request=request, format=format)