Merge branch 'master' of git://github.com/tomchristie/django-rest-framework into related_blanks

This commit is contained in:
Ian Strachan 2012-11-04 21:23:45 +00:00
commit d81ded1157
17 changed files with 469 additions and 54 deletions

2
.gitignore vendored
View File

@ -7,7 +7,7 @@ html/
coverage/
build/
dist/
rest_framework.egg-info/
*.egg-info/
MANIFEST
!.gitignore

View File

@ -57,8 +57,28 @@ To run the tests.
# Changelog
## Master
* Minor field improvements (don't stringify dicts, more robust many-pk fields)
## 2.0.2
**Date**: 2nd Nov 2012
* Fix issues with pk related fields in the browsable API.
## 2.0.1
**Date**: 1st Nov 2012
* Add support for relational fields in the browsable API.
* Added SlugRelatedField and ManySlugRelatedField.
* If PUT creates an instance return '201 Created', instead of '200 OK'.
## 2.0.0
**Date**: 30th Oct 2012
* Redesign of core components.
* Fix **all of the things**.

View File

@ -235,44 +235,48 @@ Then an example output format for a Bookmark instance would be:
'url': u'https://www.djangoproject.com/'
}
## PrimaryKeyRelatedField
## PrimaryKeyRelatedField / ManyPrimaryKeyRelatedField
This field can be applied to any "to-one" relationship, such as a `ForeignKey` field.
`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
`PrimaryKeyRelatedField` will represent the target of the field using it's primary key.
Be default these fields read-write, although you can change this behaviour using the `read_only` flag.
Be default, `PrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
**Arguments**:
## ManyPrimaryKeyRelatedField
* `queryset` - All relational fields must either set a queryset, or set `read_only=True`
This field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship.
## SlugRelatedField / ManySlugRelatedField
`PrimaryKeyRelatedField` will represent the targets of the field using their primary key.
`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
Be default, `ManyPrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
Be default these fields read-write, although you can change this behaviour using the `read_only` flag.
## HyperlinkedRelatedField
**Arguments**:
This field can be applied to any "to-one" relationship, such as a `ForeignKey` field.
* `slug_field` - The field on the target that should used as the representation. This should be a field that uniquely identifies any given instance. For example, `username`.
* `queryset` - All relational fields must either set a queryset, or set `read_only=True`
`HyperlinkedRelatedField` will represent the target of the field using a hyperlink. You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the target of the hyperlink.
## HyperlinkedRelatedField / ManyHyperlinkedRelatedField
`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
Be default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
## ManyHyperlinkedRelatedField
**Arguments**:
This field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship.
`ManyHyperlinkedRelatedField` will represent the targets of the field using hyperlinks. You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the target of the hyperlink.
Be default, `ManyHyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `queryset` - All relational fields must either set a queryset, or set `read_only=True`
## HyperLinkedIdentityField
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the model.
This field is always read-only.
**Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
[cite]: http://www.python.org/dev/peps/pep-0020/

View File

@ -66,11 +66,9 @@ If you're intending to use the browseable API you'll want to add REST framework'
Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace.
<!--
## Quickstart
Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running with REST framework.
-->
## Tutorial

View File

@ -53,7 +53,7 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
<ul class="dropdown-menu">
<!--<li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</a></li>-->
<li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</a></li>
<li><a href="{{ base_url }}/tutorial/1-serialization{{ suffix }}">1 - Serialization</a></li>
<li><a href="{{ base_url }}/tutorial/2-requests-and-responses{{ suffix }}">2 - Requests and responses</a></li>
<li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>

View File

@ -53,6 +53,7 @@ The following people have helped make REST framework great.
* Stephan Groß - [minddust]
* Pavel Savchenko - [asfaltboy]
* Otto Yiu - [ottoyiu]
* Jacob Magnusson - [jmagnusson]
Many thanks to everyone who's contributed to the project.
@ -141,3 +142,4 @@ To contact the author directly:
[minddust]: https://github.com/minddust
[asfaltboy]: https://github.com/asfaltboy
[ottoyiu]: https://github.com/OttoYiu
[jmagnusson]: https://github.com/jmagnusson

View File

