Commit 09f39bd2 by Ben Konrath

Merge branch 'master' into restframework2-filter

parents 01564fb1 455a8ced
...@@ -7,7 +7,7 @@ html/ ...@@ -7,7 +7,7 @@ html/
coverage/ coverage/
build/ build/
dist/ dist/
rest_framework.egg-info/ *.egg-info/
MANIFEST MANIFEST
!.gitignore !.gitignore
......
...@@ -57,8 +57,37 @@ To run the tests. ...@@ -57,8 +57,37 @@ To run the tests.
# Changelog # Changelog
## 2.1.0
**Date**: 5th Nov 2012
**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
* **Serializer `instance` and `data` keyword args have their position swapped.**
* `queryset` argument is now optional on writable model fields.
* Hyperlinked related fields optionally take `slug_field` and `slug_field_kwarg` arguments.
* Support Django's cache framework.
* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
* Bugfixes (Support choice field in Browseable API)
## 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 ## 2.0.0
**Date**: 30th Oct 2012
* Redesign of core components. * Redesign of core components.
* Fix **all of the things**. * Fix **all of the things**.
...@@ -93,6 +122,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -93,6 +122,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[sandbox]: http://restframework.herokuapp.com/ [sandbox]: http://restframework.herokuapp.com/
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md [rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
[docs]: http://django-rest-framework.org/ [docs]: http://django-rest-framework.org/
[urlobject]: https://github.com/zacharyvoase/urlobject [urlobject]: https://github.com/zacharyvoase/urlobject
......
...@@ -235,44 +235,50 @@ Then an example output format for a Bookmark instance would be: ...@@ -235,44 +235,50 @@ Then an example output format for a Bookmark instance would be:
'url': u'https://www.djangoproject.com/' '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. By default these fields are 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` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
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. By 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 be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
`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
Be default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag. `HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
## ManyHyperlinkedRelatedField By default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
This field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship. **Arguments**:
`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. * `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.
Be default, `ManyHyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag. * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
## HyperLinkedIdentityField ## HyperLinkedIdentityField
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. 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. 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/ [cite]: http://www.python.org/dev/peps/pep-0020/
...@@ -47,7 +47,7 @@ The first part of serializer class defines the fields that get serialized/deseri ...@@ -47,7 +47,7 @@ The first part of serializer class defines the fields that get serialized/deseri
We can now use `CommentSerializer` to serialize a comment, or list of comments. Again, using the `Serializer` class looks a lot like using a `Form` class. We can now use `CommentSerializer` to serialize a comment, or list of comments. Again, using the `Serializer` class looks a lot like using a `Form` class.
serializer = CommentSerializer(instance=comment) serializer = CommentSerializer(comment)
serializer.data serializer.data
# {'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)} # {'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}
...@@ -65,20 +65,29 @@ Deserialization is similar. First we parse a stream into python native datatype ...@@ -65,20 +65,29 @@ Deserialization is similar. First we parse a stream into python native datatype
...then we restore those native datatypes into a fully populated object instance. ...then we restore those native datatypes into a fully populated object instance.
serializer = CommentSerializer(data) serializer = CommentSerializer(data=data)
serializer.is_valid() serializer.is_valid()
# True # True
serializer.object serializer.object
# <Comment object at 0x10633b2d0> # <Comment object at 0x10633b2d0>
>>> serializer.deserialize('json', stream) >>> serializer.deserialize('json', stream)
When deserializing data, we can either create a new instance, or update an existing instance.
serializer = CommentSerializer(data=data) # Create new instance
serializer = CommentSerializer(comment, data=data) # Update `instance`
## Validation ## Validation
When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages. When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages.
### Field-level validation ### Field-level validation
You can specify custom field-level validation by adding `validate_<fieldname>()` methods to your `Serializer` subclass. These are analagous to `clean_<fieldname>` methods on Django forms, but accept slightly different arguments. They take a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided). Your `validate_<fieldname>` methods should either just return the attrs dictionary or raise a `ValidationError`. For example: You can specify custom field-level validation by adding `.validate_<fieldname>` methods to your `Serializer` subclass. These are analagous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
They take a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided).
Your `validate_<fieldname>` methods should either just return the `attrs` dictionary or raise a `ValidationError`. For example:
from rest_framework import serializers from rest_framework import serializers
...@@ -88,16 +97,22 @@ You can specify custom field-level validation by adding `validate_<fieldname>()` ...@@ -88,16 +97,22 @@ You can specify custom field-level validation by adding `validate_<fieldname>()`
def validate_title(self, attrs, source): def validate_title(self, attrs, source):
""" """
Check that the blog post is about Django Check that the blog post is about Django.
""" """
value = attrs[source] value = attrs[source]
if "Django" not in value: if "django" not in value.lower():
raise serializers.ValidationError("Blog post is not about Django") raise serializers.ValidationError("Blog post is not about Django")
return attrs return attrs
### Final cross-field validation ### Object-level validation
To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`.
## Saving object state
Serializers also include a `.save()` method that you can override if you want to provide a method of persisting the state of a deserialized object. The default behavior of the method is to simply call `.save()` on the deserialized object instance.
To do any other validation that requires access to multiple fields, add a method called `validate` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. The generic views provided by REST framework call the `.save()` method when updating or creating entities.
## Dealing with nested objects ## Dealing with nested objects
......
...@@ -66,11 +66,9 @@ If you're intending to use the browseable API you'll want to add REST framework' ...@@ -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. Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace.
<!--
## Quickstart ## Quickstart
Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running with REST framework. Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running with REST framework.
-->
## Tutorial ## Tutorial
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
<ul class="dropdown-menu"> <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/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/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> <li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
......
...@@ -52,6 +52,10 @@ The following people have helped make REST framework great. ...@@ -52,6 +52,10 @@ The following people have helped make REST framework great.
* Madis Väin - [madisvain] * Madis Väin - [madisvain]
* Stephan Groß - [minddust] * Stephan Groß - [minddust]
* Pavel Savchenko - [asfaltboy] * Pavel Savchenko - [asfaltboy]
* Otto Yiu - [ottoyiu]
* Jacob Magnusson - [jmagnusson]
* Osiloke Harold Emoekpere - [osiloke]
* Michael Shepanski - [mjs7231]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
...@@ -80,7 +84,7 @@ To contact the author directly: ...@@ -80,7 +84,7 @@ To contact the author directly:
[twitter]: http://twitter.com/_tomchristie [twitter]: http://twitter.com/_tomchristie
[bootstrap]: http://twitter.github.com/bootstrap/ [bootstrap]: http://twitter.github.com/bootstrap/
[markdown]: http://daringfireball.net/projects/markdown/ [markdown]: http://daringfireball.net/projects/markdown/
[github]: github.com/tomchristie/django-rest-framework [github]: https://github.com/tomchristie/django-rest-framework
[travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework [travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework
[piston]: https://bitbucket.org/jespern/django-piston [piston]: https://bitbucket.org/jespern/django-piston
[tastypie]: https://github.com/toastdriven/django-tastypie [tastypie]: https://github.com/toastdriven/django-tastypie
...@@ -139,3 +143,7 @@ To contact the author directly: ...@@ -139,3 +143,7 @@ To contact the author directly:
[madisvain]: https://github.com/madisvain [madisvain]: https://github.com/madisvain
[minddust]: https://github.com/minddust [minddust]: https://github.com/minddust
[asfaltboy]: https://github.com/asfaltboy [asfaltboy]: https://github.com/asfaltboy
[ottoyiu]: https://github.com/OttoYiu
[jmagnusson]: https://github.com/jmagnusson
[osiloke]: https://github.com/osiloke
[mjs7231]: https://github.com/mjs7231
\ No newline at end of file
...@@ -4,14 +4,40 @@ ...@@ -4,14 +4,40 @@
> >
> &mdash; Eric S. Raymond, [The Cathedral and the Bazaar][cite]. > &mdash; Eric S. Raymond, [The Cathedral and the Bazaar][cite].
## Master ## 2.1.0
**Date**: 5th Nov 2012
**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
* **Serializer `instance` and `data` keyword args have their position swapped.**
* `queryset` argument is now optional on writable model fields.
* Hyperlinked related fields optionally take `slug_field` and `slug_field_kwarg` arguments.
* Support Django's cache framework.
* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
* Bugfix: Support choice field in Browseable API.
* Bugfix: Related fields with `read_only=True` do not require a `queryset` argument.
## 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'. * If PUT creates an instance return '201 Created', instead of '200 OK'.
## 2.0.0 ## 2.0.0
**Date**: 30th Oct 2012
* **Fix all of the things.** (Well, almost.) * **Fix all of the things.** (Well, almost.)
* For more information please see the [2.0 migration guide][migration]. * For more information please see the [2.0 announcement][announcement].
--- ---
...@@ -117,4 +143,5 @@ ...@@ -117,4 +143,5 @@
* Initial release. * Initial release.
[cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html
[migration]: migration.md [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
\ No newline at end of file [announcement]: rest-framework-2-announcement.md
\ No newline at end of file
...@@ -162,7 +162,7 @@ Okay, once we've got a few imports out of the way, let's create a code snippet t ...@@ -162,7 +162,7 @@ Okay, once we've got a few imports out of the way, let's create a code snippet t
We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances. We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances.
serializer = SnippetSerializer(instance=snippet) serializer = SnippetSerializer(snippet)
serializer.data serializer.data
# {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} # {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
...@@ -181,7 +181,7 @@ Deserialization is similar. First we parse a stream into python native datatype ...@@ -181,7 +181,7 @@ Deserialization is similar. First we parse a stream into python native datatype
...then we restore those native datatypes into to a fully populated object instance. ...then we restore those native datatypes into to a fully populated object instance.
serializer = SnippetSerializer(data) serializer = SnippetSerializer(data=data)
serializer.is_valid() serializer.is_valid()
# True # True
serializer.object serializer.object
...@@ -240,12 +240,12 @@ The root of our API is going to be a view that supports listing all the existing ...@@ -240,12 +240,12 @@ The root of our API is going to be a view that supports listing all the existing
""" """
if request.method == 'GET': if request.method == 'GET':
snippets = Snippet.objects.all() snippets = Snippet.objects.all()
serializer = SnippetSerializer(instance=snippets) serializer = SnippetSerializer(snippets)
return JSONResponse(serializer.data) return JSONResponse(serializer.data)
elif request.method == 'POST': elif request.method == 'POST':
data = JSONParser().parse(request) data = JSONParser().parse(request)
serializer = SnippetSerializer(data) serializer = SnippetSerializer(data=data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return JSONResponse(serializer.data, status=201) return JSONResponse(serializer.data, status=201)
...@@ -267,12 +267,12 @@ We'll also need a view which corresponds to an individual snippet, and can be us ...@@ -267,12 +267,12 @@ We'll also need a view which corresponds to an individual snippet, and can be us
return HttpResponse(status=404) return HttpResponse(status=404)
if request.method == 'GET': if request.method == 'GET':
serializer = SnippetSerializer(instance=snippet) serializer = SnippetSerializer(snippet)
return JSONResponse(serializer.data) return JSONResponse(serializer.data)
elif request.method == 'PUT': elif request.method == 'PUT':
data = JSONParser().parse(request) data = JSONParser().parse(request)
serializer = SnippetSerializer(data, instance=snippet) serializer = SnippetSerializer(snippet, data=data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return JSONResponse(serializer.data) return JSONResponse(serializer.data)
......
...@@ -52,11 +52,11 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On ...@@ -52,11 +52,11 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
""" """
if request.method == 'GET': if request.method == 'GET':
snippets = Snippet.objects.all() snippets = Snippet.objects.all()
serializer = SnippetSerializer(instance=snippets) serializer = SnippetSerializer(snippets)
return Response(serializer.data) return Response(serializer.data)
elif request.method == 'POST': elif request.method == 'POST':
serializer = SnippetSerializer(request.DATA) serializer = SnippetSerializer(data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
...@@ -77,11 +77,11 @@ Our instance view is an improvement over the previous example. It's a little mo ...@@ -77,11 +77,11 @@ Our instance view is an improvement over the previous example. It's a little mo
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET': if request.method == 'GET':
serializer = SnippetSerializer(instance=snippet) serializer = SnippetSerializer(snippet)
return Response(serializer.data) return Response(serializer.data)
elif request.method == 'PUT': elif request.method == 'PUT':
serializer = SnippetSerializer(request.DATA, instance=snippet) serializer = SnippetSerializer(snippet, data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
......
...@@ -20,11 +20,11 @@ We'll start by rewriting the root view as a class based view. All this involves ...@@ -20,11 +20,11 @@ We'll start by rewriting the root view as a class based view. All this involves
""" """
def get(self, request, format=None): def get(self, request, format=None):
snippets = Snippet.objects.all() snippets = Snippet.objects.all()
serializer = SnippetSerializer(instance=snippets) serializer = SnippetSerializer(snippets)
return Response(serializer.data) return Response(serializer.data)
def post(self, request, format=None): def post(self, request, format=None):
serializer = SnippetSerializer(request.DATA) serializer = SnippetSerializer(data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
...@@ -44,12 +44,12 @@ So far, so good. It looks pretty similar to the previous case, but we've got be ...@@ -44,12 +44,12 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
def get(self, request, pk, format=None): def get(self, request, pk, format=None):
snippet = self.get_object(pk) snippet = self.get_object(pk)
serializer = SnippetSerializer(instance=snippet) serializer = SnippetSerializer(snippet)
return Response(serializer.data) return Response(serializer.data)
def put(self, request, pk, format=None): def put(self, request, pk, format=None):
snippet = self.get_object(pk) snippet = self.get_object(pk)
serializer = SnippetSerializer(request.DATA, instance=snippet) serializer = SnippetSerializer(snippet, data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
...@@ -92,7 +92,7 @@ Let's take a look at how we can compose our views by using the mixin classes. ...@@ -92,7 +92,7 @@ Let's take a look at how we can compose our views by using the mixin classes.
class SnippetList(mixins.ListModelMixin, class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin, mixins.CreateModelMixin,
generics.MultipleObjectBaseView): generics.MultipleObjectAPIView):
model = Snippet model = Snippet
serializer_class = SnippetSerializer serializer_class = SnippetSerializer
...@@ -102,7 +102,7 @@ Let's take a look at how we can compose our views by using the mixin classes. ...@@ -102,7 +102,7 @@ Let's take a look at how we can compose our views by using the mixin classes.
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs) return self.create(request, *args, **kwargs)
We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectBaseView`, and adding in `ListModelMixin` and `CreateModelMixin`. We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far. The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
......
...@@ -167,7 +167,7 @@ We've reached the end of our tutorial. If you want to get more involved in the ...@@ -167,7 +167,7 @@ We've reached the end of our tutorial. If you want to get more involved in the
* Join the [REST framework discussion group][group], and help build the community. * Join the [REST framework discussion group][group], and help build the community.
* Follow the author [on Twitter][twitter] and say hi. * Follow the author [on Twitter][twitter] and say hi.
**Now go build some awesome things.** **Now go build awesome things.**
[repo]: https://github.com/tomchristie/rest-framework-tutorial [repo]: https://github.com/tomchristie/rest-framework-tutorial
[sandbox]: http://restframework.herokuapp.com/ [sandbox]: http://restframework.herokuapp.com/
......
...@@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t ...@@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t
class GroupSerializer(serializers.HyperlinkedModelSerializer): class GroupSerializer(serializers.HyperlinkedModelSerializer):
permissions = serializers.ManySlugRelatedField(
slug_field='codename',
queryset=Permission.objects.all()
)
class Meta: class Meta:
model = Group model = Group
fields = ('url', 'name', 'permissions') 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. 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 ## Views
Right, we'd better write some views then. Open `quickstart/views.py` and get typing. 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`.. ...@@ -152,7 +159,7 @@ We can now access our API, both from the command-line, using tools like `curl`..
}, },
{ {
"email": "tom@example.com", "email": "tom@example.com",
"groups": [], "groups": [ ],
"url": "http://127.0.0.1:8000/users/2/", "url": "http://127.0.0.1:8000/users/2/",
"username": "tom" "username": "tom"
} }
......
__version__ = '2.0.0' __version__ = '2.1.0'
VERSION = __version__ # synonym VERSION = __version__ # synonym
...@@ -8,6 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError ...@@ -8,6 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix from django.core.urlresolvers import resolve, get_script_prefix
from django.conf import settings from django.conf import settings
from django.forms import widgets from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type, smart_unicode from django.utils.encoding import is_protected_type, smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
...@@ -39,7 +40,7 @@ class Field(object): ...@@ -39,7 +40,7 @@ class Field(object):
self.source = source self.source = source
def initialize(self, parent): def initialize(self, parent, field_name):
""" """
Called to set up a field prior to field_to_native or field_from_native. Called to set up a field prior to field_to_native or field_from_native.
...@@ -89,6 +90,8 @@ class Field(object): ...@@ -89,6 +90,8 @@ class Field(object):
return value return value
elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)): elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)):
return [self.to_native(item) for item in value] 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) return smart_unicode(value)
def attributes(self): def attributes(self):
...@@ -229,13 +232,92 @@ class ModelField(WritableField): ...@@ -229,13 +232,92 @@ class ModelField(WritableField):
##### Relational fields ##### ##### Relational fields #####
# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField): class RelatedField(WritableField):
""" """
Base class for related model fields. 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): def __init__(self, *args, **kwargs):
self.queryset = kwargs.pop('queryset', None) self.queryset = kwargs.pop('queryset', None)
super(RelatedField, self).__init__(*args, **kwargs) 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_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): def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name) value = getattr(obj, self.source or field_name)
...@@ -253,6 +335,8 @@ class ManyRelatedMixin(object): ...@@ -253,6 +335,8 @@ class ManyRelatedMixin(object):
""" """
Mixin to convert a related field to a many related field. Mixin to convert a related field to a many related field.
""" """
widget = widgets.SelectMultiple
def field_to_native(self, obj, field_name): def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name) value = getattr(obj, self.source or field_name)
return [self.to_native(item) for item in value.all()] return [self.to_native(item) for item in value.all()]
...@@ -276,6 +360,9 @@ class ManyRelatedMixin(object): ...@@ -276,6 +360,9 @@ class ManyRelatedMixin(object):
class ManyRelatedField(ManyRelatedMixin, RelatedField): class ManyRelatedField(ManyRelatedMixin, RelatedField):
""" """
Base class for related model managers. 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 pass
...@@ -284,9 +371,25 @@ class ManyRelatedField(ManyRelatedMixin, RelatedField): ...@@ -284,9 +371,25 @@ class ManyRelatedField(ManyRelatedMixin, RelatedField):
class PrimaryKeyRelatedField(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): def to_native(self, pk):
return pk return pk
...@@ -297,7 +400,8 @@ class PrimaryKeyRelatedField(RelatedField): ...@@ -297,7 +400,8 @@ class PrimaryKeyRelatedField(RelatedField):
try: try:
return self.queryset.get(pk=data) return self.queryset.get(pk=data)
except ObjectDoesNotExist: 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): def field_to_native(self, obj, field_name):
try: try:
...@@ -313,8 +417,23 @@ class PrimaryKeyRelatedField(RelatedField): ...@@ -313,8 +417,23 @@ class PrimaryKeyRelatedField(RelatedField):
class ManyPrimaryKeyRelatedField(ManyRelatedField): 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): def to_native(self, pk):
return pk return pk
...@@ -329,22 +448,72 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): ...@@ -329,22 +448,72 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
# Forward relationship # Forward relationship
return [self.to_native(item.pk) for item in queryset.all()] 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):
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):
pass
### Hyperlinked relationships ### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField): class HyperlinkedRelatedField(RelatedField):
"""
Represents a to-one relationship, using hyperlinking.
"""
pk_url_kwarg = 'pk' pk_url_kwarg = 'pk'
slug_url_kwarg = 'slug'
slug_field = 'slug' slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field`
default_read_only = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
try: try:
self.view_name = kwargs.pop('view_name') self.view_name = kwargs.pop('view_name')
except: except:
raise ValueError("Hyperlinked field requires 'view_name' kwarg") raise ValueError("Hyperlinked field requires 'view_name' kwarg")
self.slug_field = kwargs.pop('slug_field', self.slug_field)
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', self.slug_field)
self.format = kwargs.pop('format', None) self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def 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): def to_native(self, obj):
view_name = self.view_name view_name = self.view_name
request = self.context.get('request', None) request = self.context.get('request', None)
...@@ -417,16 +586,20 @@ class HyperlinkedRelatedField(RelatedField): ...@@ -417,16 +586,20 @@ class HyperlinkedRelatedField(RelatedField):
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField): class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
"""
Represents a to-many relationship, using hyperlinking.
"""
pass pass
class HyperlinkedIdentityField(Field): 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): def __init__(self, *args, **kwargs):
# TODO: Make this mandatory, and have the HyperlinkedModelSerializer # TODO: Make view_name mandatory, and have the
# set it on-the-fly # HyperlinkedModelSerializer set it on-the-fly
self.view_name = kwargs.pop('view_name', None) self.view_name = kwargs.pop('view_name', None)
self.format = kwargs.pop('format', None) self.format = kwargs.pop('format', None)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs) super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
......
...@@ -48,7 +48,7 @@ class GenericAPIView(views.APIView): ...@@ -48,7 +48,7 @@ class GenericAPIView(views.APIView):
# TODO: add support for seperate serializer/deserializer # TODO: add support for seperate serializer/deserializer
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
context = self.get_serializer_context() context = self.get_serializer_context()
return serializer_class(data, instance=instance, context=context) return serializer_class(instance, data=data, context=context)
class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
......
...@@ -29,7 +29,7 @@ class CreateModelMixin(object): ...@@ -29,7 +29,7 @@ class CreateModelMixin(object):
class ListModelMixin(object): class ListModelMixin(object):
""" """
List a queryset. List a queryset.
Should be mixed in with `MultipleObjectBaseView`. Should be mixed in with `MultipleObjectAPIView`.
""" """
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
......
...@@ -100,7 +100,7 @@ class JSONPRenderer(JSONRenderer): ...@@ -100,7 +100,7 @@ class JSONPRenderer(JSONRenderer):
callback = self.get_callback(renderer_context) callback = self.get_callback(renderer_context)
json = super(JSONPRenderer, self).render(data, accepted_media_type, json = super(JSONPRenderer, self).render(data, accepted_media_type,
renderer_context) renderer_context)
return "%s(%s);" % (callback, json) return u"%s(%s);" % (callback, json)
class XMLRenderer(BaseRenderer): class XMLRenderer(BaseRenderer):
...@@ -281,11 +281,14 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -281,11 +281,14 @@ class BrowsableAPIRenderer(BaseRenderer):
serializers.DateField: forms.DateField, serializers.DateField: forms.DateField,
serializers.EmailField: forms.EmailField, serializers.EmailField: forms.EmailField,
serializers.CharField: forms.CharField, serializers.CharField: forms.CharField,
serializers.ChoiceField: forms.ChoiceField,
serializers.BooleanField: forms.BooleanField, serializers.BooleanField: forms.BooleanField,
serializers.PrimaryKeyRelatedField: forms.ModelChoiceField, serializers.PrimaryKeyRelatedField: forms.ChoiceField,
serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField, serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
serializers.HyperlinkedRelatedField: forms.ModelChoiceField, serializers.SlugRelatedField: forms.ChoiceField,
serializers.ManyHyperlinkedRelatedField: forms.ModelMultipleChoiceField serializers.ManySlugRelatedField: forms.MultipleChoiceField,
serializers.HyperlinkedRelatedField: forms.ChoiceField,
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
} }
fields = {} fields = {}
...@@ -296,19 +299,14 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -296,19 +299,14 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs = {} kwargs = {}
kwargs['required'] = v.required kwargs['required'] = v.required
if getattr(v, 'queryset', None): #if getattr(v, 'queryset', None):
kwargs['queryset'] = v.queryset # kwargs['queryset'] = v.queryset
if getattr(v, 'choices', None) is not None:
kwargs['choices'] = v.choices
if getattr(v, 'widget', None): if getattr(v, 'widget', None):
widget = copy.deepcopy(v.widget) 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, "%s (%s)" % (desc, ident))
for (ident, desc) in choices]
widget.choices = choices
kwargs['widget'] = widget kwargs['widget'] = widget
if getattr(v, 'default', None) is not None: if getattr(v, 'default', None) is not None:
...@@ -319,6 +317,9 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -319,6 +317,9 @@ class BrowsableAPIRenderer(BaseRenderer):
try: try:
fields[k] = field_mapping[v.__class__](**kwargs) fields[k] = field_mapping[v.__class__](**kwargs)
except KeyError: except KeyError:
if getattr(v, 'choices', None) is not None:
fields[k] = forms.ChoiceField(**kwargs)
else:
fields[k] = forms.CharField(**kwargs) fields[k] = forms.CharField(**kwargs)
return fields return fields
......
...@@ -45,3 +45,13 @@ class Response(SimpleTemplateResponse): ...@@ -45,3 +45,13 @@ class Response(SimpleTemplateResponse):
# TODO: Deprecate and use a template tag instead # TODO: Deprecate and use a template tag instead
# TODO: Status code text for RFC 6585 status codes # TODO: Status code text for RFC 6585 status codes
return STATUS_CODE_TEXT.get(self.status_code, '') return STATUS_CODE_TEXT.get(self.status_code, '')
def __getstate__(self):
"""
Remove attributes from the response that shouldn't be cached
"""
state = super(Response, self).__getstate__()
for key in ('accepted_renderer', 'renderer_context', 'data'):
if key in state:
del state[key]
return state
...@@ -32,10 +32,10 @@ def main(): ...@@ -32,10 +32,10 @@ def main():
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.', 'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
DeprecationWarning DeprecationWarning
) )
failures = TestRunner(['rest_framework']) failures = TestRunner(['tests'])
else: else:
test_runner = TestRunner() test_runner = TestRunner()
failures = test_runner.run_tests(['rest_framework']) failures = test_runner.run_tests(['tests'])
cov.stop() cov.stop()
# Discover the list of all modules that we should test coverage for # Discover the list of all modules that we should test coverage for
......
...@@ -21,6 +21,12 @@ DATABASES = { ...@@ -21,6 +21,12 @@ DATABASES = {
} }
} }
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
# Local time zone for this installation. Choices can be found here: # Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems. # although not all choices may be available on all operating systems.
......
...@@ -6,6 +6,15 @@ from django.db import models ...@@ -6,6 +6,15 @@ from django.db import models
from django.forms import widgets from django.forms import widgets
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from rest_framework.compat import get_concrete_model from rest_framework.compat import get_concrete_model
# Note: We do the following so that users of the framework can use this style:
#
# example_field = serializers.CharField(...)
#
# This helps keep the seperation between model fields, form fields, and
# serializer fields more explicit.
from rest_framework.fields import * from rest_framework.fields import *
...@@ -82,10 +91,10 @@ class BaseSerializer(Field): ...@@ -82,10 +91,10 @@ class BaseSerializer(Field):
_options_class = SerializerOptions _options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations. _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations.
def __init__(self, data=None, instance=None, context=None, **kwargs): def __init__(self, instance=None, data=None, context=None, **kwargs):
super(BaseSerializer, self).__init__(**kwargs) super(BaseSerializer, self).__init__(**kwargs)
self.fields = copy.deepcopy(self.base_fields)
self.opts = self._options_class(self.Meta) self.opts = self._options_class(self.Meta)
self.fields = copy.deepcopy(self.base_fields)
self.parent = None self.parent = None
self.root = None self.root = None
...@@ -100,13 +109,13 @@ class BaseSerializer(Field): ...@@ -100,13 +109,13 @@ class BaseSerializer(Field):
##### #####
# Methods to determine which fields to use when (de)serializing objects. # Methods to determine which fields to use when (de)serializing objects.
def default_fields(self, serialize, obj=None, data=None, nested=False): def default_fields(self, nested=False):
""" """
Return the complete set of default fields for the object, as a dict. Return the complete set of default fields for the object, as a dict.
""" """
return {} return {}
def get_fields(self, serialize, obj=None, data=None, nested=False): def get_fields(self, nested=False):
""" """
Returns the complete set of fields for the object as a dict. Returns the complete set of fields for the object as a dict.
...@@ -119,10 +128,10 @@ class BaseSerializer(Field): ...@@ -119,10 +128,10 @@ class BaseSerializer(Field):
for key, field in self.fields.items(): for key, field in self.fields.items():
ret[key] = field ret[key] = field
# Set up the field # Set up the field
field.initialize(parent=self) field.initialize(parent=self, field_name=key)
# Add in the default fields # Add in the default fields
fields = self.default_fields(serialize, obj, data, nested) fields = self.default_fields(nested)
for key, val in fields.items(): for key, val in fields.items():
if key not in ret: if key not in ret:
ret[key] = val ret[key] = val
...@@ -144,12 +153,12 @@ class BaseSerializer(Field): ...@@ -144,12 +153,12 @@ class BaseSerializer(Field):
##### #####
# Field methods - used when the serializer class is itself used as a field. # Field methods - used when the serializer class is itself used as a field.
def initialize(self, parent): def initialize(self, parent, field_name):
""" """
Same behaviour as usual Field, except that we need to keep track Same behaviour as usual Field, except that we need to keep track
of state so that we can deal with handling maximum depth. of state so that we can deal with handling maximum depth.
""" """
super(BaseSerializer, self).initialize(parent) super(BaseSerializer, self).initialize(parent, field_name)
if parent.opts.depth: if parent.opts.depth:
self.opts.depth = parent.opts.depth - 1 self.opts.depth = parent.opts.depth - 1
...@@ -170,7 +179,7 @@ class BaseSerializer(Field): ...@@ -170,7 +179,7 @@ class BaseSerializer(Field):
ret = self._dict_class() ret = self._dict_class()
ret.fields = {} ret.fields = {}
fields = self.get_fields(serialize=True, obj=obj, nested=bool(self.opts.depth)) fields = self.get_fields(nested=bool(self.opts.depth))
for field_name, field in fields.items(): for field_name, field in fields.items():
key = self.get_field_key(field_name) key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name) value = field.field_to_native(obj, field_name)
...@@ -183,7 +192,7 @@ class BaseSerializer(Field): ...@@ -183,7 +192,7 @@ class BaseSerializer(Field):
Core of deserialization, together with `restore_object`. Core of deserialization, together with `restore_object`.
Converts a dictionary of data into a dictionary of deserialized fields. Converts a dictionary of data into a dictionary of deserialized fields.
""" """
fields = self.get_fields(serialize=False, data=data, nested=bool(self.opts.depth)) fields = self.get_fields(nested=bool(self.opts.depth))
reverted_data = {} reverted_data = {}
for field_name, field in fields.items(): for field_name, field in fields.items():
try: try:
...@@ -198,7 +207,7 @@ class BaseSerializer(Field): ...@@ -198,7 +207,7 @@ class BaseSerializer(Field):
Run `validate_<fieldname>()` and `validate()` methods on the serializer Run `validate_<fieldname>()` and `validate()` methods on the serializer
""" """
# TODO: refactor this so we're not determining the fields again # TODO: refactor this so we're not determining the fields again
fields = self.get_fields(serialize=False, data=attrs, nested=bool(self.opts.depth)) fields = self.get_fields(nested=bool(self.opts.depth))
for field_name, field in fields.items(): for field_name, field in fields.items():
try: try:
...@@ -237,11 +246,8 @@ class BaseSerializer(Field): ...@@ -237,11 +246,8 @@ class BaseSerializer(Field):
""" """
Serialize objects -> primatives. Serialize objects -> primatives.
""" """
if isinstance(obj, dict): if hasattr(obj, '__iter__'):
return dict([(key, self.to_native(val)) return [self.convert_object(item) for item in obj]
for (key, val) in obj.items()])
elif hasattr(obj, '__iter__'):
return [self.to_native(item) for item in obj]
return self.convert_object(obj) return self.convert_object(obj)
def from_native(self, data): def from_native(self, data):
...@@ -323,7 +329,7 @@ class ModelSerializer(Serializer): ...@@ -323,7 +329,7 @@ class ModelSerializer(Serializer):
""" """
_options_class = ModelSerializerOptions _options_class = ModelSerializerOptions
def default_fields(self, serialize, obj=None, data=None, nested=False): def default_fields(self, nested=False):
""" """
Return all the fields that should be serialized for the model. Return all the fields that should be serialized for the model.
""" """
...@@ -360,7 +366,7 @@ class ModelSerializer(Serializer): ...@@ -360,7 +366,7 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field) field = self.get_field(model_field)
if field: if field:
field.initialize(parent=self) field.initialize(parent=self, field_name=model_field.name)
ret[model_field.name] = field ret[model_field.name] = field
return ret return ret
......
...@@ -36,6 +36,13 @@ ul.breadcrumb { ...@@ -36,6 +36,13 @@ ul.breadcrumb {
margin: 58px 0 0 0; margin: 58px 0 0 0;
} }
form select, form input {
width: 90%;
}
form select[multiple] {
height: 150px;
}
/* To allow tooltips to work on disabled elements */ /* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield { .disabled-tooltip-shield {
position: absolute; position: absolute;
......
...@@ -131,12 +131,12 @@ ...@@ -131,12 +131,12 @@
{% csrf_token %} {% csrf_token %}
{{ post_form.non_field_errors }} {{ post_form.non_field_errors }}
{% for field in post_form %} {% 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" }} {{ field.label_tag|add_class:"control-label" }}
<div class="controls"> <div class="controls">
{{ field|add_class:"input-xlarge" }} {{ field }}
<span class="help-inline">{{ field.help_text }}</span> <span class="help-inline">{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }} <!--{{ field.errors|add_class:"help-block" }}-->
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
...@@ -156,12 +156,12 @@ ...@@ -156,12 +156,12 @@
{% csrf_token %} {% csrf_token %}
{{ put_form.non_field_errors }} {{ put_form.non_field_errors }}
{% for field in put_form %} {% 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" }} {{ field.label_tag|add_class:"control-label" }}
<div class="controls"> <div class="controls">
{{ field|add_class:"input-xlarge" }} {{ field }}
<span class='help-inline'>{{ field.help_text }}</span> <span class='help-inline'>{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }} <!--{{ field.errors|add_class:"help-block" }}-->
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
......
...@@ -25,7 +25,7 @@ class TestGenericRelations(TestCase): ...@@ -25,7 +25,7 @@ class TestGenericRelations(TestCase):
model = Bookmark model = Bookmark
exclude = ('id',) exclude = ('id',)
serializer = BookmarkSerializer(instance=self.bookmark) serializer = BookmarkSerializer(self.bookmark)
expected = { expected = {
'tags': [u'django', u'python'], 'tags': [u'django', u'python'],
'url': u'https://www.djangoproject.com/' 'url': u'https://www.djangoproject.com/'
......
...@@ -2,17 +2,26 @@ from django.conf.urls.defaults import patterns, url ...@@ -2,17 +2,26 @@ from django.conf.urls.defaults import patterns, url
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from rest_framework import generics, status, serializers from rest_framework import generics, status, serializers
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo
factory = RequestFactory() factory = RequestFactory()
class BlogPostCommentSerializer(serializers.Serializer): class BlogPostCommentSerializer(serializers.ModelSerializer):
text = serializers.CharField() text = serializers.CharField()
blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail', queryset=BlogPost.objects.all()) blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
class Meta:
model = BlogPostComment
fields = ('text', 'blog_post_url')
class PhotoSerializer(serializers.Serializer):
description = serializers.CharField()
album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), slug_field='title', slug_url_kwarg='title')
def restore_object(self, attrs, instance=None): def restore_object(self, attrs, instance=None):
return BlogPostComment(**attrs) return Photo(**attrs)
class BasicList(generics.ListCreateAPIView): class BasicList(generics.ListCreateAPIView):
...@@ -42,12 +51,22 @@ class ManyToManyDetail(generics.RetrieveAPIView): ...@@ -42,12 +51,22 @@ class ManyToManyDetail(generics.RetrieveAPIView):
class BlogPostCommentListCreate(generics.ListCreateAPIView): class BlogPostCommentListCreate(generics.ListCreateAPIView):
model = BlogPostComment model = BlogPostComment
model_serializer_class = BlogPostCommentSerializer serializer_class = BlogPostCommentSerializer
class BlogPostDetail(generics.RetrieveAPIView): class BlogPostDetail(generics.RetrieveAPIView):
model = BlogPost model = BlogPost
class PhotoListCreate(generics.ListCreateAPIView):
model = Photo
model_serializer_class = PhotoSerializer
class AlbumDetail(generics.RetrieveAPIView):
model = Album
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'), url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'), url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
...@@ -55,7 +74,9 @@ urlpatterns = patterns('', ...@@ -55,7 +74,9 @@ urlpatterns = patterns('',
url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'), url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'), url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'), url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list') url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list')
) )
...@@ -163,6 +184,30 @@ class TestCreateWithForeignKeys(TestCase): ...@@ -163,6 +184,30 @@ class TestCreateWithForeignKeys(TestCase):
request = factory.post('/comments/', data=data) request = factory.post('/comments/', data=data)
response = self.create_view(request).render() response = self.create_view(request).render()
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(self.post.blogpostcomment_set.count(), 1) self.assertEqual(self.post.blogpostcomment_set.count(), 1)
self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment') self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
class TestCreateWithForeignKeysAndCustomSlug(TestCase):
urls = 'rest_framework.tests.hyperlinkedserializers'
def setUp(self):
"""
Create an Album
"""
self.post = Album.objects.create(title='test-album')
self.list_create_view = PhotoListCreate.as_view()
def test_create_photo(self):
data = {
'description': 'A test photo',
'album_url': 'http://testserver/albums/test-album/'
}
request = factory.post('/photos/', data=data)
response = self.list_create_view(request).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(self.post.photo_set.count(), 1)
self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
...@@ -125,10 +125,26 @@ class BlogPostComment(RESTFrameworkModel): ...@@ -125,10 +125,26 @@ class BlogPostComment(RESTFrameworkModel):
blog_post = models.ForeignKey(BlogPost) blog_post = models.ForeignKey(BlogPost)
class Album(RESTFrameworkModel):
title = models.CharField(max_length=100, unique=True)
class Photo(RESTFrameworkModel):
description = models.TextField()
album = models.ForeignKey(Album)
class Person(RESTFrameworkModel): class Person(RESTFrameworkModel):
name = models.CharField(max_length=10) name = models.CharField(max_length=10)
age = models.IntegerField(null=True, blank=True) age = models.IntegerField(null=True, blank=True)
@property
def info(self):
return {
'name': self.name,
'age': self.age,
}
# Model for issue #324 # Model for issue #324
class BlankFieldModel(RESTFrameworkModel): class BlankFieldModel(RESTFrameworkModel):
......
...@@ -141,13 +141,13 @@ class UnitTestPagination(TestCase): ...@@ -141,13 +141,13 @@ class UnitTestPagination(TestCase):
self.last_page = paginator.page(3) self.last_page = paginator.page(3)
def test_native_pagination(self): def test_native_pagination(self):
serializer = pagination.PaginationSerializer(instance=self.first_page) serializer = pagination.PaginationSerializer(self.first_page)
self.assertEquals(serializer.data['count'], 26) self.assertEquals(serializer.data['count'], 26)
self.assertEquals(serializer.data['next'], '?page=2') self.assertEquals(serializer.data['next'], '?page=2')
self.assertEquals(serializer.data['previous'], None) self.assertEquals(serializer.data['previous'], None)
self.assertEquals(serializer.data['results'], self.objects[:10]) self.assertEquals(serializer.data['results'], self.objects[:10])
serializer = pagination.PaginationSerializer(instance=self.last_page) serializer = pagination.PaginationSerializer(self.last_page)
self.assertEquals(serializer.data['count'], 26) self.assertEquals(serializer.data['count'], 26)
self.assertEquals(serializer.data['next'], None) self.assertEquals(serializer.data['next'], None)
self.assertEquals(serializer.data['previous'], '?page=2') self.assertEquals(serializer.data['previous'], '?page=2')
......
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(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(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())
self.assertEquals(serializer.data, data)
serializer.save()
# 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())
self.assertEquals(serializer.data, data)
serializer.save()
# 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)
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(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)
# 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)
import pickle
import re import re
from django.conf.urls.defaults import patterns, url, include from django.conf.urls.defaults import patterns, url, include
from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
...@@ -83,6 +85,7 @@ class HTMLView1(APIView): ...@@ -83,6 +85,7 @@ class HTMLView1(APIView):
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^cache$', MockGETView.as_view()),
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
url(r'^html$', HTMLView.as_view()), url(r'^html$', HTMLView.as_view()),
...@@ -416,3 +419,89 @@ class XMLRendererTestCase(TestCase): ...@@ -416,3 +419,89 @@ class XMLRendererTestCase(TestCase):
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>')) self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
self.assertTrue(xml.endswith('</root>')) self.assertTrue(xml.endswith('</root>'))
self.assertTrue(string in xml, '%r not in %r' % (string, xml)) self.assertTrue(string in xml, '%r not in %r' % (string, xml))
# Tests for caching issue, #346
class CacheRenderTest(TestCase):
"""
Tests specific to caching responses
"""
urls = 'rest_framework.tests.renderers'
cache_key = 'just_a_cache_key'
@classmethod
def _get_pickling_errors(cls, obj, seen=None):
""" Return any errors that would be raised if `obj' is pickled
Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897
"""
if seen == None:
seen = []
try:
state = obj.__getstate__()
except AttributeError:
return
if state == None:
return
if isinstance(state,tuple):
if not isinstance(state[0],dict):
state=state[1]
else:
state=state[0].update(state[1])
result = {}
for i in state:
try:
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)
return result
def http_resp(self, http_method, url):
"""
Simple wrapper for Client http requests
Removes the `client' and `request' attributes from as they are
added by django.test.client.Client and not part of caching
responses outside of tests.
"""
method = getattr(self.client, http_method)
resp = method(url)
del resp.client, resp.request
return resp
def test_obj_pickling(self):
"""
Test that responses are properly pickled
"""
resp = self.http_resp('get', '/cache')
# Make sure that no pickling errors occurred
self.assertEqual(self._get_pickling_errors(resp), {})
# Unfortunately LocMem backend doesn't raise PickleErrors but returns
# None instead.
cache.set(self.cache_key, resp)
self.assertTrue(cache.get(self.cache_key) is not None)
def test_head_caching(self):
"""
Test caching of HEAD requests
"""
resp = self.http_resp('head', '/cache')
cache.set(self.cache_key, resp)
cached_resp = cache.get(self.cache_key)
self.assertIsInstance(cached_resp, Response)
def test_get_caching(self):
"""
Test caching of GET requests
"""
resp = self.http_resp('get', '/cache')
cache.set(self.cache_key, resp)
cached_resp = cache.get(self.cache_key)
self.assertIsInstance(cached_resp, Response)
self.assertEqual(cached_resp.content, resp.content)
import datetime import datetime
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers 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): class SubComment(object):
...@@ -44,8 +46,11 @@ class ActionItemSerializer(serializers.ModelSerializer): ...@@ -44,8 +46,11 @@ class ActionItemSerializer(serializers.ModelSerializer):
class PersonSerializer(serializers.ModelSerializer): class PersonSerializer(serializers.ModelSerializer):
info = serializers.Field(source='info')
class Meta: class Meta:
model = Person model = Person
fields = ('name', 'age', 'info')
class BasicTests(TestCase): class BasicTests(TestCase):
...@@ -67,6 +72,9 @@ class BasicTests(TestCase): ...@@ -67,6 +72,9 @@ class BasicTests(TestCase):
'created': datetime.datetime(2012, 1, 1), 'created': datetime.datetime(2012, 1, 1),
'sub_comment': 'And Merry Christmas!' '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): def test_empty(self):
serializer = CommentSerializer() serializer = CommentSerializer()
...@@ -79,11 +87,11 @@ class BasicTests(TestCase): ...@@ -79,11 +87,11 @@ class BasicTests(TestCase):
self.assertEquals(serializer.data, expected) self.assertEquals(serializer.data, expected)
def test_retrieve(self): def test_retrieve(self):
serializer = CommentSerializer(instance=self.comment) serializer = CommentSerializer(self.comment)
self.assertEquals(serializer.data, self.expected) self.assertEquals(serializer.data, self.expected)
def test_create(self): def test_create(self):
serializer = CommentSerializer(self.data) serializer = CommentSerializer(data=self.data)
expected = self.comment expected = self.comment
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
self.assertEquals(serializer.object, expected) self.assertEquals(serializer.object, expected)
...@@ -91,13 +99,28 @@ class BasicTests(TestCase): ...@@ -91,13 +99,28 @@ class BasicTests(TestCase):
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
def test_update(self): def test_update(self):
serializer = CommentSerializer(self.data, instance=self.comment) serializer = CommentSerializer(self.comment, data=self.data)
expected = self.comment expected = self.comment
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
self.assertEquals(serializer.object, expected) self.assertEquals(serializer.object, expected)
self.assertTrue(serializer.object is expected) self.assertTrue(serializer.object is expected)
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') 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(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(self.person)
expected = self.person_data
self.assertEquals(serializer.data['info'], expected)
class ValidationTests(TestCase): class ValidationTests(TestCase):
def setUp(self): def setUp(self):
...@@ -115,12 +138,12 @@ class ValidationTests(TestCase): ...@@ -115,12 +138,12 @@ class ValidationTests(TestCase):
) )
def test_create(self): def test_create(self):
serializer = CommentSerializer(self.data) serializer = CommentSerializer(data=self.data)
self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.is_valid(), False)
self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']})
def test_update(self): def test_update(self):
serializer = CommentSerializer(self.data, instance=self.comment) serializer = CommentSerializer(self.comment, data=self.data)
self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.is_valid(), False)
self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']})
...@@ -129,7 +152,7 @@ class ValidationTests(TestCase): ...@@ -129,7 +152,7 @@ class ValidationTests(TestCase):
'content': 'xxx', 'content': 'xxx',
'created': datetime.datetime(2012, 1, 1) 'created': datetime.datetime(2012, 1, 1)
} }
serializer = CommentSerializer(data, instance=self.comment) serializer = CommentSerializer(self.comment, data=data)
self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.is_valid(), False)
self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) self.assertEquals(serializer.errors, {'email': [u'This field is required.']})
...@@ -140,7 +163,7 @@ class ValidationTests(TestCase): ...@@ -140,7 +163,7 @@ class ValidationTests(TestCase):
'title': 'Some action item', 'title': 'Some action item',
#No 'done' value. #No 'done' value.
} }
serializer = ActionItemSerializer(data, instance=self.actionitem) serializer = ActionItemSerializer(self.actionitem, data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
self.assertEquals(serializer.errors, {}) self.assertEquals(serializer.errors, {})
...@@ -160,12 +183,12 @@ class ValidationTests(TestCase): ...@@ -160,12 +183,12 @@ class ValidationTests(TestCase):
'created': datetime.datetime(2012, 1, 1) 'created': datetime.datetime(2012, 1, 1)
} }
serializer = CommentSerializerWithFieldValidator(data) serializer = CommentSerializerWithFieldValidator(data=data)
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid())
data['content'] = 'This should not validate' data['content'] = 'This should not validate'
serializer = CommentSerializerWithFieldValidator(data) serializer = CommentSerializerWithFieldValidator(data=data)
self.assertFalse(serializer.is_valid()) self.assertFalse(serializer.is_valid())
self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) self.assertEquals(serializer.errors, {'content': [u'Test not in value']})
...@@ -184,12 +207,12 @@ class ValidationTests(TestCase): ...@@ -184,12 +207,12 @@ class ValidationTests(TestCase):
'created': datetime.datetime(2012, 1, 1) 'created': datetime.datetime(2012, 1, 1)
} }
serializer = CommentSerializerWithCrossFieldValidator(data) serializer = CommentSerializerWithCrossFieldValidator(data=data)
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid())
data['content'] = 'A comment from foo@bar.com' data['content'] = 'A comment from foo@bar.com'
serializer = CommentSerializerWithCrossFieldValidator(data) serializer = CommentSerializerWithCrossFieldValidator(data=data)
self.assertFalse(serializer.is_valid()) self.assertFalse(serializer.is_valid())
self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']}) self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']})
...@@ -197,7 +220,7 @@ class ValidationTests(TestCase): ...@@ -197,7 +220,7 @@ class ValidationTests(TestCase):
""" """
Omitting a value for null-field should validate. Omitting a value for null-field should validate.
""" """
serializer = PersonSerializer({'name': 'marko'}) serializer = PersonSerializer(data={'name': 'marko'})
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
self.assertEquals(serializer.errors, {}) self.assertEquals(serializer.errors, {})
...@@ -247,7 +270,7 @@ class ManyToManyTests(TestCase): ...@@ -247,7 +270,7 @@ class ManyToManyTests(TestCase):
Create an instance of a model with a ManyToMany relationship. Create an instance of a model with a ManyToMany relationship.
""" """
data = {'rel': [self.anchor.id]} data = {'rel': [self.anchor.id]}
serializer = self.serializer_class(data) serializer = self.serializer_class(data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(len(ManyToManyModel.objects.all()), 2)
...@@ -261,7 +284,7 @@ class ManyToManyTests(TestCase): ...@@ -261,7 +284,7 @@ class ManyToManyTests(TestCase):
new_anchor = Anchor() new_anchor = Anchor()
new_anchor.save() new_anchor.save()
data = {'rel': [self.anchor.id, new_anchor.id]} data = {'rel': [self.anchor.id, new_anchor.id]}
serializer = self.serializer_class(data, instance=self.instance) serializer = self.serializer_class(self.instance, data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(ManyToManyModel.objects.all()), 1) self.assertEquals(len(ManyToManyModel.objects.all()), 1)
...@@ -274,7 +297,7 @@ class ManyToManyTests(TestCase): ...@@ -274,7 +297,7 @@ class ManyToManyTests(TestCase):
containing no items. containing no items.
""" """
data = {'rel': []} data = {'rel': []}
serializer = self.serializer_class(data) serializer = self.serializer_class(data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(len(ManyToManyModel.objects.all()), 2)
...@@ -289,7 +312,7 @@ class ManyToManyTests(TestCase): ...@@ -289,7 +312,7 @@ class ManyToManyTests(TestCase):
new_anchor = Anchor() new_anchor = Anchor()
new_anchor.save() new_anchor.save()
data = {'rel': []} data = {'rel': []}
serializer = self.serializer_class(data, instance=self.instance) serializer = self.serializer_class(self.instance, data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(ManyToManyModel.objects.all()), 1) self.assertEquals(len(ManyToManyModel.objects.all()), 1)
...@@ -303,7 +326,7 @@ class ManyToManyTests(TestCase): ...@@ -303,7 +326,7 @@ class ManyToManyTests(TestCase):
lists (eg form data). lists (eg form data).
""" """
data = {'rel': ''} data = {'rel': ''}
serializer = self.serializer_class(data) serializer = self.serializer_class(data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(len(ManyToManyModel.objects.all()), 2)
...@@ -341,7 +364,7 @@ class ReadOnlyManyToManyTests(TestCase): ...@@ -341,7 +364,7 @@ class ReadOnlyManyToManyTests(TestCase):
new_anchor = Anchor() new_anchor = Anchor()
new_anchor.save() new_anchor.save()
data = {'rel': [self.anchor.id, new_anchor.id]} data = {'rel': [self.anchor.id, new_anchor.id]}
serializer = self.serializer_class(data, instance=self.instance) serializer = self.serializer_class(self.instance, data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1) self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1)
...@@ -357,7 +380,7 @@ class ReadOnlyManyToManyTests(TestCase): ...@@ -357,7 +380,7 @@ class ReadOnlyManyToManyTests(TestCase):
new_anchor = Anchor() new_anchor = Anchor()
new_anchor.save() new_anchor.save()
data = {} data = {}
serializer = self.serializer_class(data, instance=self.instance) serializer = self.serializer_class(self.instance, data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1) self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1)
...@@ -377,7 +400,7 @@ class DefaultValueTests(TestCase): ...@@ -377,7 +400,7 @@ class DefaultValueTests(TestCase):
def test_create_using_default(self): def test_create_using_default(self):
data = {} data = {}
serializer = self.serializer_class(data) serializer = self.serializer_class(data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(self.objects.all()), 1) self.assertEquals(len(self.objects.all()), 1)
...@@ -386,7 +409,7 @@ class DefaultValueTests(TestCase): ...@@ -386,7 +409,7 @@ class DefaultValueTests(TestCase):
def test_create_overriding_default(self): def test_create_overriding_default(self):
data = {'text': 'overridden'} data = {'text': 'overridden'}
serializer = self.serializer_class(data) serializer = self.serializer_class(data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(self.objects.all()), 1) self.assertEquals(len(self.objects.all()), 1)
...@@ -405,7 +428,7 @@ class CallableDefaultValueTests(TestCase): ...@@ -405,7 +428,7 @@ class CallableDefaultValueTests(TestCase):
def test_create_using_default(self): def test_create_using_default(self):
data = {} data = {}
serializer = self.serializer_class(data) serializer = self.serializer_class(data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(self.objects.all()), 1) self.assertEquals(len(self.objects.all()), 1)
...@@ -414,7 +437,7 @@ class CallableDefaultValueTests(TestCase): ...@@ -414,7 +437,7 @@ class CallableDefaultValueTests(TestCase):
def test_create_overriding_default(self): def test_create_overriding_default(self):
data = {'text': 'overridden'} data = {'text': 'overridden'}
serializer = self.serializer_class(data) serializer = self.serializer_class(data=data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
instance = serializer.save() instance = serializer.save()
self.assertEquals(len(self.objects.all()), 1) self.assertEquals(len(self.objects.all()), 1)
...@@ -476,11 +499,11 @@ class BlankFieldTests(TestCase): ...@@ -476,11 +499,11 @@ class BlankFieldTests(TestCase):
self.data = {'title': ''} self.data = {'title': ''}
def test_create_blank_field(self): def test_create_blank_field(self):
serializer = self.serializer_class(self.data) serializer = self.serializer_class(data=self.data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
def test_create_model_blank_field(self): def test_create_model_blank_field(self):
serializer = self.model_serializer_class(self.data) serializer = self.model_serializer_class(data=self.data)
self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.is_valid(), True)
def test_create_not_blank_field(self): def test_create_not_blank_field(self):
...@@ -488,7 +511,7 @@ class BlankFieldTests(TestCase): ...@@ -488,7 +511,7 @@ class BlankFieldTests(TestCase):
Test to ensure blank data in a field not marked as blank=True Test to ensure blank data in a field not marked as blank=True
is considered invalid in a non-model serializer is considered invalid in a non-model serializer
""" """
serializer = self.not_blank_serializer_class(self.data) serializer = self.not_blank_serializer_class(data=self.data)
self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.is_valid(), False)
def test_create_model_not_blank_field(self): def test_create_model_not_blank_field(self):
...@@ -496,5 +519,5 @@ class BlankFieldTests(TestCase): ...@@ -496,5 +519,5 @@ class BlankFieldTests(TestCase):
Test to ensure blank data in a field not marked as blank=True Test to ensure blank data in a field not marked as blank=True
is considered invalid in a model serializer is considered invalid in a model serializer
""" """
serializer = self.not_blank_model_serializer_class(self.data) serializer = self.not_blank_model_serializer_class(data=self.data)
self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.is_valid(), False)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment