mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-23 22:49:50 +03:00
Merge branch 'master' into add_pk_support
This commit is contained in:
commit
bf4e6234be
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,7 +7,7 @@ html/
|
||||||
coverage/
|
coverage/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
rest_framework.egg-info/
|
*.egg-info/
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
|
||||||
!.gitignore
|
!.gitignore
|
||||||
|
|
30
README.md
30
README.md
|
@ -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.
|
||||||
[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
|
||||||
|
|
|
@ -30,7 +30,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework.authentication.UserBasicAuthentication',
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
|
||||||
You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
|
You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
|
||||||
|
|
||||||
class ExampleView(APIView):
|
class ExampleView(APIView):
|
||||||
authentication_classes = (SessionAuthentication, UserBasicAuthentication)
|
authentication_classes = (SessionAuthentication, BasicAuthentication)
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
|
@ -51,7 +51,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
|
||||||
Or, if you're using the `@api_view` decorator with function based views.
|
Or, if you're using the `@api_view` decorator with function based views.
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@authentication_classes((SessionAuthentication, UserBasicAuthentication))
|
@authentication_classes((SessionAuthentication, BasicAuthentication))
|
||||||
@permissions_classes((IsAuthenticated,))
|
@permissions_classes((IsAuthenticated,))
|
||||||
def example_view(request, format=None):
|
def example_view(request, format=None):
|
||||||
content = {
|
content = {
|
||||||
|
|
|
@ -235,44 +235,54 @@ 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'`.
|
||||||
|
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
||||||
|
* `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.
|
||||||
|
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
||||||
|
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
||||||
|
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
||||||
|
|
||||||
[cite]: http://www.python.org/dev/peps/pep-0020/
|
[cite]: http://www.python.org/dev/peps/pep-0020/
|
||||||
|
|
|
@ -257,6 +257,21 @@ In [the words of Roy Fielding][quote], "A REST API should spend almost all of it
|
||||||
|
|
||||||
For good examples of custom media types, see GitHub's use of a custom [application/vnd.github+json] media type, and Mike Amundsen's IANA approved [application/vnd.collection+json] JSON-based hypermedia.
|
For good examples of custom media types, see GitHub's use of a custom [application/vnd.github+json] media type, and Mike Amundsen's IANA approved [application/vnd.collection+json] JSON-based hypermedia.
|
||||||
|
|
||||||
|
## HTML error views
|
||||||
|
|
||||||
|
Typically a renderer will behave the same regardless of if it's dealing with a regular response, or with a response caused by an exception being raised, such as an `Http404` or `PermissionDenied` exception, or a subclass of `APIException`.
|
||||||
|
|
||||||
|
If you're using either the `TemplateHTMLRenderer` or the `StaticHTMLRenderer` and an exception is raised, the behavior is slightly different, and mirrors [Django's default handling of error views][django-error-views].
|
||||||
|
|
||||||
|
Exceptions raised and handled by an HTML renderer will attempt to render using one of the following methods, by order of precedence.
|
||||||
|
|
||||||
|
* Load and render a template named `{status_code}.html`.
|
||||||
|
* Load and render a template named `api_exception.html`.
|
||||||
|
* Render the HTTP status code and text, for example "404 Not Found".
|
||||||
|
|
||||||
|
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
|
||||||
|
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
||||||
[conneg]: content-negotiation.md
|
[conneg]: content-negotiation.md
|
||||||
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
||||||
|
@ -265,3 +280,4 @@ For good examples of custom media types, see GitHub's use of a custom [applicati
|
||||||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||||
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
||||||
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
|
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
|
||||||
|
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
|
|
@ -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
|
||||||
|
|
||||||
...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>()`
|
||||||
|
|
||||||
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`.
|
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.
|
||||||
|
|
||||||
|
The generic views provided by REST framework call the `.save()` method when updating or creating entities.
|
||||||
|
|
||||||
## Dealing with nested objects
|
## Dealing with nested objects
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_THROTTLE_CLASSES': (
|
'DEFAULT_THROTTLE_CLASSES': (
|
||||||
'rest_framework.throttles.AnonThrottle',
|
'rest_framework.throttling.AnonRateThrottle',
|
||||||
'rest_framework.throttles.UserThrottle'
|
'rest_framework.throttling.UserRateThrottle'
|
||||||
),
|
),
|
||||||
'DEFAULT_THROTTLE_RATES': {
|
'DEFAULT_THROTTLE_RATES': {
|
||||||
'anon': '100/day',
|
'anon': '100/day',
|
||||||
|
@ -136,7 +136,7 @@ For example, given the following views...
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_THROTTLE_CLASSES': (
|
'DEFAULT_THROTTLE_CLASSES': (
|
||||||
'rest_framework.throttles.ScopedRateThrottle'
|
'rest_framework.throttling.ScopedRateThrottle'
|
||||||
),
|
),
|
||||||
'DEFAULT_THROTTLE_RATES': {
|
'DEFAULT_THROTTLE_RATES': {
|
||||||
'contacts': '1000/day',
|
'contacts': '1000/day',
|
||||||
|
|
|
@ -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 @@
|
||||||
<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,11 @@ 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]
|
||||||
|
* Toni Michel - [tonimichel]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -80,7 +85,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 +144,8 @@ 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
|
||||||
|
[tonimichel]: https://github.com/tonimichel
|
|
@ -6,12 +6,42 @@
|
||||||
|
|
||||||
## Master
|
## Master
|
||||||
|
|
||||||
|
* Support use of HTML exception templates. Eg. `403.html`
|
||||||
|
|
||||||
|
## 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 +147,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
|
||||||
|
[announcement]: rest-framework-2-announcement.md
|
|
@ -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
|
||||||
|
|
||||||
...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
|
||||||
"""
|
"""
|
||||||
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
|
||||||
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
|
||||||
"""
|
"""
|
||||||
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
|
||||||
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
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
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.
|
||||||
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
|
||||||
* 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
|
||||||
|
|
||||||
|
|
||||||
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`..
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
__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
|
||||||
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):
|
||||||
|
|
||||||
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):
|
||||||
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):
|
||||||
|
@ -132,7 +135,7 @@ class WritableField(Field):
|
||||||
self.error_messages = messages
|
self.error_messages = messages
|
||||||
|
|
||||||
self.validators = self.default_validators + validators
|
self.validators = self.default_validators + validators
|
||||||
self.default = default or self.default
|
self.default = default if default is not None else self.default
|
||||||
self.blank = blank
|
self.blank = blank
|
||||||
|
|
||||||
# Widgets are ony used for HTML forms.
|
# Widgets are ony used for HTML forms.
|
||||||
|
@ -209,11 +212,11 @@ class ModelField(WritableField):
|
||||||
super(ModelField, self).__init__(*args, **kwargs)
|
super(ModelField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
try:
|
rel = getattr(self.model_field, "rel", None)
|
||||||
rel = self.model_field.rel
|
if rel is not None:
|
||||||
except:
|
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
||||||
|
else:
|
||||||
return self.model_field.to_python(value)
|
return self.model_field.to_python(value)
|
||||||
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
value = self.model_field._get_val_from_obj(obj)
|
value = self.model_field._get_val_from_obj(obj)
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
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):
|
||||||
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):
|
||||||
|
|
||||||
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):
|
||||||
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):
|
||||||
|
|
||||||
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,76 @@ 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` unless overridden
|
||||||
|
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)
|
||||||
|
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||||
|
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
|
||||||
|
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
|
||||||
|
|
||||||
self.format = kwargs.pop('format', None)
|
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,26 +590,61 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
pk_url_kwarg = 'pk'
|
||||||
|
slug_field = 'slug'
|
||||||
|
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
self.slug_field = kwargs.pop('slug_field', self.slug_field)
|
||||||
|
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||||
|
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
|
||||||
|
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
|
||||||
|
|
||||||
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
|
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
request = self.context.get('request', None)
|
request = self.context.get('request', None)
|
||||||
format = self.format or self.context.get('format', None)
|
format = self.format or self.context.get('format', None)
|
||||||
view_name = self.view_name or self.parent.opts.view_name
|
view_name = self.view_name or self.parent.opts.view_name
|
||||||
view_kwargs = {'pk': obj.pk}
|
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||||
return reverse(view_name, kwargs=view_kwargs, request=request, format=format)
|
try:
|
||||||
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
slug = getattr(obj, self.slug_field, None)
|
||||||
|
|
||||||
|
if not slug:
|
||||||
|
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||||
|
|
||||||
|
kwargs = {self.slug_url_kwarg: slug}
|
||||||
|
try:
|
||||||
|
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
||||||
|
try:
|
||||||
|
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
|
||||||
|
|
||||||
|
|
||||||
##### Typed Fields #####
|
##### Typed Fields #####
|
||||||
|
|
|
@ -43,12 +43,12 @@ class GenericAPIView(views.APIView):
|
||||||
|
|
||||||
return serializer_class
|
return serializer_class
|
||||||
|
|
||||||
def get_serializer(self, data=None, files=None, instance=None):
|
def get_serializer(self, instance=None, data=None, files=None):
|
||||||
# TODO: add support for files
|
# TODO: add support for files
|
||||||
# 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):
|
||||||
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."
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ class ListModelMixin(object):
|
||||||
paginator, page, queryset, is_paginated = packed
|
paginator, page, queryset, is_paginated = packed
|
||||||
serializer = self.get_pagination_serializer(page)
|
serializer = self.get_pagination_serializer(page)
|
||||||
else:
|
else:
|
||||||
serializer = self.get_serializer(instance=self.object_list)
|
serializer = self.get_serializer(self.object_list)
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class RetrieveModelMixin(object):
|
||||||
"""
|
"""
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
serializer = self.get_serializer(instance=self.object)
|
serializer = self.get_serializer(self.object)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ class UpdateModelMixin(object):
|
||||||
self.object = None
|
self.object = None
|
||||||
success_status = status.HTTP_201_CREATED
|
success_status = status.HTTP_201_CREATED
|
||||||
|
|
||||||
serializer = self.get_serializer(data=request.DATA, instance=self.object)
|
serializer = self.get_serializer(self.object, data=request.DATA)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(serializer.object)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import copy
|
||||||
import string
|
import string
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.http.multipartparser import parse_header
|
from django.http.multipartparser import parse_header
|
||||||
from django.template import RequestContext, loader
|
from django.template import RequestContext, loader, Template
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
from rest_framework.compat import yaml
|
from rest_framework.compat import yaml
|
||||||
from rest_framework.exceptions import ConfigurationError
|
from rest_framework.exceptions import ConfigurationError
|
||||||
|
@ -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):
|
||||||
|
@ -162,6 +162,10 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
media_type = 'text/html'
|
media_type = 'text/html'
|
||||||
format = 'html'
|
format = 'html'
|
||||||
template_name = None
|
template_name = None
|
||||||
|
exception_template_names = [
|
||||||
|
'%(status_code)s.html',
|
||||||
|
'api_exception.html'
|
||||||
|
]
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
|
@ -178,15 +182,21 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
request = renderer_context['request']
|
request = renderer_context['request']
|
||||||
response = renderer_context['response']
|
response = renderer_context['response']
|
||||||
|
|
||||||
template_names = self.get_template_names(response, view)
|
if response.exception:
|
||||||
template = self.resolve_template(template_names)
|
template = self.get_exception_template(response)
|
||||||
context = self.resolve_context(data, request)
|
else:
|
||||||
|
template_names = self.get_template_names(response, view)
|
||||||
|
template = self.resolve_template(template_names)
|
||||||
|
|
||||||
|
context = self.resolve_context(data, request, response)
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
def resolve_template(self, template_names):
|
def resolve_template(self, template_names):
|
||||||
return loader.select_template(template_names)
|
return loader.select_template(template_names)
|
||||||
|
|
||||||
def resolve_context(self, data, request):
|
def resolve_context(self, data, request, response):
|
||||||
|
if response.exception:
|
||||||
|
data['status_code'] = response.status_code
|
||||||
return RequestContext(request, data)
|
return RequestContext(request, data)
|
||||||
|
|
||||||
def get_template_names(self, response, view):
|
def get_template_names(self, response, view):
|
||||||
|
@ -198,8 +208,21 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
return view.get_template_names()
|
return view.get_template_names()
|
||||||
raise ConfigurationError('Returned a template response with no template_name')
|
raise ConfigurationError('Returned a template response with no template_name')
|
||||||
|
|
||||||
|
def get_exception_template(self, response):
|
||||||
|
template_names = [name % {'status_code': response.status_code}
|
||||||
|
for name in self.exception_template_names]
|
||||||
|
|
||||||
class StaticHTMLRenderer(BaseRenderer):
|
try:
|
||||||
|
# Try to find an appropriate error template
|
||||||
|
return self.resolve_template(template_names)
|
||||||
|
except:
|
||||||
|
# Fall back to using eg '404 Not Found'
|
||||||
|
return Template('%d %s' % (response.status_code,
|
||||||
|
response.status_text.title()))
|
||||||
|
|
||||||
|
|
||||||
|
# Note, subclass TemplateHTMLRenderer simply for the exception behavior
|
||||||
|
class StaticHTMLRenderer(TemplateHTMLRenderer):
|
||||||
"""
|
"""
|
||||||
An HTML renderer class that simply returns pre-rendered HTML.
|
An HTML renderer class that simply returns pre-rendered HTML.
|
||||||
|
|
||||||
|
@ -216,6 +239,15 @@ class StaticHTMLRenderer(BaseRenderer):
|
||||||
format = 'html'
|
format = 'html'
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
renderer_context = renderer_context or {}
|
||||||
|
response = renderer_context['response']
|
||||||
|
|
||||||
|
if response and response.exception:
|
||||||
|
request = renderer_context['request']
|
||||||
|
template = self.get_exception_template(response)
|
||||||
|
context = self.resolve_context(data, request, response)
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -281,11 +313,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 +331,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,7 +349,10 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
try:
|
try:
|
||||||
fields[k] = field_mapping[v.__class__](**kwargs)
|
fields[k] = field_mapping[v.__class__](**kwargs)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
fields[k] = forms.CharField(**kwargs)
|
if getattr(v, 'choices', None) is not None:
|
||||||
|
fields[k] = forms.ChoiceField(**kwargs)
|
||||||
|
else:
|
||||||
|
fields[k] = forms.CharField(**kwargs)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_form(self, view, method, request):
|
def get_form(self, view, method, request):
|
||||||
|
|
|
@ -9,7 +9,8 @@ class Response(SimpleTemplateResponse):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data=None, status=200,
|
def __init__(self, data=None, status=200,
|
||||||
template_name=None, headers=None):
|
template_name=None, headers=None,
|
||||||
|
exception=False):
|
||||||
"""
|
"""
|
||||||
Alters the init arguments slightly.
|
Alters the init arguments slightly.
|
||||||
For example, drop 'template_name', and instead use 'data'.
|
For example, drop 'template_name', and instead use 'data'.
|
||||||
|
@ -21,6 +22,7 @@ class Response(SimpleTemplateResponse):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.headers = headers and headers[:] or []
|
self.headers = headers and headers[:] or []
|
||||||
self.template_name = template_name
|
self.template_name = template_name
|
||||||
|
self.exception = exception
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rendered_content(self):
|
def rendered_content(self):
|
||||||
|
@ -45,3 +47,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():
|
||||||
'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 = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
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):
|
||||||
_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):
|
||||||
#####
|
#####
|
||||||
# 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):
|
||||||
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
|
||||||
|
@ -154,12 +163,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
|
||||||
|
|
||||||
|
@ -180,7 +189,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)
|
||||||
|
@ -193,7 +202,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:
|
||||||
|
@ -208,7 +217,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:
|
||||||
|
@ -247,11 +256,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):
|
||||||
|
@ -333,7 +339,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.
|
||||||
"""
|
"""
|
||||||
|
@ -369,7 +375,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
|
||||||
|
@ -464,7 +470,7 @@ class ModelSerializer(Serializer):
|
||||||
"""
|
"""
|
||||||
self.object.save()
|
self.object.save()
|
||||||
|
|
||||||
if self.m2m_data and save_m2m:
|
if getattr(self, 'm2m_data', None) and save_m2m:
|
||||||
for accessor_name, object_list in self.m2m_data.items():
|
for accessor_name, object_list in self.m2m_data.items():
|
||||||
setattr(self.object, accessor_name, object_list)
|
setattr(self.object, accessor_name, object_list)
|
||||||
self.m2m_data = {}
|
self.m2m_data = {}
|
||||||
|
|
|
@ -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 @@
|
||||||
{% 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 @@
|
||||||
{% 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):
|
||||||
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/'
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
|
from django.http import Http404
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.template import TemplateDoesNotExist, Template
|
from django.template import TemplateDoesNotExist, Template
|
||||||
import django.template.loader
|
import django.template.loader
|
||||||
|
@ -17,8 +19,22 @@ def example(request):
|
||||||
return Response(data, template_name='example.html')
|
return Response(data, template_name='example.html')
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(('GET',))
|
||||||
|
@renderer_classes((TemplateHTMLRenderer,))
|
||||||
|
def permission_denied(request):
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(('GET',))
|
||||||
|
@renderer_classes((TemplateHTMLRenderer,))
|
||||||
|
def not_found(request):
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', example),
|
url(r'^$', example),
|
||||||
|
url(r'^permission_denied$', permission_denied),
|
||||||
|
url(r'^not_found$', not_found),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,3 +64,52 @@ class TemplateHTMLRendererTests(TestCase):
|
||||||
response = self.client.get('/')
|
response = self.client.get('/')
|
||||||
self.assertContains(response, "example: foobar")
|
self.assertContains(response, "example: foobar")
|
||||||
self.assertEquals(response['Content-Type'], 'text/html')
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
def test_not_found_html_view(self):
|
||||||
|
response = self.client.get('/not_found')
|
||||||
|
self.assertEquals(response.status_code, 404)
|
||||||
|
self.assertEquals(response.content, "404 Not Found")
|
||||||
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
def test_permission_denied_html_view(self):
|
||||||
|
response = self.client.get('/permission_denied')
|
||||||
|
self.assertEquals(response.status_code, 403)
|
||||||
|
self.assertEquals(response.content, "403 Forbidden")
|
||||||
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateHTMLRendererExceptionTests(TestCase):
|
||||||
|
urls = 'rest_framework.tests.htmlrenderer'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Monkeypatch get_template
|
||||||
|
"""
|
||||||
|
self.get_template = django.template.loader.get_template
|
||||||
|
|
||||||
|
def get_template(template_name):
|
||||||
|
if template_name == '404.html':
|
||||||
|
return Template("404: {{ detail }}")
|
||||||
|
if template_name == '403.html':
|
||||||
|
return Template("403: {{ detail }}")
|
||||||
|
raise TemplateDoesNotExist(template_name)
|
||||||
|
|
||||||
|
django.template.loader.get_template = get_template
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""
|
||||||
|
Revert monkeypatching
|
||||||
|
"""
|
||||||
|
django.template.loader.get_template = self.get_template
|
||||||
|
|
||||||
|
def test_not_found_html_view_with_template(self):
|
||||||
|
response = self.client.get('/not_found')
|
||||||
|
self.assertEquals(response.status_code, 404)
|
||||||
|
self.assertEquals(response.content, "404: Not found")
|
||||||
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
def test_permission_denied_html_view_with_template(self):
|
||||||
|
response = self.client.get('/permission_denied')
|
||||||
|
self.assertEquals(response.status_code, 403)
|
||||||
|
self.assertEquals(response.content, "403: Permission denied")
|
||||||
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
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('',
|
||||||
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):
|
||||||
|
|
||||||
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')
|
||||||
|
|
|
@ -118,10 +118,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):
|
||||||
|
|
|
@ -74,13 +74,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')
|
||||||
|
|
187
rest_framework/tests/pk_relations.py
Normal file
187
rest_framework/tests/pk_relations.py
Normal 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()
|
||||||
|
|
||||||
|
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)
|
|
@ -1,6 +1,8 @@
|
||||||
|
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):
|
||||||
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):
|
||||||
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)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
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):
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
'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):
|
||||||
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):
|
||||||
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):
|
||||||
)
|
)
|
||||||
|
|
||||||
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):
|
||||||
'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):
|
||||||
'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):
|
||||||
'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):
|
||||||
'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):
|
||||||
"""
|
"""
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
|
|
||||||
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):
|
||||||
|
|
||||||
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):
|
||||||
|
|
||||||
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):
|
||||||
|
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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)
|
||||||
|
|
|
@ -320,13 +320,17 @@ class APIView(View):
|
||||||
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
||||||
|
|
||||||
if isinstance(exc, exceptions.APIException):
|
if isinstance(exc, exceptions.APIException):
|
||||||
return Response({'detail': exc.detail}, status=exc.status_code)
|
return Response({'detail': exc.detail},
|
||||||
|
status=exc.status_code,
|
||||||
|
exception=True)
|
||||||
elif isinstance(exc, Http404):
|
elif isinstance(exc, Http404):
|
||||||
return Response({'detail': 'Not found'},
|
return Response({'detail': 'Not found'},
|
||||||
status=status.HTTP_404_NOT_FOUND)
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
exception=True)
|
||||||
elif isinstance(exc, PermissionDenied):
|
elif isinstance(exc, PermissionDenied):
|
||||||
return Response({'detail': 'Permission denied'},
|
return Response({'detail': 'Permission denied'},
|
||||||
status=status.HTTP_403_FORBIDDEN)
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
exception=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Note: session based authentication is explicitly CSRF validated,
|
# Note: session based authentication is explicitly CSRF validated,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user