@ -6,10 +6,26 @@
## Master
* Minor field improvements (don't stringify dicts, more robust many-pk fields)
## 2.0.2
**Date**: 2nd Nov 2012
* Fix issues with pk related fields in the browsable API.
## 2.0.1
**Date**: 1st Nov 2012
* Add support for relational fields in the browsable API.
* Added SlugRelatedField and ManySlugRelatedField.
* If PUT creates an instance return '201 Created', instead of '200 OK'.
## 2.0.0
**Date**: 30th Oct 2012
* **Fix all of the things.** (Well, almost.)
* For more information please see the [2.0 migration guide][migration].

View File

@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t
class GroupSerializer(serializers.HyperlinkedModelSerializer):
permissions = serializers.ManySlugRelatedField(
slug_field='codename',
queryset=Permission.objects.all()
)
class Meta:
model = Group
fields = ('url', 'name', 'permissions')
Notice that we're using hyperlinked relations in this case, with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design.
We've also overridden the `permission` field on the `GroupSerializer`. In this case we don't want to use a hyperlinked representation, but instead use the list of permission codenames associated with the group, so we've used a `ManySlugRelatedField`, using the `codename` field for the representation.
## Views
Right, we'd better write some views then. Open `quickstart/views.py` and get typing.
@ -152,7 +159,7 @@ We can now access our API, both from the command-line, using tools like `curl`..
},
{
"email": "tom@example.com",
"groups": [],
"groups": [ ],
"url": "http://127.0.0.1:8000/users/2/",
"username": "tom"
}

View File

@ -1,3 +1,3 @@
__version__ = '2.0.0'
__version__ = '2.0.2'
VERSION = __version__ # synonym

View File

@ -8,6 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix
from django.conf import settings
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type, smart_unicode
from django.utils.translation import ugettext_lazy as _
from rest_framework.reverse import reverse
@ -89,6 +90,8 @@ class Field(object):
return value
elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)):
return [self.to_native(item) for item in value]
elif isinstance(value, dict):
return dict(map(self.to_native, (k, v)) for k, v in value.items())
return smart_unicode(value)
def attributes(self):
@ -229,13 +232,77 @@ class ModelField(WritableField):
##### 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 relatinship, 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 = self.default_read_only
### 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_unicode(obj)
ident = smart_unicode(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 serializier stuff...
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
@ -253,6 +320,8 @@ 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()]
@ -276,6 +345,9 @@ class ManyRelatedMixin(object):
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
@ -284,9 +356,25 @@ class ManyRelatedField(ManyRelatedMixin, RelatedField):
class PrimaryKeyRelatedField(RelatedField):
"""
Serializes a related field or related object to a pk value.
Represents a to-one relationship as a pk value.
"""
default_read_only = False
# 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_unicode(obj)
ident = smart_unicode(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
@ -297,7 +385,8 @@ class PrimaryKeyRelatedField(RelatedField):
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
raise ValidationError('Invalid hyperlink - object does not exist.')
msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
raise ValidationError(msg)
def field_to_native(self, obj, field_name):
try:
@ -313,8 +402,23 @@ class PrimaryKeyRelatedField(RelatedField):
class ManyPrimaryKeyRelatedField(ManyRelatedField):
"""
Serializes a to-many related field or related manager to a pk value.
Represents a to-many relationship as a pk value.
"""
default_read_only = False
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_unicode(obj)
ident = smart_unicode(self.to_native(obj.pk))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
def to_native(self, pk):
return pk
@ -329,13 +433,52 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
# 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_unicode(data)
raise ValidationError(msg)
### Slug relationships
class SlugRelatedField(RelatedField):
default_read_only = False
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):
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):
pass
### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
"""
Represents a to-one relationship, using hyperlinking.
"""
pk_url_kwarg = 'pk'
slug_url_kwarg = 'slug'
slug_field = 'slug'
default_read_only = False
def __init__(self, *args, **kwargs):
try:
@ -417,16 +560,20 @@ class HyperlinkedRelatedField(RelatedField):
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
"""
Represents a to-many relationship, using hyperlinking.
"""
pass
class HyperlinkedIdentityField(Field):
"""
A field that represents the model's identity using a hyperlink.
Represents the instance, or a property on the instance, using hyperlinking.
"""
def __init__(self, *args, **kwargs):
# TODO: Make this mandatory, and have the HyperlinkedModelSerializer
# set it on-the-fly
# 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)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)

View File

@ -282,10 +282,12 @@ class BrowsableAPIRenderer(BaseRenderer):
serializers.EmailField: forms.EmailField,
serializers.CharField: forms.CharField,
serializers.BooleanField: forms.BooleanField,
serializers.PrimaryKeyRelatedField: forms.ModelChoiceField,
serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField,
serializers.HyperlinkedRelatedField: forms.ModelChoiceField,
serializers.ManyHyperlinkedRelatedField: forms.ModelMultipleChoiceField
serializers.PrimaryKeyRelatedField: forms.ChoiceField,
serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
serializers.SlugRelatedField: forms.ChoiceField,
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
serializers.HyperlinkedRelatedField: forms.ChoiceField,
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
}
fields = {}
@ -296,19 +298,14 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs = {}
kwargs['required'] = v.required
if getattr(v, 'queryset', None):
kwargs['queryset'] = v.queryset
#if getattr(v, 'queryset', None):
# kwargs['queryset'] = v.queryset
if getattr(v, 'choices', None) is not None:
kwargs['choices'] = v.choices
if getattr(v, 'widget', None):
widget = copy.deepcopy(v.widget)
# If choices have friendly readable names,
# then add in the identities too
if getattr(widget, 'choices', None):
choices = widget.choices
if any([ident != desc for (ident, desc) in choices]):
choices = [(ident, u"%s (%s)" % (desc, ident))
for (ident, desc) in choices]
widget.choices = choices
kwargs['widget'] = widget
if getattr(v, 'default', None) is not None:

