aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorXavier Ordoquy2013-01-02 16:09:21 +0100
committerXavier Ordoquy2013-01-02 16:09:21 +0100
commit737349d2389197d23886b72c1cb44f53c501ac9a (patch)
tree0c542b6c73deee0280705c8253334126c5f5d254 /rest_framework
parent5fad46d7e213afed503b1533515cab96875a5936 (diff)
parentd379997aba5b1e41309bbed8740ed704c0feb58b (diff)
downloaddjango-rest-framework-737349d2389197d23886b72c1cb44f53c501ac9a.tar.bz2
Merge remote-tracking branch 'reference/py3k' into p3k
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authtoken/serializers.py2
-rw-r--r--rest_framework/authtoken/views.py5
-rw-r--r--rest_framework/compat.py16
-rw-r--r--rest_framework/fields.py446
-rw-r--r--rest_framework/mixins.py8
-rw-r--r--rest_framework/relations.py452
-rw-r--r--rest_framework/renderers.py5
-rw-r--r--rest_framework/request.py17
-rwxr-xr-xrest_framework/runtests/runcoverage.py9
-rwxr-xr-xrest_framework/runtests/runtests.py3
-rw-r--r--rest_framework/runtests/urls.py2
-rw-r--r--rest_framework/serializers.py123
-rw-r--r--rest_framework/templates/rest_framework/base.html17
-rw-r--r--rest_framework/templates/rest_framework/login.html8
-rw-r--r--rest_framework/templatetags/rest_framework.py83
-rw-r--r--rest_framework/tests/authentication.py22
-rw-r--r--rest_framework/tests/breadcrumbs.py2
-rw-r--r--rest_framework/tests/decorators.py2
-rw-r--r--rest_framework/tests/generics.py36
-rw-r--r--rest_framework/tests/htmlrenderer.py2
-rw-r--r--rest_framework/tests/hyperlinkedserializers.py27
-rw-r--r--rest_framework/tests/models.py32
-rw-r--r--rest_framework/tests/modelviews.py2
-rw-r--r--rest_framework/tests/pagination.py45
-rw-r--r--rest_framework/tests/relations_hyperlink.py434
-rw-r--r--rest_framework/tests/relations_nested.py102
-rw-r--r--rest_framework/tests/relations_pk.py414
-rw-r--r--rest_framework/tests/renderers.py15
-rw-r--r--rest_framework/tests/request.py41
-rw-r--r--rest_framework/tests/response.py3
-rw-r--r--rest_framework/tests/reverse.py2
-rw-r--r--rest_framework/tests/serializer.py232
-rw-r--r--rest_framework/tests/testcases.py6
-rw-r--r--rest_framework/urlpatterns.py2
-rw-r--r--rest_framework/urls.py2
36 files changed, 2073 insertions, 548 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 48cebbc5..151ba832 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '2.1.6'
+__version__ = '2.1.14'
VERSION = __version__ # synonym
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index a5ed6e6d..60a3740e 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -1,6 +1,7 @@
from django.contrib.auth import authenticate
from rest_framework import serializers
+
class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
@@ -21,4 +22,3 @@ class AuthTokenSerializer(serializers.Serializer):
raise serializers.ValidationError('Unable to login with provided credentials.')
else:
raise serializers.ValidationError('Must include "username" and "password"')
-
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index cfaacbe9..d318c723 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -6,11 +6,12 @@ from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.serializers import AuthTokenSerializer
+
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
- renderer_classes = (renderers.JSONRenderer,)
+ renderer_classes = (renderers.JSONRenderer,)
model = Token
def post(self, request):
@@ -18,7 +19,7 @@ class ObtainAuthToken(APIView):
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
- return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
obtain_auth_token = ObtainAuthToken.as_view()
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 6ffada48..d5a9d995 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -8,6 +8,12 @@ import six
import django
+# location of patterns, url, include changes in 1.4 onwards
+try:
+ from django.conf.urls import patterns, url, include
+except:
+ from django.conf.urls.defaults import patterns, url, include
+
# django-filter is optional
try:
import django_filters
@@ -24,6 +30,16 @@ except ImportError:
from six import BytesIO
+# Try to import PIL in either of the two ways it can end up installed.
+try:
+ from PIL import Image
+except ImportError:
+ try:
+ import Image
+ except ImportError:
+ Image = None
+
+
def get_concrete_model(model_cls):
try:
return model_cls._meta.concrete_model
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 8ed7efa5..23e2ac44 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -9,19 +9,16 @@ import re
import warnings
from django.core import validators
-from django.core.exceptions import ObjectDoesNotExist, ValidationError
-from django.core.urlresolvers import resolve, get_script_prefix
+from django.core.exceptions import ValidationError
from django.conf import settings
from django import forms
from django.forms import widgets
-from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type
try:
from django.utils.encoding import smart_text
except ImportError:
from django.utils.encoding import smart_unicode as smart_text
from django.utils.translation import ugettext_lazy as _
-from rest_framework.reverse import reverse
from rest_framework.compat import parse_date, parse_datetime
from rest_framework.compat import timezone
from rest_framework.compat import BytesIO
@@ -42,6 +39,7 @@ def is_simple_callable(obj):
class Field(object):
+ read_only = True
creation_counter = 0
empty = ''
type_name = None
@@ -143,7 +141,7 @@ class WritableField(Field):
if required is None:
self.required = not(read_only)
else:
- assert not read_only, "Cannot set required=True and read_only=True"
+ assert not (read_only and required), "Cannot set required=True and read_only=True"
self.required = required
messages = {}
@@ -261,430 +259,6 @@ class ModelField(WritableField):
"type": self.model_field.get_internal_type()
}
-##### Relational fields #####
-
-
-# Not actually Writable, but subclasses may need to be.
-class RelatedField(WritableField):
- """
- Base class for related model fields.
-
- If not overridden, this represents a to-one relationship, using the unicode
- representation of the target.
- """
- widget = widgets.Select
- cache_choices = False
- empty_label = None
- default_read_only = True # TODO: Remove this
-
- def __init__(self, *args, **kwargs):
- self.queryset = kwargs.pop('queryset', None)
- super(RelatedField, self).__init__(*args, **kwargs)
- self.read_only = kwargs.pop('read_only', self.default_read_only)
-
- def initialize(self, parent, field_name):
- super(RelatedField, self).initialize(parent, field_name)
- if self.queryset is None and not self.read_only:
- try:
- manager = getattr(self.parent.opts.model, self.source or field_name)
- if hasattr(manager, 'related'): # Forward
- self.queryset = manager.related.model._default_manager.all()
- else: # Reverse
- self.queryset = manager.field.rel.to._default_manager.all()
- except:
- raise
- msg = ('Serializer related fields must include a `queryset`' +
- ' argument or set `read_only=True')
- raise Exception(msg)
-
- ### We need this stuff to make form choices work...
-
- # def __deepcopy__(self, memo):
- # result = super(RelatedField, self).__deepcopy__(memo)
- # result.queryset = result.queryset
- # return result
-
- def prepare_value(self, obj):
- return self.to_native(obj)
-
- def label_from_instance(self, obj):
- """
- Return a readable representation for use with eg. select widgets.
- """
- desc = smart_text(obj)
- ident = smart_text(self.to_native(obj))
- if desc == ident:
- return desc
- return "%s - %s" % (desc, ident)
-
- def _get_queryset(self):
- return self._queryset
-
- def _set_queryset(self, queryset):
- self._queryset = queryset
- self.widget.choices = self.choices
-
- queryset = property(_get_queryset, _set_queryset)
-
- def _get_choices(self):
- # If self._choices is set, then somebody must have manually set
- # the property self.choices. In this case, just return self._choices.
- if hasattr(self, '_choices'):
- return self._choices
-
- # Otherwise, execute the QuerySet in self.queryset to determine the
- # choices dynamically. Return a fresh ModelChoiceIterator that has not been
- # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
- # time _get_choices() is called (and, thus, each time self.choices is
- # accessed) so that we can ensure the QuerySet has not been consumed. This
- # construct might look complicated but it allows for lazy evaluation of
- # the queryset.
- return ModelChoiceIterator(self)
-
- def _set_choices(self, value):
- # Setting choices also sets the choices on the widget.
- # choices can be any iterable, but we call list() on it because
- # it will be consumed more than once.
- self._choices = self.widget.choices = list(value)
-
- choices = property(_get_choices, _set_choices)
-
- ### Regular serializer stuff...
-
- def field_to_native(self, obj, field_name):
- value = getattr(obj, self.source or field_name)
- return self.to_native(value)
-
- def field_from_native(self, data, files, field_name, into):
- if self.read_only:
- return
-
- value = data.get(field_name)
- into[(self.source or field_name)] = self.from_native(value)
-
-
-class ManyRelatedMixin(object):
- """
- Mixin to convert a related field to a many related field.
- """
- widget = widgets.SelectMultiple
-
- def field_to_native(self, obj, field_name):
- value = getattr(obj, self.source or field_name)
- return [self.to_native(item) for item in value.all()]
-
- def field_from_native(self, data, files, field_name, into):
- if self.read_only:
- return
-
- try:
- # Form data
- value = data.getlist(self.source or field_name)
- except:
- # Non-form data
- value = data.get(self.source or field_name)
- else:
- if value == ['']:
- value = []
- into[field_name] = [self.from_native(item) for item in value]
-
-
-class ManyRelatedField(ManyRelatedMixin, RelatedField):
- """
- Base class for related model managers.
-
- If not overridden, this represents a to-many relationship, using the unicode
- representations of the target, and is read-only.
- """
- pass
-
-
-### PrimaryKey relationships
-
-class PrimaryKeyRelatedField(RelatedField):
- """
- Represents a to-one relationship as a pk value.
- """
- default_read_only = False
- form_field_class = forms.ChoiceField
-
- # TODO: Remove these field hacks...
- def prepare_value(self, obj):
- return self.to_native(obj.pk)
-
- def label_from_instance(self, obj):
- """
- Return a readable representation for use with eg. select widgets.
- """
- desc = smart_text(obj)
- ident = smart_text(self.to_native(obj.pk))
- if desc == ident:
- return desc
- return "%s - %s" % (desc, ident)
-
- # TODO: Possibly change this to just take `obj`, through prob less performant
- def to_native(self, pk):
- return pk
-
- def from_native(self, data):
- if self.queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
-
- try:
- return self.queryset.get(pk=data)
- except ObjectDoesNotExist:
- msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
- raise ValidationError(msg)
-
- def field_to_native(self, obj, field_name):
- try:
- # Prefer obj.serializable_value for performance reasons
- pk = obj.serializable_value(self.source or field_name)
- except AttributeError:
- # RelatedObject (reverse relationship)
- obj = getattr(obj, self.source or field_name)
- return self.to_native(obj.pk)
- # Forward relationship
- return self.to_native(pk)
-
-
-class ManyPrimaryKeyRelatedField(ManyRelatedField):
- """
- Represents a to-many relationship as a pk value.
- """
- default_read_only = False
- form_field_class = forms.MultipleChoiceField
-
- def prepare_value(self, obj):
- return self.to_native(obj.pk)
-
- def label_from_instance(self, obj):
- """
- Return a readable representation for use with eg. select widgets.
- """
- desc = smart_text(obj)
- ident = smart_text(self.to_native(obj.pk))
- if desc == ident:
- return desc
- return "%s - %s" % (desc, ident)
-
- def to_native(self, pk):
- return pk
-
- def field_to_native(self, obj, field_name):
- try:
- # Prefer obj.serializable_value for performance reasons
- queryset = obj.serializable_value(self.source or field_name)
- except AttributeError:
- # RelatedManager (reverse relationship)
- queryset = getattr(obj, self.source or field_name)
- return [self.to_native(item.pk) for item in queryset.all()]
- # Forward relationship
- return [self.to_native(item.pk) for item in queryset.all()]
-
- def from_native(self, data):
- if self.queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
-
- try:
- return self.queryset.get(pk=data)
- except ObjectDoesNotExist:
- msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
- raise ValidationError(msg)
-
-### Slug relationships
-
-
-class SlugRelatedField(RelatedField):
- default_read_only = False
- form_field_class = forms.ChoiceField
-
- def __init__(self, *args, **kwargs):
- self.slug_field = kwargs.pop('slug_field', None)
- assert self.slug_field, 'slug_field is required'
- super(SlugRelatedField, self).__init__(*args, **kwargs)
-
- def to_native(self, obj):
- return getattr(obj, self.slug_field)
-
- def from_native(self, data):
- if self.queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
-
- try:
- return self.queryset.get(**{self.slug_field: data})
- except ObjectDoesNotExist:
- raise ValidationError('Object with %s=%s does not exist.' %
- (self.slug_field, unicode(data)))
-
-
-class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
- form_field_class = forms.MultipleChoiceField
-
-
-### Hyperlinked relationships
-
-class HyperlinkedRelatedField(RelatedField):
- """
- Represents a to-one relationship, using hyperlinking.
- """
- pk_url_kwarg = 'pk'
- slug_field = 'slug'
- slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
- default_read_only = False
- form_field_class = forms.ChoiceField
-
- def __init__(self, *args, **kwargs):
- try:
- self.view_name = kwargs.pop('view_name')
- except:
- raise ValueError("Hyperlinked field requires 'view_name' kwarg")
-
- self.slug_field = kwargs.pop('slug_field', 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.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
-
- self.format = kwargs.pop('format', None)
- super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
-
- def get_slug_field(self):
- """
- Get the name of a slug field to be used to look up by slug.
- """
- return self.slug_field
-
- def to_native(self, obj):
- 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)
- if pk is None:
- return
- kwargs = {self.pk_url_kwarg: pk}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except:
- pass
-
- slug = getattr(obj, self.slug_field, None)
-
- if not slug:
- raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
-
- kwargs = {self.slug_url_kwarg: slug}
- try:
- return reverse(self.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)
- except:
- pass
-
- raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
-
- def from_native(self, value):
- # Convert URL -> model instance pk
- # TODO: Use values_list
- if self.queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
-
- if value.startswith('http:') or value.startswith('https:'):
- # If needed convert absolute URLs to relative path
- value = urlparse(value).path
- prefix = get_script_prefix()
- if value.startswith(prefix):
- value = '/' + value[len(prefix):]
-
- try:
- match = resolve(value)
- except:
- raise ValidationError('Invalid hyperlink - No URL match')
-
- if match.url_name != self.view_name:
- raise ValidationError('Invalid hyperlink - Incorrect URL match')
-
- pk = match.kwargs.get(self.pk_url_kwarg, None)
- slug = match.kwargs.get(self.slug_url_kwarg, None)
-
- # Try explicit primary key.
- if pk is not None:
- queryset = self.queryset.filter(pk=pk)
- # Next, try looking up by slug.
- elif slug is not None:
- slug_field = self.get_slug_field()
- queryset = self.queryset.filter(**{slug_field: slug})
- # If none of those are defined, it's an error.
- else:
- raise ValidationError('Invalid hyperlink')
-
- try:
- obj = queryset.get()
- except ObjectDoesNotExist:
- raise ValidationError('Invalid hyperlink - object does not exist.')
- return obj
-
-
-class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
- """
- Represents a to-many relationship, using hyperlinking.
- """
- form_field_class = forms.MultipleChoiceField
-
-
-class HyperlinkedIdentityField(Field):
- """
- Represents the instance, or a property on the instance, using hyperlinking.
- """
- pk_url_kwarg = 'pk'
- slug_field = 'slug'
- 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)
- self.format = kwargs.pop('format', None)
-
- self.slug_field = kwargs.pop('slug_field', 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.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
-
- super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
-
- 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
- kwargs = {self.pk_url_kwarg: obj.pk}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except:
- pass
-
- slug = getattr(obj, self.slug_field, None)
-
- if not slug:
- raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
-
- kwargs = {self.slug_url_kwarg: slug}
- try:
- return reverse(self.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)
- except:
- pass
-
- raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
-
##### Typed Fields #####
@@ -703,9 +277,9 @@ class BooleanField(WritableField):
default = False
def from_native(self, value):
- if value in ('t', 'True', '1'):
+ if value in ('true', 't', 'True', '1'):
return True
- if value in ('f', 'False', '0'):
+ if value in ('false', 'f', 'False', '0'):
return False
return bool(value)
@@ -797,7 +371,7 @@ class ChoiceField(WritableField):
if value == smart_text(k2):
return True
else:
- if value == smart_text(k):
+ if value == smart_text(k) or value == k:
return True
return False
@@ -827,6 +401,7 @@ class EmailField(CharField):
class RegexField(CharField):
type_name = 'RegexField'
+ form_field_class = forms.RegexField
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
@@ -1058,11 +633,8 @@ class ImageField(FileField):
if f is None:
return None
- # Try to import PIL in either of the two ways it can end up installed.
- try:
- from PIL import Image
- except ImportError:
- import Image
+ from compat import Image
+ assert Image is not None, 'PIL must be installed for ImageField support'
# We need to get a file object for PIL. We might have a path or we might
# have to read the data into memory.
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 87d97bed..503376ce 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -115,6 +115,10 @@ class UpdateModelMixin(object):
slug_field = self.get_slug_field()
setattr(obj, slug_field, slug)
+ # Ensure we clean the attributes so that we don't eg return integer
+ # pk using a string representation, as provided by the url conf kwarg.
+ obj.full_clean()
+
class DestroyModelMixin(object):
"""
@@ -122,6 +126,6 @@ class DestroyModelMixin(object):
Should be mixed in with `SingleObjectBaseView`.
"""
def destroy(self, request, *args, **kwargs):
- self.object = self.get_object()
- self.object.delete()
+ obj = self.get_object()
+ obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
new file mode 100644
index 00000000..fe8cbc44
--- /dev/null
+++ b/rest_framework/relations.py
@@ -0,0 +1,452 @@
+
+from __future__ import unicode_literals
+
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.core.urlresolvers import resolve, get_script_prefix
+from django import forms
+from django.forms import widgets
+from django.forms.models import ModelChoiceIterator
+try:
+ from django.utils.encoding import smart_text
+except ImportError:
+ from django.utils.encoding import smart_unicode as smart_text
+from rest_framework.fields import Field, WritableField
+from rest_framework.reverse import reverse
+from urlparse import urlparse
+
+##### Relational fields #####
+
+
+# Not actually Writable, but subclasses may need to be.
+class RelatedField(WritableField):
+ """
+ Base class for related model fields.
+
+ If not overridden, this represents a to-one relationship, using the unicode
+ representation of the target.
+ """
+ widget = widgets.Select
+ cache_choices = False
+ empty_label = None
+ default_read_only = True # TODO: Remove this
+
+ def __init__(self, *args, **kwargs):
+ self.queryset = kwargs.pop('queryset', None)
+ self.null = kwargs.pop('null', False)
+ super(RelatedField, self).__init__(*args, **kwargs)
+ self.read_only = kwargs.pop('read_only', self.default_read_only)
+
+ def initialize(self, parent, field_name):
+ super(RelatedField, self).initialize(parent, field_name)
+ if self.queryset is None and not self.read_only:
+ try:
+ manager = getattr(self.parent.opts.model, self.source or field_name)
+ if hasattr(manager, 'related'): # Forward
+ self.queryset = manager.related.model._default_manager.all()
+ else: # Reverse
+ self.queryset = manager.field.rel.to._default_manager.all()
+ except:
+ raise
+ msg = ('Serializer related fields must include a `queryset`' +
+ ' argument or set `read_only=True')
+ raise Exception(msg)
+
+ ### We need this stuff to make form choices work...
+
+ # def __deepcopy__(self, memo):
+ # result = super(RelatedField, self).__deepcopy__(memo)
+ # result.queryset = result.queryset
+ # return result
+
+ def prepare_value(self, obj):
+ return self.to_native(obj)
+
+ def label_from_instance(self, obj):
+ """
+ Return a readable representation for use with eg. select widgets.
+ """
+ desc = smart_text(obj)
+ ident = smart_text(self.to_native(obj))
+ if desc == ident:
+ return desc
+ return "%s - %s" % (desc, ident)
+
+ def _get_queryset(self):
+ return self._queryset
+
+ def _set_queryset(self, queryset):
+ self._queryset = queryset
+ self.widget.choices = self.choices
+
+ queryset = property(_get_queryset, _set_queryset)
+
+ def _get_choices(self):
+ # If self._choices is set, then somebody must have manually set
+ # the property self.choices. In this case, just return self._choices.
+ if hasattr(self, '_choices'):
+ return self._choices
+
+ # Otherwise, execute the QuerySet in self.queryset to determine the
+ # choices dynamically. Return a fresh ModelChoiceIterator that has not been
+ # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
+ # time _get_choices() is called (and, thus, each time self.choices is
+ # accessed) so that we can ensure the QuerySet has not been consumed. This
+ # construct might look complicated but it allows for lazy evaluation of
+ # the queryset.
+ return ModelChoiceIterator(self)
+
+ def _set_choices(self, value):
+ # Setting choices also sets the choices on the widget.
+ # choices can be any iterable, but we call list() on it because
+ # it will be consumed more than once.
+ self._choices = self.widget.choices = list(value)
+
+ choices = property(_get_choices, _set_choices)
+
+ ### Regular serializer stuff...
+
+ def field_to_native(self, obj, field_name):
+ value = getattr(obj, self.source or field_name)
+ return self.to_native(value)
+
+ def field_from_native(self, data, files, field_name, into):
+ if self.read_only:
+ return
+
+ try:
+ value = data[field_name]
+ except KeyError:
+ if self.required:
+ raise ValidationError(self.error_messages['required'])
+ return
+
+ if value in (None, '') and not self.null:
+ raise ValidationError('Value may not be null')
+ elif value in (None, '') and self.null:
+ into[(self.source or field_name)] = None
+ else:
+ into[(self.source or field_name)] = self.from_native(value)
+
+
+class ManyRelatedMixin(object):
+ """
+ Mixin to convert a related field to a many related field.
+ """
+ widget = widgets.SelectMultiple
+
+ def field_to_native(self, obj, field_name):
+ value = getattr(obj, self.source or field_name)
+ return [self.to_native(item) for item in value.all()]
+
+ def field_from_native(self, data, files, field_name, into):
+ if self.read_only:
+ return
+
+ try:
+ # Form data
+ value = data.getlist(self.source or field_name)
+ except:
+ # Non-form data
+ value = data.get(self.source or field_name)
+ else:
+ if value == ['']:
+ value = []
+
+ into[field_name] = [self.from_native(item) for item in value]
+
+
+class ManyRelatedField(ManyRelatedMixin, RelatedField):
+ """
+ Base class for related model managers.
+
+ If not overridden, this represents a to-many relationship, using the unicode
+ representations of the target, and is read-only.
+ """
+ pass
+
+
+### PrimaryKey relationships
+
+class PrimaryKeyRelatedField(RelatedField):
+ """
+ Represents a to-one relationship as a pk value.
+ """
+ default_read_only = False
+ form_field_class = forms.ChoiceField
+
+ # TODO: Remove these field hacks...
+ def prepare_value(self, obj):
+ return self.to_native(obj.pk)
+
+ def label_from_instance(self, obj):
+ """
+ Return a readable representation for use with eg. select widgets.
+ """
+ desc = smart_text(obj)
+ ident = smart_text(self.to_native(obj.pk))
+ if desc == ident:
+ return desc
+ return "%s - %s" % (desc, ident)
+
+ # TODO: Possibly change this to just take `obj`, through prob less performant
+ def to_native(self, pk):
+ return pk
+
+ def from_native(self, data):
+ if self.queryset is None:
+ raise Exception('Writable related fields must include a `queryset` argument')
+
+ try:
+ return self.queryset.get(pk=data)
+ except ObjectDoesNotExist:
+ msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
+ raise ValidationError(msg)
+
+ def field_to_native(self, obj, field_name):
+ try:
+ # Prefer obj.serializable_value for performance reasons
+ pk = obj.serializable_value(self.source or field_name)
+ except AttributeError:
+ # RelatedObject (reverse relationship)
+ obj = getattr(obj, self.source or field_name)
+ return self.to_native(obj.pk)
+ # Forward relationship
+ return self.to_native(pk)
+
+
+class ManyPrimaryKeyRelatedField(ManyRelatedField):
+ """
+ Represents a to-many relationship as a pk value.
+ """
+ default_read_only = False
+ form_field_class = forms.MultipleChoiceField
+
+ def prepare_value(self, obj):
+ return self.to_native(obj.pk)
+
+ def label_from_instance(self, obj):
+ """
+ Return a readable representation for use with eg. select widgets.
+ """
+ desc = smart_text(obj)
+ ident = smart_text(self.to_native(obj.pk))
+ if desc == ident:
+ return desc
+ return "%s - %s" % (desc, ident)
+
+ def to_native(self, pk):
+ return pk
+
+ def field_to_native(self, obj, field_name):
+ try:
+ # Prefer obj.serializable_value for performance reasons
+ queryset = obj.serializable_value(self.source or field_name)
+ except AttributeError:
+ # RelatedManager (reverse relationship)
+ queryset = getattr(obj, self.source or field_name)
+ return [self.to_native(item.pk) for item in queryset.all()]
+ # Forward relationship
+ return [self.to_native(item.pk) for item in queryset.all()]
+
+ def from_native(self, data):
+ if self.queryset is None:
+ raise Exception('Writable related fields must include a `queryset` argument')
+
+ try:
+ return self.queryset.get(pk=data)
+ except ObjectDoesNotExist:
+ msg = "Invalid pk '%s' - object does not exist." % smart_text(data)
+ raise ValidationError(msg)
+
+### Slug relationships
+
+
+class SlugRelatedField(RelatedField):
+ default_read_only = False
+ form_field_class = forms.ChoiceField
+
+ def __init__(self, *args, **kwargs):
+ self.slug_field = kwargs.pop('slug_field', None)
+ assert self.slug_field, 'slug_field is required'
+ super(SlugRelatedField, self).__init__(*args, **kwargs)
+
+ def to_native(self, obj):
+ return getattr(obj, self.slug_field)
+
+ def from_native(self, data):
+ if self.queryset is None:
+ raise Exception('Writable related fields must include a `queryset` argument')
+
+ try:
+ return self.queryset.get(**{self.slug_field: data})
+ except ObjectDoesNotExist:
+ raise ValidationError('Object with %s=%s does not exist.' %
+ (self.slug_field, unicode(data)))
+
+
+class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
+ form_field_class = forms.MultipleChoiceField
+
+
+### Hyperlinked relationships
+
+class HyperlinkedRelatedField(RelatedField):
+ """
+ Represents a to-one relationship, using hyperlinking.
+ """
+ pk_url_kwarg = 'pk'
+ slug_field = 'slug'
+ slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
+ default_read_only = False
+ form_field_class = forms.ChoiceField
+
+ def __init__(self, *args, **kwargs):
+ try:
+ self.view_name = kwargs.pop('view_name')
+ except:
+ raise ValueError("Hyperlinked field requires 'view_name' kwarg")
+
+ self.slug_field = kwargs.pop('slug_field', 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.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
+
+ self.format = kwargs.pop('format', None)
+ super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
+
+ def get_slug_field(self):
+ """
+ Get the name of a slug field to be used to look up by slug.
+ """
+ return self.slug_field
+
+ def to_native(self, obj):
+ 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)
+ if pk is None:
+ return
+ kwargs = {self.pk_url_kwarg: pk}
+ try:
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
+ except:
+ pass
+
+ slug = getattr(obj, self.slug_field, None)
+
+ if not slug:
+ raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+
+ kwargs = {self.slug_url_kwarg: slug}
+ try:
+ return reverse(self.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)
+ except:
+ pass
+
+ raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+
+ def from_native(self, value):
+ # Convert URL -> model instance pk
+ # TODO: Use values_list
+ if self.queryset is None:
+ raise Exception('Writable related fields must include a `queryset` argument')
+
+ if value.startswith('http:') or value.startswith('https:'):
+ # If needed convert absolute URLs to relative path
+ value = urlparse(value).path
+ prefix = get_script_prefix()
+ if value.startswith(prefix):
+ value = '/' + value[len(prefix):]
+
+ try:
+ match = resolve(value)
+ except:
+ raise ValidationError('Invalid hyperlink - No URL match')
+
+ if match.url_name != self.view_name:
+ raise ValidationError('Invalid hyperlink - Incorrect URL match')
+
+ pk = match.kwargs.get(self.pk_url_kwarg, None)
+ slug = match.kwargs.get(self.slug_url_kwarg, None)
+
+ # Try explicit primary key.
+ if pk is not None:
+ queryset = self.queryset.filter(pk=pk)
+ # Next, try looking up by slug.
+ elif slug is not None:
+ slug_field = self.get_slug_field()
+ queryset = self.queryset.filter(**{slug_field: slug})
+ # If none of those are defined, it's an error.
+ else:
+ raise ValidationError('Invalid hyperlink')
+
+ try:
+ obj = queryset.get()
+ except ObjectDoesNotExist:
+ raise ValidationError('Invalid hyperlink - object does not exist.')
+ return obj
+
+
+class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
+ """
+ Represents a to-many relationship, using hyperlinking.
+ """
+ form_field_class = forms.MultipleChoiceField
+
+
+class HyperlinkedIdentityField(Field):
+ """
+ Represents the instance, or a property on the instance, using hyperlinking.
+ """
+ pk_url_kwarg = 'pk'
+ slug_field = 'slug'
+ 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)
+ self.format = kwargs.pop('format', None)
+
+ self.slug_field = kwargs.pop('slug_field', 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.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
+
+ super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
+
+ 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
+ kwargs = {self.pk_url_kwarg: obj.pk}
+ try:
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
+ except:
+ pass
+
+ slug = getattr(obj, self.slug_field, None)
+
+ if not slug:
+ raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+
+ kwargs = {self.slug_url_kwarg: slug}
+ try:
+ return reverse(self.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)
+ except:
+ pass
+
+ raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 44a40baf..54930167 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -22,7 +22,7 @@ from rest_framework.utils import dict2xml
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework import VERSION, status
-from rest_framework import serializers, parsers
+from rest_framework import parsers
class BaseRenderer(object):
@@ -322,6 +322,9 @@ class BrowsableAPIRenderer(BaseRenderer):
if getattr(v, 'choices', None) is not None:
kwargs['choices'] = v.choices
+ if getattr(v, 'regex', None) is not None:
+ kwargs['regex'] = v.regex
+
if getattr(v, 'widget', None):
widget = copy.deepcopy(v.widget)
kwargs['widget'] = widget
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 05424f21..c50ae5ad 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -170,6 +170,15 @@ class Request(object):
self._user, self._auth = self._authenticate()
return self._user
+ @user.setter
+ def user(self, value):
+ """
+ Sets the user on the current request. This is necessary to maintain
+ compatilbility with django.contrib.auth where the user proprety is
+ set in the login and logout functions.
+ """
+ self._user = value
+
@property
def auth(self):
"""
@@ -180,6 +189,14 @@ class Request(object):
self._user, self._auth = self._authenticate()
return self._auth
+ @auth.setter
+ def auth(self, value):
+ """
+ Sets any non-user authentication information associated with the
+ request, such as an authentication token.
+ """
+ self._auth = value
+
def _load_data_and_files(self):
"""
Parses the request content into self.DATA and self.FILES.
diff --git a/rest_framework/runtests/runcoverage.py b/rest_framework/runtests/runcoverage.py
index 0ce379eb..bcab1d14 100755
--- a/rest_framework/runtests/runcoverage.py
+++ b/rest_framework/runtests/runcoverage.py
@@ -8,6 +8,9 @@ Useful tool to run the test suite for rest_framework and generate a coverage rep
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os
import sys
+
+# fix sys path so we don't need to setup PYTHONPATH
+sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from coverage import coverage
@@ -55,6 +58,12 @@ def main():
if 'compat.py' in files:
files.remove('compat.py')
+ # Same applies to template tags module.
+ # This module has to include branching on Django versions,
+ # so it's never possible for it to have full coverage.
+ if 'rest_framework.py' in files:
+ files.remove('rest_framework.py')
+
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
cov.report(cov_files)
diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py
index 138c725b..4a333fb3 100755
--- a/rest_framework/runtests/runtests.py
+++ b/rest_framework/runtests/runtests.py
@@ -5,6 +5,9 @@
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os
import sys
+
+# fix sys path so we don't need to setup PYTHONPATH
+sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from django.conf import settings
diff --git a/rest_framework/runtests/urls.py b/rest_framework/runtests/urls.py
index 4b7da787..ed5baeae 100644
--- a/rest_framework/runtests/urls.py
+++ b/rest_framework/runtests/urls.py
@@ -1,7 +1,7 @@
"""
Blank URLConf just to keep runtests.py happy.
"""
-from django.conf.urls.defaults import *
+from rest_framework.compat import patterns
urlpatterns = patterns('',
)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index cbb6b4df..91fbe176 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -16,7 +16,7 @@ from rest_framework.compat import get_concrete_model
# This helps keep the seperation between model fields, form fields, and
# serializer fields more explicit.
-
+from rest_framework.relations import *
from rest_framework.fields import *
@@ -24,7 +24,16 @@ class DictWithMetadata(dict):
"""
A dict-like object, that can have additional properties attached.
"""
- pass
+ def __getstate__(self):
+ """
+ Used by pickle (e.g., caching).
+ Overriden to remove metadata from the dict, since it shouldn't be pickled
+ and may in some instances be unpickleable.
+ """
+ # return an instance of the first dict in MRO that isn't a DictWithMetadata
+ for base in self.__class__.__mro__:
+ if not isinstance(base, DictWithMetadata) and isinstance(base, dict):
+ return base(self)
class SortedDictWithMetadata(SortedDict, DictWithMetadata):
@@ -93,7 +102,8 @@ class BaseSerializer(Field):
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
- 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)
self.opts = self._options_class(self.Meta)
self.parent = None
@@ -133,8 +143,6 @@ class BaseSerializer(Field):
base_fields = copy.deepcopy(self.base_fields)
for key, field in base_fields.items():
ret[key] = field
- # Set up the field
- field.initialize(parent=self, field_name=key)
# Add in the default fields
default_fields = self.get_default_fields()
@@ -154,6 +162,9 @@ class BaseSerializer(Field):
for key in self.opts.exclude:
ret.pop(key, None)
+ for key, field in ret.items():
+ field.initialize(parent=self, field_name=key)
+
return ret
#####
@@ -186,6 +197,7 @@ class BaseSerializer(Field):
ret.fields = {}
for field_name, field in self.fields.items():
+ field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
ret[key] = value
@@ -199,6 +211,7 @@ class BaseSerializer(Field):
"""
reverted_data = {}
for field_name, field in self.fields.items():
+ field.initialize(parent=self, field_name=field_name)
try:
field.field_from_native(data, files, field_name, reverted_data)
except ValidationError as err:
@@ -219,10 +232,18 @@ class BaseSerializer(Field):
except ValidationError as err:
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
- try:
- attrs = self.validate(attrs)
- except ValidationError as err:
- self._errors['non_field_errors'] = err.messages
+ # If there are already errors, we don't run .validate() because
+ # field-validation failed and thus `attrs` may not be complete.
+ # which in turn can cause inconsistent validation errors.
+ if not self._errors:
+ try:
+ attrs = self.validate(attrs)
+ except ValidationError as err:
+ if hasattr(err, 'message_dict'):
+ for field_name, error_messages in err.message_dict.items():
+ self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
+ elif hasattr(err, 'messages'):
+ self._errors['non_field_errors'] = err.messages
return attrs
@@ -274,15 +295,23 @@ class BaseSerializer(Field):
Override default so that we can apply ModelSerializer as a nested
field to relationships.
"""
- obj = getattr(obj, self.source or field_name)
-
- if is_simple_callable(obj):
- obj = obj()
+ if self.source:
+ for component in self.source.split('.'):
+ obj = getattr(obj, component)
+ if is_simple_callable(obj):
+ obj = obj()
+ else:
+ obj = getattr(obj, field_name)
+ if is_simple_callable(obj):
+ obj = value()
# If the object has an "all" method, assume it's a relationship
if is_simple_callable(getattr(obj, 'all', None)):
return [self.to_native(item) for item in obj.all()]
+ if obj is None:
+ return None
+
return self.to_native(obj)
@property
@@ -366,7 +395,6 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field)
if field:
- field.initialize(parent=self, field_name=model_field.name)
ret[model_field.name] = field
for field_name in self.opts.read_only_fields:
@@ -398,10 +426,14 @@ class ModelSerializer(Serializer):
"""
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
- queryset = model_field.rel.to._default_manager
+ kwargs = {
+ 'null': model_field.null,
+ 'queryset': model_field.rel.to._default_manager
+ }
+
if to_many:
- return ManyPrimaryKeyRelatedField(queryset=queryset)
- return PrimaryKeyRelatedField(queryset=queryset)
+ return ManyPrimaryKeyRelatedField(**kwargs)
+ return PrimaryKeyRelatedField(**kwargs)
def get_field(self, model_field):
"""
@@ -411,7 +443,7 @@ class ModelSerializer(Serializer):
kwargs['blank'] = model_field.blank
- if model_field.null:
+ if model_field.null or model_field.blank:
kwargs['required'] = False
if model_field.has_default():
@@ -426,10 +458,6 @@ class ModelSerializer(Serializer):
kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs)
- max_length = getattr(model_field, 'max_length', None)
- if max_length:
- kwargs['max_length'] = max_length
-
field_mapping = {
models.FloatField: FloatField,
models.IntegerField: IntegerField,
@@ -453,28 +481,56 @@ class ModelSerializer(Serializer):
except KeyError:
return ModelField(model_field=model_field, **kwargs)
+ def get_validation_exclusions(self):
+ """
+ Return a list of field names to exclude from model validation.
+ """
+ cls = self.opts.model
+ opts = get_concrete_model(cls)._meta
+ exclusions = [field.name for field in opts.fields + opts.many_to_many]
+ for field_name, field in self.fields.items():
+ if field_name in exclusions and not field.read_only:
+ exclusions.remove(field_name)
+ return exclusions
+
def restore_object(self, attrs, instance=None):
"""
Restore the model instance.
"""
self.m2m_data = {}
+ self.related_data = {}
- if instance is not None:
- for key, val in attrs.items():
- setattr(instance, key, val)
- return instance
+ # Reverse fk relations
+ for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model():
+ field_name = obj.field.related_query_name()
+ if field_name in attrs:
+ self.related_data[field_name] = attrs.pop(field_name)
- # Reverse relations
+ # Reverse m2m relations
for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model():
field_name = obj.field.related_query_name()
if field_name in attrs:
self.m2m_data[field_name] = attrs.pop(field_name)
- # Forward relations
+ # Forward m2m relations
for field in self.opts.model._meta.many_to_many:
if field.name in attrs:
self.m2m_data[field.name] = attrs.pop(field.name)
- return self.opts.model(**attrs)
+
+ if instance is not None:
+ for key, val in attrs.items():
+ setattr(instance, key, val)
+
+ else:
+ instance = self.opts.model(**attrs)
+
+ try:
+ instance.full_clean(exclude=self.get_validation_exclusions())
+ except ValidationError, err:
+ self._errors = err.message_dict
+ return None
+
+ return instance
def save(self, save_m2m=True):
"""
@@ -487,6 +543,11 @@ class ModelSerializer(Serializer):
setattr(self.object, accessor_name, object_list)
self.m2m_data = {}
+ if getattr(self, 'related_data', None):
+ for accessor_name, object_list in self.related_data.items():
+ setattr(self.object, accessor_name, object_list)
+ self.related_data = {}
+
return self.object
@@ -533,9 +594,9 @@ class HyperlinkedModelSerializer(ModelSerializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
rel = model_field.rel.to
- queryset = rel._default_manager
kwargs = {
- 'queryset': queryset,
+ 'null': model_field.null,
+ 'queryset': rel._default_manager,
'view_name': self._get_default_view_name(rel)
}
if to_many:
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index fb0e19f0..42e49cb9 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -1,6 +1,5 @@
{% load url from future %}
{% load rest_framework %}
-{% load static %}
<!DOCTYPE html>
<html>
<head>
@@ -14,10 +13,10 @@
<title>{% block title %}Django REST framework{% endblock %}</title>
{% block style %}
- <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/>
- <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/>
- <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/prettify.css'/>
- <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %}
{% endblock %}
@@ -195,10 +194,10 @@
{% endblock %}
{% block script %}
- <script src="{% get_static_prefix %}rest_framework/js/jquery-1.8.1-min.js"></script>
- <script src="{% get_static_prefix %}rest_framework/js/bootstrap.min.js"></script>
- <script src="{% get_static_prefix %}rest_framework/js/prettify-min.js"></script>
- <script src="{% get_static_prefix %}rest_framework/js/default.js"></script>
+ <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
+ <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
+ <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
+ <script src="{% static "rest_framework/js/default.js" %}"></script>
{% endblock %}
</body>
</html>
diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html
index c1271399..6e2bd8d4 100644
--- a/rest_framework/templates/rest_framework/login.html
+++ b/rest_framework/templates/rest_framework/login.html
@@ -1,11 +1,11 @@
{% load url from future %}
-{% load static %}
+{% load rest_framework %}
<html>
<head>
- <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/>
- <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/>
- <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
</head>
<body class="container">
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index 1fc174ff..18427802 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -20,6 +20,89 @@ import string
register = template.Library()
+# Note we don't use 'load staticfiles', because we need a 1.3 compatible
+# version, so instead we include the `static` template tag ourselves.
+
+# When 1.3 becomes unsupported by REST framework, we can instead start to
+# use the {% load staticfiles %} tag, remove the following code,
+# and add a dependancy that `django.contrib.staticfiles` must be installed.
+
+# Note: We can't put this into the `compat` module because the compat import
+# from rest_framework.compat import ...
+# conflicts with this rest_framework template tag module.
+
+try: # Django 1.5+
+ from django.contrib.staticfiles.templatetags import StaticFilesNode
+
+ @register.tag('static')
+ def do_static(parser, token):
+ return StaticFilesNode.handle_token(parser, token)
+
+except:
+ try: # Django 1.4
+ from django.contrib.staticfiles.storage import staticfiles_storage
+
+ @register.simple_tag
+ def static(path):
+ """
+ A template tag that returns the URL to a file
+ using staticfiles' storage backend
+ """
+ return staticfiles_storage.url(path)
+
+ except: # Django 1.3
+ from urlparse import urljoin
+ from django import template
+ from django.templatetags.static import PrefixNode
+
+ class StaticNode(template.Node):
+ def __init__(self, varname=None, path=None):
+ if path is None:
+ raise template.TemplateSyntaxError(
+ "Static template nodes must be given a path to return.")
+ self.path = path
+ self.varname = varname
+
+ def url(self, context):
+ path = self.path.resolve(context)
+ return self.handle_simple(path)
+
+ def render(self, context):
+ url = self.url(context)
+ if self.varname is None:
+ return url
+ context[self.varname] = url
+ return ''
+
+ @classmethod
+ def handle_simple(cls, path):
+ return urljoin(PrefixNode.handle_simple("STATIC_URL"), path)
+
+ @classmethod
+ def handle_token(cls, parser, token):
+ """
+ Class method to parse prefix node and return a Node.
+ """
+ bits = token.split_contents()
+
+ if len(bits) < 2:
+ raise template.TemplateSyntaxError(
+ "'%s' takes at least one argument (path to file)" % bits[0])
+
+ path = parser.compile_filter(bits[1])
+
+ if len(bits) >= 2 and bits[-2] == 'as':
+ varname = bits[3]
+ else:
+ varname = None
+
+ return cls(varname, path)
+
+ @register.tag('static')
+ def do_static_13(parser, token):
+ return StaticNode.handle_token(parser, token)
+
+
def replace_query_param(url, key, val):
"""
Given a URL and a key/val pair, set or replace an item in the query
diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py
index b1da5b6f..8c0bfc47 100644
--- a/rest_framework/tests/authentication.py
+++ b/rest_framework/tests/authentication.py
@@ -1,15 +1,13 @@
-from django.conf.urls.defaults import patterns, include
from django.contrib.auth.models import User
+from django.http import HttpResponse
from django.test import Client, TestCase
-
from django.utils import simplejson as json
-from django.http import HttpResponse
-from rest_framework.views import APIView
from rest_framework import permissions
-
from rest_framework.authtoken.models import Token
from rest_framework.authentication import TokenAuthentication
+from rest_framework.compat import patterns
+from rest_framework.views import APIView
import base64
@@ -27,7 +25,7 @@ MockView.authentication_classes += (TokenAuthentication,)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
- (r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'),
+ (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
)
@@ -157,7 +155,7 @@ class TokenAuthTests(TestCase):
def test_token_login_json(self):
"""Ensure token login view using JSON POST works."""
client = Client(enforce_csrf_checks=True)
- response = client.post('/auth-token/login/',
+ response = client.post('/auth-token/',
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)
@@ -165,21 +163,21 @@ class TokenAuthTests(TestCase):
def test_token_login_json_bad_creds(self):
"""Ensure token login view using JSON POST fails if bad credentials are used."""
client = Client(enforce_csrf_checks=True)
- response = client.post('/auth-token/login/',
+ response = client.post('/auth-token/',
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
- self.assertEqual(response.status_code, 401)
+ self.assertEqual(response.status_code, 400)
def test_token_login_json_missing_fields(self):
"""Ensure token login view using JSON POST fails if missing fields."""
client = Client(enforce_csrf_checks=True)
- response = client.post('/auth-token/login/',
+ response = client.post('/auth-token/',
json.dumps({'username': self.username}), 'application/json')
- self.assertEqual(response.status_code, 401)
+ self.assertEqual(response.status_code, 400)
def test_token_login_form(self):
"""Ensure token login view using form POST works."""
client = Client(enforce_csrf_checks=True)
- response = client.post('/auth-token/login/',
+ response = client.post('/auth-token/',
{'username': self.username, 'password': self.password})
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)
diff --git a/rest_framework/tests/breadcrumbs.py b/rest_framework/tests/breadcrumbs.py
index 647ab96d..df891683 100644
--- a/rest_framework/tests/breadcrumbs.py
+++ b/rest_framework/tests/breadcrumbs.py
@@ -1,5 +1,5 @@
-from django.conf.urls.defaults import patterns, url
from django.test import TestCase
+from rest_framework.compat import patterns, url
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.views import APIView
diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py
index 41864d71..8079c8cb 100644
--- a/rest_framework/tests/decorators.py
+++ b/rest_framework/tests/decorators.py
@@ -1,7 +1,7 @@
from django.test import TestCase
+from django.test.client import RequestFactory
from rest_framework import status
from rest_framework.response import Response
-from django.test.client import RequestFactory
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from rest_framework.authentication import BasicAuthentication
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index b6d21847..a877574e 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import six
+from django.db import models
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import simplejson as json
@@ -178,7 +179,7 @@ class TestInstanceView(TestCase):
content = {'text': 'foobar'}
request = factory.put('/1', json.dumps(content),
content_type='application/json')
- response = self.view(request, pk=1).render()
+ response = self.view(request, pk='1').render()
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
updated = self.objects.get(id=1)
@@ -305,3 +306,36 @@ class TestCreateModelWithAutoNowAddField(TestCase):
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
created = self.objects.get(id=1)
self.assertEquals(created.content, 'foobar')
+
+
+# Test for particularly ugly reression with m2m in browseable API
+class ClassB(models.Model):
+ name = models.CharField(max_length=255)
+
+
+class ClassA(models.Model):
+ name = models.CharField(max_length=255)
+ childs = models.ManyToManyField(ClassB, blank=True, null=True)
+
+
+class ClassASerializer(serializers.ModelSerializer):
+ childs = serializers.ManyPrimaryKeyRelatedField(source='childs')
+
+ class Meta:
+ model = ClassA
+
+
+class ExampleView(generics.ListCreateAPIView):
+ serializer_class = ClassASerializer
+ model = ClassA
+
+
+class TestM2MBrowseableAPI(TestCase):
+ def test_m2m_in_browseable_api(self):
+ """
+ Test for particularly ugly reression with m2m in browseable API
+ """
+ request = factory.get('/', HTTP_ACCEPT='text/html')
+ view = ExampleView().as_view()
+ response = view(request).render()
+ self.assertEquals(response.status_code, status.HTTP_200_OK)
diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py
index cef3ffe9..d4662465 100644
--- a/rest_framework/tests/htmlrenderer.py
+++ b/rest_framework/tests/htmlrenderer.py
@@ -1,11 +1,11 @@
import six
from django.core.exceptions import PermissionDenied
-from django.conf.urls.defaults import patterns, url
from django.http import Http404
from django.test import TestCase
from django.template import TemplateDoesNotExist, Template
import django.template.loader
+from rest_framework.compat import patterns, url
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py
index d7effce7..ee4d8e57 100644
--- a/rest_framework/tests/hyperlinkedserializers.py
+++ b/rest_framework/tests/hyperlinkedserializers.py
@@ -1,7 +1,8 @@
-from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from django.test.client import RequestFactory
+from django.utils import simplejson as json
from rest_framework import generics, status, serializers
+from rest_framework.compat import patterns, url
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
factory = RequestFactory()
@@ -54,10 +55,12 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView):
model = BlogPostComment
serializer_class = BlogPostCommentSerializer
+
class BlogPostCommentDetail(generics.RetrieveAPIView):
model = BlogPostComment
serializer_class = BlogPostCommentSerializer
+
class BlogPostDetail(generics.RetrieveAPIView):
model = BlogPost
@@ -71,7 +74,7 @@ class AlbumDetail(generics.RetrieveAPIView):
model = Album
-class OptionalRelationDetail(generics.RetrieveAPIView):
+class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
model = OptionalRelationModel
model_serializer_class = serializers.HyperlinkedModelSerializer
@@ -162,7 +165,7 @@ class TestManyToManyHyperlinkedView(TestCase):
GET requests to ListCreateAPIView should return list of objects.
"""
request = factory.get('/manytomany/')
- response = self.list_view(request).render()
+ response = self.list_view(request)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data)
@@ -171,7 +174,7 @@ class TestManyToManyHyperlinkedView(TestCase):
GET requests to ListCreateAPIView should return list of objects.
"""
request = factory.get('/manytomany/1/')
- response = self.detail_view(request, pk=1).render()
+ response = self.detail_view(request, pk=1)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data[0])
@@ -194,7 +197,7 @@ class TestCreateWithForeignKeys(TestCase):
}
request = factory.post('/comments/', data=data)
- response = self.create_view(request).render()
+ response = self.create_view(request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response['Location'], 'http://testserver/comments/1/')
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
@@ -219,7 +222,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
}
request = factory.post('/photos/', data=data)
- response = self.list_create_view(request).render()
+ response = self.list_create_view(request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
self.assertEqual(self.post.photo_set.count(), 1)
@@ -244,6 +247,16 @@ class TestOptionalRelationHyperlinkedView(TestCase):
for non existing relations.
"""
request = factory.get('/optionalrelationmodel-detail/1')
- response = self.detail_view(request, pk=1).render()
+ response = self.detail_view(request, pk=1)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data)
+
+ def test_put_detail_view(self):
+ """
+ PUT requests to RetrieveUpdateDestroyAPIView with optional relations
+ should accept None for non existing relations.
+ """
+ response = self.client.put('/optionalrelation/1/',
+ data=json.dumps(self.data),
+ content_type='application/json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index c35861c6..0759650a 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -51,6 +51,11 @@ class RESTFrameworkModel(models.Model):
abstract = True
+class HasPositiveIntegerAsChoice(RESTFrameworkModel):
+ some_choices = ((1, 'A'), (2, 'B'), (3, 'C'))
+ some_integer = models.PositiveIntegerField(choices=some_choices)
+
+
class Anchor(RESTFrameworkModel):
text = models.CharField(max_length=100, default='anchor')
@@ -124,8 +129,21 @@ class ActionItem(RESTFrameworkModel):
# Models for reverse relations
+class Person(RESTFrameworkModel):
+ name = models.CharField(max_length=10)
+ age = models.IntegerField(null=True, blank=True)
+
+ @property
+ def info(self):
+ return {
+ 'name': self.name,
+ 'age': self.age,
+ }
+
+
class BlogPost(RESTFrameworkModel):
title = models.CharField(max_length=100)
+ writer = models.ForeignKey(Person, null=True, blank=True)
def get_first_comment(self):
return self.blogpostcomment_set.all()[0]
@@ -145,21 +163,9 @@ class Photo(RESTFrameworkModel):
album = models.ForeignKey(Album)
-class Person(RESTFrameworkModel):
- name = models.CharField(max_length=10)
- age = models.IntegerField(null=True, blank=True)
-
- @property
- def info(self):
- return {
- 'name': self.name,
- 'age': self.age,
- }
-
-
# Model for issue #324
class BlankFieldModel(RESTFrameworkModel):
- title = models.CharField(max_length=100, blank=True)
+ title = models.CharField(max_length=100, blank=True, null=False)
# Model for issue #380
diff --git a/rest_framework/tests/modelviews.py b/rest_framework/tests/modelviews.py
index 1f8468e8..f12e3b97 100644
--- a/rest_framework/tests/modelviews.py
+++ b/rest_framework/tests/modelviews.py
@@ -1,4 +1,4 @@
-# from django.conf.urls.defaults import patterns, url
+# from rest_framework.compat import patterns, url
# from django.forms import ModelForm
# from django.contrib.auth.models import Group, User
# from rest_framework.resources import ModelResource
diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py
index 3062007d..81d297a1 100644
--- a/rest_framework/tests/pagination.py
+++ b/rest_framework/tests/pagination.py
@@ -4,7 +4,7 @@ from django.core.paginator import Paginator
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import unittest
-from rest_framework import generics, status, pagination, filters
+from rest_framework import generics, status, pagination, filters, serializers
from rest_framework.compat import django_filters
from rest_framework.tests.models import BasicModel, FilterableItem
@@ -148,6 +148,11 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.assertEquals(response.data['previous'], None)
+class PassOnContextPaginationSerializer(pagination.PaginationSerializer):
+ class Meta:
+ object_serializer_class = serializers.Serializer
+
+
class UnitTestPagination(TestCase):
"""
Unit tests for pagination of primitive objects.
@@ -172,6 +177,15 @@ class UnitTestPagination(TestCase):
self.assertEquals(serializer.data['previous'], '?page=2')
self.assertEquals(serializer.data['results'], self.objects[20:])
+ def test_context_available_in_result(self):
+ """
+ Ensure context gets passed through to the object serializer.
+ """
+ serializer = PassOnContextPaginationSerializer(self.first_page)
+ serializer.data
+ results = serializer.fields[serializer.results_field]
+ self.assertTrue(serializer.context is results.context)
+
class TestUnpaginated(TestCase):
"""
@@ -236,3 +250,32 @@ class TestCustomPaginateByParam(TestCase):
response = self.view(request).render()
self.assertEquals(response.data['count'], 13)
self.assertEquals(response.data['results'], self.data[:5])
+
+
+class CustomField(serializers.Field):
+ def to_native(self, value):
+ if not 'view' in self.context:
+ raise RuntimeError("context isn't getting passed into custom field")
+ return "value"
+
+
+class BasicModelSerializer(serializers.Serializer):
+ text = CustomField()
+
+
+class TestContextPassedToCustomField(TestCase):
+ def setUp(self):
+ BasicModel.objects.create(text='ala ma kota')
+
+ def test_with_pagination(self):
+ class ListView(generics.ListCreateAPIView):
+ model = BasicModel
+ serializer_class = BasicModelSerializer
+ paginate_by = 1
+
+ self.view = ListView.as_view()
+ request = factory.get('/')
+ response = self.view(request).render()
+
+ self.assertEquals(response.status_code, status.HTTP_200_OK)
+
diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py
new file mode 100644
index 00000000..0a7ea0f4
--- /dev/null
+++ b/rest_framework/tests/relations_hyperlink.py
@@ -0,0 +1,434 @@
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+from rest_framework.compat import patterns, url
+
+
+def dummy_view(request, pk):
+ pass
+
+urlpatterns = patterns('',
+ url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
+ url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
+ url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
+ url(r'^foreignkeytarget/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'),
+ url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
+)
+
+
+# ManyToMany
+
+class ManyToManyTarget(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManySource(models.Model):
+ name = models.CharField(max_length=100)
+ targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
+
+
+class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer):
+ sources = serializers.ManyHyperlinkedRelatedField(view_name='manytomanysource-detail')
+
+ class Meta:
+ model = ManyToManyTarget
+
+
+class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = ManyToManySource
+
+
+# ForeignKey
+
+class ForeignKeyTarget(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ForeignKeySource(models.Model):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
+
+
+class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer):
+ sources = serializers.ManyHyperlinkedRelatedField(view_name='foreignkeysource-detail')
+
+ class Meta:
+ model = ForeignKeyTarget
+
+
+class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = ForeignKeySource
+
+
+# Nullable ForeignKey
+
+class NullableForeignKeySource(models.Model):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
+ related_name='nullable_sources')
+
+
+class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = NullableForeignKeySource
+
+
+# TODO: Add test that .data cannot be accessed prior to .is_valid
+
+class HyperlinkedManyToManyTests(TestCase):
+ urls = 'rest_framework.tests.relations_hyperlink'
+
+ def setUp(self):
+ for idx in range(1, 4):
+ target = ManyToManyTarget(name='target-%d' % idx)
+ target.save()
+ source = ManyToManySource(name='source-%d' % idx)
+ source.save()
+ for target in ManyToManyTarget.objects.all():
+ source.targets.add(target)
+
+ def test_many_to_many_retrieve(self):
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(queryset)
+ expected = [
+ {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']},
+ {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
+ {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_many_to_many_retrieve(self):
+ queryset = ManyToManyTarget.objects.all()
+ serializer = ManyToManyTargetSerializer(queryset)
+ expected = [
+ {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']},
+ {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
+ {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_many_to_many_update(self):
+ data = {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
+ instance = ManyToManySource.objects.get(pk=1)
+ serializer = ManyToManySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ self.assertEquals(serializer.data, data)
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(queryset)
+ expected = [
+ {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']},
+ {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
+ {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_many_to_many_update(self):
+ data = {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']}
+ instance = ManyToManyTarget.objects.get(pk=1)
+ serializer = ManyToManyTargetSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ self.assertEquals(serializer.data, data)
+
+ # Ensure target 1 is updated, and everything else is as expected
+ queryset = ManyToManyTarget.objects.all()
+ serializer = ManyToManyTargetSerializer(queryset)
+ expected = [
+ {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']},
+ {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
+ {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}
+
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_many_to_many_create(self):
+ data = {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']}
+ serializer = ManyToManySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is added, and everything else is as expected
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(queryset)
+ expected = [
+ {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']},
+ {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']},
+ {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']},
+ {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_many_to_many_create(self):
+ data = {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']}
+ serializer = ManyToManyTargetSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'target-4')
+
+ # Ensure target 4 is added, and everything else is as expected
+ queryset = ManyToManyTarget.objects.all()
+ serializer = ManyToManyTargetSerializer(queryset)
+ expected = [
+ {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']},
+ {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']},
+ {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']},
+ {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+
+class HyperlinkedForeignKeyTests(TestCase):
+ urls = 'rest_framework.tests.relations_hyperlink'
+
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ new_target = ForeignKeyTarget(name='target-2')
+ new_target.save()
+ for idx in range(1, 4):
+ source = ForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve(self):
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
+ {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
+ {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_retrieve(self):
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']},
+ {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update(self):
+ data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'},
+ {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
+ {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_update(self):
+ data = {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}
+ instance = ForeignKeyTarget.objects.get(pk=2)
+ serializer = ForeignKeyTargetSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ # We shouldn't have saved anything to the db yet since save
+ # hasn't been called.
+ queryset = ForeignKeyTarget.objects.all()
+ new_serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']},
+ {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []},
+ ]
+ self.assertEquals(new_serializer.data, expected)
+
+ serializer.save()
+ self.assertEquals(serializer.data, data)
+
+ # Ensure target 2 is update, and everything else is as expected
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']},
+ {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create(self):
+ data = {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'}
+ serializer = ForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
+ {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
+ {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'},
+ {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_create(self):
+ data = {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}
+ serializer = ForeignKeyTargetSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'target-3')
+
+ # Ensure target 4 is added, and everything else is as expected
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']},
+ {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []},
+ {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_invalid_null(self):
+ data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': None}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'target': [u'Value may not be null']})
+
+
+class HyperlinkedNullableForeignKeyTests(TestCase):
+ urls = 'rest_framework.tests.relations_hyperlink'
+
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ for idx in range(1, 4):
+ if idx == 3:
+ target = None
+ source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve_with_null(self):
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
+ {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
+ {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create_with_valid_null(self):
+ data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
+ serializer = NullableForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is created, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
+ {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
+ {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
+ {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create_with_valid_emptystring(self):
+ """
+ The emptystring should be interpreted as null in the context
+ of relationships.
+ """
+ data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': ''}
+ expected_data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
+ serializer = NullableForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, expected_data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is created, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'},
+ {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
+ {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
+ {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_valid_null(self):
+ data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}
+ instance = NullableForeignKeySource.objects.get(pk=1)
+ serializer = NullableForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None},
+ {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
+ {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_valid_emptystring(self):
+ """
+ The emptystring should be interpreted as null in the context
+ of relationships.
+ """
+ data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': ''}
+ expected_data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}
+ instance = NullableForeignKeySource.objects.get(pk=1)
+ serializer = NullableForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, expected_data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None},
+ {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'},
+ {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ # reverse foreign keys MUST be read_only
+ # In the general case they do not provide .remove() or .clear()
+ # and cannot be arbitrarily set.
+
+ # def test_reverse_foreign_key_update(self):
+ # data = {'id': 1, 'name': u'target-1', 'sources': [1]}
+ # instance = ForeignKeyTarget.objects.get(pk=1)
+ # serializer = ForeignKeyTargetSerializer(instance, data=data)
+ # self.assertTrue(serializer.is_valid())
+ # self.assertEquals(serializer.data, data)
+ # serializer.save()
+
+ # # Ensure target 1 is updated, and everything else is as expected
+ # queryset = ForeignKeyTarget.objects.all()
+ # serializer = ForeignKeyTargetSerializer(queryset)
+ # expected = [
+ # {'id': 1, 'name': u'target-1', 'sources': [1]},
+ # {'id': 2, 'name': u'target-2', 'sources': []},
+ # ]
+ # self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py
new file mode 100644
index 00000000..b1147378
--- /dev/null
+++ b/rest_framework/tests/relations_nested.py
@@ -0,0 +1,102 @@
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+# ForeignKey
+
+class ForeignKeyTarget(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ForeignKeySource(models.Model):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
+
+
+class ForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ depth = 1
+ model = ForeignKeySource
+
+
+class FlatForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeySource
+
+
+class ForeignKeyTargetSerializer(serializers.ModelSerializer):
+ sources = FlatForeignKeySourceSerializer()
+
+ class Meta:
+ model = ForeignKeyTarget
+
+
+# Nullable ForeignKey
+
+class NullableForeignKeySource(models.Model):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
+ related_name='nullable_sources')
+
+
+class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ depth = 1
+ model = NullableForeignKeySource
+
+
+class ReverseForeignKeyTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ new_target = ForeignKeyTarget(name='target-2')
+ new_target.save()
+ for idx in range(1, 4):
+ source = ForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve(self):
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}},
+ {'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}},
+ {'id': 3, 'name': u'source-3', 'target': {'id': 1, 'name': u'target-1'}},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_retrieve(self):
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1},
+ ]},
+ {'id': 2, 'name': u'target-2', 'sources': [
+ ]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+
+class NestedNullableForeignKeyTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ for idx in range(1, 4):
+ if idx == 3:
+ target = None
+ source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve_with_null(self):
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}},
+ {'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}},
+ {'id': 3, 'name': u'source-3', 'target': None},
+ ]
+ self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py
new file mode 100644
index 00000000..28967099
--- /dev/null
+++ b/rest_framework/tests/relations_pk.py
@@ -0,0 +1,414 @@
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+# ManyToMany
+
+class ManyToManyTarget(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManySource(models.Model):
+ name = models.CharField(max_length=100)
+ targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
+
+
+class ManyToManyTargetSerializer(serializers.ModelSerializer):
+ sources = serializers.ManyPrimaryKeyRelatedField()
+
+ class Meta:
+ model = ManyToManyTarget
+
+
+class ManyToManySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ManyToManySource
+
+
+# ForeignKey
+
+class ForeignKeyTarget(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ForeignKeySource(models.Model):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
+
+
+class ForeignKeyTargetSerializer(serializers.ModelSerializer):
+ sources = serializers.ManyPrimaryKeyRelatedField()
+
+ class Meta:
+ model = ForeignKeyTarget
+
+
+class ForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeySource
+
+
+# Nullable ForeignKey
+
+class NullableForeignKeySource(models.Model):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
+ related_name='nullable_sources')
+
+
+class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = NullableForeignKeySource
+
+
+# TODO: Add test that .data cannot be accessed prior to .is_valid
+
+class PKManyToManyTests(TestCase):
+ def setUp(self):
+ for idx in range(1, 4):
+ target = ManyToManyTarget(name='target-%d' % idx)
+ target.save()
+ source = ManyToManySource(name='source-%d' % idx)
+ source.save()
+ for target in ManyToManyTarget.objects.all():
+ source.targets.add(target)
+
+ def test_many_to_many_retrieve(self):
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'targets': [1]},
+ {'id': 2, 'name': u'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_many_to_many_retrieve(self):
+ queryset = ManyToManyTarget.objects.all()
+ serializer = ManyToManyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
+ {'id': 2, 'name': u'target-2', 'sources': [2, 3]},
+ {'id': 3, 'name': u'target-3', 'sources': [3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_many_to_many_update(self):
+ data = {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]}
+ instance = ManyToManySource.objects.get(pk=1)
+ serializer = ManyToManySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ self.assertEquals(serializer.data, data)
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]},
+ {'id': 2, 'name': u'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_many_to_many_update(self):
+ data = {'id': 1, 'name': u'target-1', 'sources': [1]}
+ instance = ManyToManyTarget.objects.get(pk=1)
+ serializer = ManyToManyTargetSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ self.assertEquals(serializer.data, data)
+
+ # Ensure target 1 is updated, and everything else is as expected
+ queryset = ManyToManyTarget.objects.all()
+ serializer = ManyToManyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [1]},
+ {'id': 2, 'name': u'target-2', 'sources': [2, 3]},
+ {'id': 3, 'name': u'target-3', 'sources': [3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_many_to_many_create(self):
+ data = {'id': 4, 'name': u'source-4', 'targets': [1, 3]}
+ serializer = ManyToManySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is added, and everything else is as expected
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'targets': [1]},
+ {'id': 2, 'name': u'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]},
+ {'id': 4, 'name': u'source-4', 'targets': [1, 3]},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_many_to_many_create(self):
+ data = {'id': 4, 'name': u'target-4', 'sources': [1, 3]}
+ serializer = ManyToManyTargetSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'target-4')
+
+ # Ensure target 4 is added, and everything else is as expected
+ queryset = ManyToManyTarget.objects.all()
+ serializer = ManyToManyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
+ {'id': 2, 'name': u'target-2', 'sources': [2, 3]},
+ {'id': 3, 'name': u'target-3', 'sources': [3]},
+ {'id': 4, 'name': u'target-4', 'sources': [1, 3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+
+class PKForeignKeyTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ new_target = ForeignKeyTarget(name='target-2')
+ new_target.save()
+ for idx in range(1, 4):
+ source = ForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve(self):
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_retrieve(self):
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
+ {'id': 2, 'name': u'target-2', 'sources': []},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update(self):
+ data = {'id': 1, 'name': u'source-1', 'target': 2}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 2},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_update(self):
+ data = {'id': 2, 'name': u'target-2', 'sources': [1, 3]}
+ instance = ForeignKeyTarget.objects.get(pk=2)
+ serializer = ForeignKeyTargetSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ # We shouldn't have saved anything to the db yet since save
+ # hasn't been called.
+ queryset = ForeignKeyTarget.objects.all()
+ new_serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
+ {'id': 2, 'name': u'target-2', 'sources': []},
+ ]
+ self.assertEquals(new_serializer.data, expected)
+
+ serializer.save()
+ self.assertEquals(serializer.data, data)
+
+ # Ensure target 2 is update, and everything else is as expected
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [2]},
+ {'id': 2, 'name': u'target-2', 'sources': [1, 3]},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create(self):
+ data = {'id': 4, 'name': u'source-4', 'target': 2}
+ serializer = ForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is added, and everything else is as expected
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1},
+ {'id': 4, 'name': u'source-4', 'target': 2},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_create(self):
+ data = {'id': 3, 'name': u'target-3', 'sources': [1, 3]}
+ serializer = ForeignKeyTargetSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'target-3')
+
+ # Ensure target 3 is added, and everything else is as expected
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [2]},
+ {'id': 2, 'name': u'target-2', 'sources': []},
+ {'id': 3, 'name': u'target-3', 'sources': [1, 3]},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_invalid_null(self):
+ data = {'id': 1, 'name': u'source-1', 'target': None}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'target': [u'Value may not be null']})
+
+
+class PKNullableForeignKeyTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ for idx in range(1, 4):
+ if idx == 3:
+ target = None
+ source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve_with_null(self):
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': None},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create_with_valid_null(self):
+ data = {'id': 4, 'name': u'source-4', 'target': None}
+ serializer = NullableForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is created, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': None},
+ {'id': 4, 'name': u'source-4', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_create_with_valid_emptystring(self):
+ """
+ The emptystring should be interpreted as null in the context
+ of relationships.
+ """
+ data = {'id': 4, 'name': u'source-4', 'target': ''}
+ expected_data = {'id': 4, 'name': u'source-4', 'target': None}
+ serializer = NullableForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, expected_data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is created, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': None},
+ {'id': 4, 'name': u'source-4', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_valid_null(self):
+ data = {'id': 1, 'name': u'source-1', 'target': None}
+ instance = NullableForeignKeySource.objects.get(pk=1)
+ serializer = NullableForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': None},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_valid_emptystring(self):
+ """
+ The emptystring should be interpreted as null in the context
+ of relationships.
+ """
+ data = {'id': 1, 'name': u'source-1', 'target': ''}
+ expected_data = {'id': 1, 'name': u'source-1', 'target': None}
+ instance = NullableForeignKeySource.objects.get(pk=1)
+ serializer = NullableForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, expected_data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': None},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ # reverse foreign keys MUST be read_only
+ # In the general case they do not provide .remove() or .clear()
+ # and cannot be arbitrarily set.
+
+ # def test_reverse_foreign_key_update(self):
+ # data = {'id': 1, 'name': u'target-1', 'sources': [1]}
+ # instance = ForeignKeyTarget.objects.get(pk=1)
+ # serializer = ForeignKeyTargetSerializer(instance, data=data)
+ # self.assertTrue(serializer.is_valid())
+ # self.assertEquals(serializer.data, data)
+ # serializer.save()
+
+ # # Ensure target 1 is updated, and everything else is as expected
+ # queryset = ForeignKeyTarget.objects.all()
+ # serializer = ForeignKeyTargetSerializer(queryset)
+ # expected = [
+ # {'id': 1, 'name': u'target-1', 'sources': [1]},
+ # {'id': 2, 'name': u'target-2', 'sources': []},
+ # ]
+ # self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py
index 79ace78d..b02fccf4 100644
--- a/rest_framework/tests/renderers.py
+++ b/rest_framework/tests/renderers.py
@@ -2,13 +2,12 @@ import pickle
import re
import six
-from django.conf.urls.defaults import patterns, url, include
from django.core.cache import cache
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework import status, permissions
-from rest_framework.compat import yaml
+from rest_framework.compat import yaml, patterns, url, include
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
@@ -448,19 +447,19 @@ class CacheRenderTest(TestCase):
return
if state == None:
return
- if isinstance(state,tuple):
- if not isinstance(state[0],dict):
- state=state[1]
+ if isinstance(state, tuple):
+ if not isinstance(state[0], dict):
+ state = state[1]
else:
- state=state[0].update(state[1])
+ state = state[0].update(state[1])
result = {}
for i in state:
try:
- pickle.dumps(state[i],protocol=2)
+ pickle.dumps(state[i], protocol=2)
except pickle.PicklingError:
if not state[i] in seen:
seen.append(state[i])
- result[i] = cls._get_pickling_errors(state[i],seen)
+ result[i] = cls._get_pickling_errors(state[i], seen)
return result
def http_resp(self, http_method, url):
diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py
index 68cfd029..fe5116a8 100644
--- a/rest_framework/tests/request.py
+++ b/rest_framework/tests/request.py
@@ -3,14 +3,15 @@ Tests for content parsing, and form-overloaded content parsing.
"""
import six
-from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
+from django.contrib.auth import authenticate, login, logout
+from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, Client
+from django.test.client import RequestFactory
from django.utils import simplejson as json
-
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
-from django.test.client import RequestFactory
+from rest_framework.compat import patterns
from rest_framework.parsers import (
BaseParser,
FormParser,
@@ -278,3 +279,37 @@ class TestContentParsingWithAuthentication(TestCase):
# response = self.csrf_client.post('/', content)
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
+
+
+class TestUserSetter(TestCase):
+
+ def setUp(self):
+ # Pass request object through session middleware so session is
+ # available to login and logout functions
+ self.request = Request(factory.get('/'))
+ SessionMiddleware().process_request(self.request)
+
+ User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow')
+ self.user = authenticate(username='ringo', password='yellow')
+
+ def test_user_can_be_set(self):
+ self.request.user = self.user
+ self.assertEqual(self.request.user, self.user)
+
+ def test_user_can_login(self):
+ login(self.request, self.user)
+ self.assertEqual(self.request.user, self.user)
+
+ def test_user_can_logout(self):
+ self.request.user = self.user
+ self.assertFalse(self.request.user.is_anonymous())
+ logout(self.request)
+ self.assertTrue(self.request.user.is_anonymous())
+
+
+class TestAuthSetter(TestCase):
+
+ def test_auth_can_be_set(self):
+ request = Request(factory.get('/'))
+ request.auth = 'DUMMY'
+ self.assertEqual(request.auth, 'DUMMY')
diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py
index 237b12a9..fd153f40 100644
--- a/rest_framework/tests/response.py
+++ b/rest_framework/tests/response.py
@@ -1,9 +1,8 @@
import unittest
import six
-from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase
-
+from rest_framework.compat import patterns, url, include
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
diff --git a/rest_framework/tests/reverse.py b/rest_framework/tests/reverse.py
index fd9a7d64..8c86e1fb 100644
--- a/rest_framework/tests/reverse.py
+++ b/rest_framework/tests/reverse.py
@@ -1,6 +1,6 @@
-from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from django.test.client import RequestFactory
+from rest_framework.compat import patterns, url
from rest_framework.reverse import reverse
factory = RequestFactory()
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 329d27a9..4654882e 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1,11 +1,12 @@
from __future__ import unicode_literals
import datetime
+import pickle
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
+from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
- ManyToManyModel, Person, ReadOnlyManyToManyModel)
+ ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
class SubComment(object):
@@ -64,6 +65,18 @@ class PersonSerializer(serializers.ModelSerializer):
read_only_fields = ('age',)
+class AlbumsSerializer(serializers.ModelSerializer):
+
+ class Meta:
+ model = Album
+ fields = ['title'] # lists are also valid options
+
+class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = HasPositiveIntegerAsChoice
+ fields = ['some_integer']
+
+
class BasicTests(TestCase):
def setUp(self):
self.comment = Comment(
@@ -171,7 +184,7 @@ class ValidationTests(TestCase):
'content': 'x' * 1001,
'created': datetime.datetime(2012, 1, 1)
}
- self.actionitem = ActionItem('Some to do item',
+ self.actionitem = ActionItem(title='Some to do item',
)
def test_create(self):
@@ -279,6 +292,57 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.errors, {'info': ['Ensure this value has at most 12 characters (it has 13).']})
+class PositiveIntegerAsChoiceTests(TestCase):
+ def test_positive_integer_in_json_is_correctly_parsed(self):
+ data = {'some_integer':1}
+ serializer = PositiveIntegerAsChoiceSerializer(data=data)
+ self.assertEquals(serializer.is_valid(), True)
+
+class ModelValidationTests(TestCase):
+ def test_validate_unique(self):
+ """
+ Just check if serializers.ModelSerializer handles unique checks via .full_clean()
+ """
+ serializer = AlbumsSerializer(data={'title': 'a'})
+ serializer.is_valid()
+ serializer.save()
+ second_serializer = AlbumsSerializer(data={'title': 'a'})
+ self.assertFalse(second_serializer.is_valid())
+ self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']})
+
+ def test_foreign_key_with_partial(self):
+ """
+ Test ModelSerializer validation with partial=True
+
+ Specifically test foreign key validation.
+ """
+
+ album = Album(title='test')
+ album.save()
+
+ class PhotoSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Photo
+
+ photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
+ self.assertTrue(photo_serializer.is_valid())
+ photo = photo_serializer.save()
+
+ # Updating only the album (foreign key)
+ photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True)
+ self.assertTrue(photo_serializer.is_valid())
+ self.assertTrue(photo_serializer.save())
+
+ # Updating only the description
+ photo_serializer = PhotoSerializer(instance=photo,
+ data={'description': 'new'},
+ partial=True)
+
+ self.assertTrue(photo_serializer.is_valid())
+ self.assertTrue(photo_serializer.save())
+
+
+
class RegexValidationTest(TestCase):
def test_create_failed(self):
serializer = BookSerializer(data={'isbn': '1234567890'})
@@ -562,6 +626,47 @@ class ManyRelatedTests(TestCase):
self.assertEqual(serializer.data, expected)
+class RelatedTraversalTest(TestCase):
+ def test_nested_traversal(self):
+ user = Person.objects.create(name="django")
+ post = BlogPost.objects.create(title="Test blog post", writer=user)
+ post.blogpostcomment_set.create(text="I love this blog post")
+
+ from rest_framework.tests.models import BlogPostComment
+
+ class PersonSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Person
+ fields = ("name", "age")
+
+ class BlogPostCommentSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BlogPostComment
+ fields = ("text", "post_owner")
+
+ text = serializers.CharField()
+ post_owner = PersonSerializer(source='blog_post.writer')
+
+ class BlogPostSerializer(serializers.Serializer):
+ title = serializers.CharField()
+ comments = BlogPostCommentSerializer(source='blogpostcomment_set')
+
+ serializer = BlogPostSerializer(instance=post)
+
+ expected = {
+ 'title': u'Test blog post',
+ 'comments': [{
+ 'text': u'I love this blog post',
+ 'post_owner': {
+ "name": u"django",
+ "age": None
+ }
+ }]
+ }
+
+ self.assertEqual(serializer.data, expected)
+
+
class SerializerMethodFieldTests(TestCase):
def setUp(self):
@@ -628,6 +733,10 @@ class BlankFieldTests(TestCase):
serializer = self.model_serializer_class(data=self.data)
self.assertEquals(serializer.is_valid(), True)
+ def test_create_model_null_field(self):
+ serializer = self.model_serializer_class(data={'title': None})
+ self.assertEquals(serializer.is_valid(), True)
+
def test_create_not_blank_field(self):
"""
Test to ensure blank data in a field not marked as blank=True
@@ -643,3 +752,120 @@ class BlankFieldTests(TestCase):
"""
serializer = self.not_blank_model_serializer_class(data=self.data)
self.assertEquals(serializer.is_valid(), False)
+
+ def test_create_model_null_field(self):
+ serializer = self.model_serializer_class(data={})
+ self.assertEquals(serializer.is_valid(), True)
+
+
+#test for issue #460
+class SerializerPickleTests(TestCase):
+ """
+ Test pickleability of the output of Serializers
+ """
+ def test_pickle_simple_model_serializer_data(self):
+ """
+ Test simple serializer
+ """
+ pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data)
+
+ def test_pickle_inner_serializer(self):
+ """
+ Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will
+ have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle.
+ See DictWithMetadata.__getstate__
+ """
+ class InnerPersonSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Person
+ fields = ('name', 'age')
+ pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data)
+
+
+class DepthTest(TestCase):
+ def test_implicit_nesting(self):
+ writer = Person.objects.create(name="django", age=1)
+ post = BlogPost.objects.create(title="Test blog post", writer=writer)
+
+ class BlogPostSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BlogPost
+ depth = 1
+
+ serializer = BlogPostSerializer(instance=post)
+ expected = {'id': 1, 'title': u'Test blog post',
+ 'writer': {'id': 1, 'name': u'django', 'age': 1}}
+
+ self.assertEqual(serializer.data, expected)
+
+ def test_explicit_nesting(self):
+ writer = Person.objects.create(name="django", age=1)
+ post = BlogPost.objects.create(title="Test blog post", writer=writer)
+
+ class PersonSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Person
+
+ class BlogPostSerializer(serializers.ModelSerializer):
+ writer = PersonSerializer()
+
+ class Meta:
+ model = BlogPost
+
+ serializer = BlogPostSerializer(instance=post)
+ expected = {'id': 1, 'title': u'Test blog post',
+ 'writer': {'id': 1, 'name': u'django', 'age': 1}}
+
+ self.assertEqual(serializer.data, expected)
+
+
+class NestedSerializerContextTests(TestCase):
+
+ def test_nested_serializer_context(self):
+ """
+ Regression for #497
+
+ https://github.com/tomchristie/django-rest-framework/issues/497
+ """
+ class PhotoSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Photo
+ fields = ("description", "callable")
+
+ callable = serializers.SerializerMethodField('_callable')
+
+ def _callable(self, instance):
+ if not 'context_item' in self.context:
+ raise RuntimeError("context isn't getting passed into 2nd level nested serializer")
+ return "success"
+
+ class AlbumSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Album
+ fields = ("photo_set", "callable")
+
+ photo_set = PhotoSerializer(source="photo_set")
+ callable = serializers.SerializerMethodField("_callable")
+
+ def _callable(self, instance):
+ if not 'context_item' in self.context:
+ raise RuntimeError("context isn't getting passed into 1st level nested serializer")
+ return "success"
+
+ class AlbumCollection(object):
+ albums = None
+
+ class AlbumCollectionSerializer(serializers.Serializer):
+ albums = AlbumSerializer(source="albums")
+
+ album1 = Album.objects.create(title="album 1")
+ album2 = Album.objects.create(title="album 2")
+ Photo.objects.create(description="Bigfoot", album=album1)
+ Photo.objects.create(description="Unicorn", album=album1)
+ Photo.objects.create(description="Yeti", album=album2)
+ Photo.objects.create(description="Sasquatch", album=album2)
+ album_collection = AlbumCollection()
+ album_collection.albums = [album1, album2]
+
+ # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
+ AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data
diff --git a/rest_framework/tests/testcases.py b/rest_framework/tests/testcases.py
index c90224aa..97f492ff 100644
--- a/rest_framework/tests/testcases.py
+++ b/rest_framework/tests/testcases.py
@@ -6,6 +6,7 @@ from django.test import TestCase
NO_SETTING = ('!', None)
+
class TestSettingsManager(object):
"""
A class which can modify some Django settings temporarily for a
@@ -19,7 +20,7 @@ class TestSettingsManager(object):
self._original_settings = {}
def set(self, **kwargs):
- for k,v in kwargs.iteritems():
+ for k, v in kwargs.iteritems():
self._original_settings.setdefault(k, getattr(settings, k,
NO_SETTING))
setattr(settings, k, v)
@@ -31,7 +32,7 @@ class TestSettingsManager(object):
call_command('syncdb', verbosity=0)
def revert(self):
- for k,v in self._original_settings.iteritems():
+ for k, v in self._original_settings.iteritems():
if v == NO_SETTING:
delattr(settings, k)
else:
@@ -57,6 +58,7 @@ class SettingsTestCase(TestCase):
def tearDown(self):
self.settings_manager.revert()
+
class TestModelsTestCase(SettingsTestCase):
def setUp(self, *args, **kwargs):
installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',)
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index 0ad926fa..143928c9 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -1,4 +1,4 @@
-from django.conf.urls.defaults import url
+from rest_framework.compat import url
from rest_framework.settings import api_settings
diff --git a/rest_framework/urls.py b/rest_framework/urls.py
index bcdc23e7..fbe4bc07 100644
--- a/rest_framework/urls.py
+++ b/rest_framework/urls.py
@@ -12,7 +12,7 @@ your authentication settings include `SessionAuthentication`.
url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
)
"""
-from django.conf.urls.defaults import patterns, url
+from rest_framework.compat import patterns, url
template_name = {'template_name': 'rest_framework/login.html'}