View File

@ -32,10 +32,10 @@ def main():
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
DeprecationWarning
)
failures = TestRunner(['rest_framework'])
failures = TestRunner(['tests'])
else:
test_runner = TestRunner()
failures = test_runner.run_tests(['rest_framework'])
failures = test_runner.run_tests(['tests'])
cov.stop()
# Discover the list of all modules that we should test coverage for

View File

@ -36,6 +36,13 @@ ul.breadcrumb {
margin: 58px 0 0 0;
}
form select, form input {
width: 90%;
}
form select[multiple] {
height: 150px;
}
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
position: absolute;

View File

@ -131,12 +131,12 @@
{% csrf_token %}
{{ post_form.non_field_errors }}
{% for field in post_form %}
<div class="control-group {% if field.errors %}error{% endif %}">
<div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
{{ field|add_class:"input-xlarge" }}
{{ field }}
<span class="help-inline">{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }}
<!--{{ field.errors|add_class:"help-block" }}-->
</div>
</div>
{% endfor %}
@ -156,12 +156,12 @@
{% csrf_token %}
{{ put_form.non_field_errors }}
{% for field in put_form %}
<div class="control-group {% if field.errors %}error{% endif %}">
<div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
{{ field|add_class:"input-xlarge" }}
{{ field }}
<span class='help-inline'>{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }}
<!--{{ field.errors|add_class:"help-block" }}-->
</div>
</div>
{% endfor %}

View File

@ -122,6 +122,13 @@ 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):

View File

@ -0,0 +1,187 @@
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(queryset=ManyToManySource.objects.all())
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(read_only=True)
class Meta:
model = ForeignKeyTarget
class ForeignKeySourceSerializer(serializers.ModelSerializer):
class Meta:
model = ForeignKeySource
# TODO: Add test that .data cannot be accessed prior to .is_valid
class PrimaryKeyManyToManyTests(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(instance=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(instance=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(data, instance=instance)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure source 1 is updated, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(instance=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(data, instance=instance)
self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.data, data)
serializer.save()
# Ensure target 1 is updated, and everything else is as expected
queryset = ManyToManyTarget.objects.all()
serializer = ManyToManyTargetSerializer(instance=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)
class PrimaryKeyForeignKeyTests(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(instance=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(instance=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(data, instance=instance)
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(instance=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)
# 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(data, instance=instance)
# 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(instance=queryset)
# expected = [
# {'id': 1, 'name': u'target-1', 'sources': [1]},
# {'id': 2, 'name': u'target-2', 'sources': []},
# ]
# self.assertEquals(serializer.data, expected)

View File

@ -1,7 +1,9 @@
import datetime
from django.test import TestCase
from rest_framework import serializers
from rest_framework.tests.models import *
from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, CallableDefaultValueModel, DefaultValueModel,
ManyToManyModel, Person, ReadOnlyManyToManyModel)
class SubComment(object):
@ -44,8 +46,11 @@ class ActionItemSerializer(serializers.ModelSerializer):
class PersonSerializer(serializers.ModelSerializer):
info = serializers.Field(source='info')
class Meta:
model = Person
fields = ('name', 'age', 'info')
class BasicTests(TestCase):
@ -67,6 +72,9 @@ class BasicTests(TestCase):
'created': datetime.datetime(2012, 1, 1),
'sub_comment': 'And Merry Christmas!'
}
self.person_data = {'name': 'dwight', 'age': 35}
self.person = Person(**self.person_data)
self.person.save()
def test_empty(self):
serializer = CommentSerializer()
@ -97,6 +105,21 @@ class BasicTests(TestCase):
self.assertEquals(serializer.object, expected)
self.assertTrue(serializer.object is expected)
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
def test_model_fields_as_expected(self):
""" Make sure that the fields returned are the same as defined
in the Meta data
"""
serializer = PersonSerializer(instance=self.person)
self.assertEquals(set(serializer.data.keys()),
set(['name', 'age', 'info']))
def test_field_with_dictionary(self):
""" Make sure that dictionaries from fields are left intact
"""
serializer = PersonSerializer(instance=self.person)
expected = self.person_data
self.assertEquals(serializer.data['info'], expected)
class ValidationTests(TestCase):