mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-27 03:54:01 +03:00
commit
4042180392
|
@ -62,7 +62,6 @@ To run the tests, clone the repository, and then:
|
||||||
virtualenv env
|
virtualenv env
|
||||||
env/bin/activate
|
env/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install -r requirements-test.txt
|
|
||||||
|
|
||||||
# Run the tests
|
# Run the tests
|
||||||
./runtests.py
|
./runtests.py
|
||||||
|
|
12
README.md
12
README.md
|
@ -3,13 +3,13 @@
|
||||||
[![build-status-image]][travis]
|
[![build-status-image]][travis]
|
||||||
[![pypi-version]][pypi]
|
[![pypi-version]][pypi]
|
||||||
|
|
||||||
**Awesome web-browseable Web APIs.**
|
**Awesome web-browsable Web APIs.**
|
||||||
|
|
||||||
Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
|
Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: The incoming 3.0 version has now been merged to the `master` branch on GitHub. For the source of the currently available PyPI version, please see the `2.4.4` tag.
|
**Note**: We have now released Django REST framework 3.0. For older codebases you may want to refer to the version 2.4.4 [source code](https://github.com/tomchristie/django-rest-framework/tree/version-2.4.x), and [documentation](http://tomchristie.github.io/rest-framework-2-docs/).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ Django REST framework is a powerful and flexible toolkit for building Web APIs.
|
||||||
|
|
||||||
Some reasons you might want to use REST framework:
|
Some reasons you might want to use REST framework:
|
||||||
|
|
||||||
* The [Web browseable API][sandbox] is a huge useability win for your developers.
|
* The [Web browsable API][sandbox] is a huge usability win for your developers.
|
||||||
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
|
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
|
||||||
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
|
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
|
||||||
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
||||||
|
@ -27,7 +27,7 @@ Some reasons you might want to use REST framework:
|
||||||
|
|
||||||
There is a live example API for testing purposes, [available here][sandbox].
|
There is a live example API for testing purposes, [available here][sandbox].
|
||||||
|
|
||||||
**Below**: *Screenshot from the browseable API*
|
**Below**: *Screenshot from the browsable API*
|
||||||
|
|
||||||
![Screenshot][image]
|
![Screenshot][image]
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ router.register(r'users', UserViewSet)
|
||||||
|
|
||||||
|
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
# Additionally, we include login URLs for the browseable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^', include(router.urls)),
|
url(r'^', include(router.urls)),
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
|
@ -180,7 +180,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
|
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master
|
||||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||||
[pypi-version]: https://pypip.in/version/djangorestframework/badge.svg
|
[pypi-version]: https://pypip.in/version/djangorestframework/badge.svg
|
||||||
[pypi]: https://pypi.python.org/pypi/djangorestframework
|
[pypi]: https://pypi.python.org/pypi/djangorestframework
|
||||||
|
|
|
@ -453,7 +453,7 @@ If you want to create a custom field, you'll need to subclass `Field` and then o
|
||||||
|
|
||||||
The `.to_representation()` method is called to convert the initial datatype into a primitive, serializable datatype.
|
The `.to_representation()` method is called to convert the initial datatype into a primitive, serializable datatype.
|
||||||
|
|
||||||
The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation.
|
The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. This method should raise a `serializer.ValidationError` if the data is invalid.
|
||||||
|
|
||||||
Note that the `WritableField` class that was present in version 2.x no longer exists. You should subclass `Field` and override `to_internal_value()` if the field supports data input.
|
Note that the `WritableField` class that was present in version 2.x no longer exists. You should subclass `Field` and override `to_internal_value()` if the field supports data input.
|
||||||
|
|
||||||
|
@ -498,6 +498,53 @@ As an example, let's create a field that can be used represent the class name of
|
||||||
"""
|
"""
|
||||||
return obj.__class__.__name__
|
return obj.__class__.__name__
|
||||||
|
|
||||||
|
#### Raising validation errors
|
||||||
|
|
||||||
|
Our `ColorField` class above currently does not perform any data validation.
|
||||||
|
To indicate invalid data, we should raise a `serializers.ValidationError`, like so:
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if not isinstance(data, six.text_type):
|
||||||
|
msg = 'Incorrect type. Expected a string, but got %s'
|
||||||
|
raise ValidationError(msg % type(data).__name__)
|
||||||
|
|
||||||
|
if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data):
|
||||||
|
raise ValidationError('Incorrect format. Expected `rgb(#,#,#)`.')
|
||||||
|
|
||||||
|
data = data.strip('rgb(').rstrip(')')
|
||||||
|
red, green, blue = [int(col) for col in data.split(',')]
|
||||||
|
|
||||||
|
if any([col > 255 or col < 0 for col in (red, green, blue)]):
|
||||||
|
raise ValidationError('Value out of range. Must be between 0 and 255.')
|
||||||
|
|
||||||
|
return Color(red, green, blue)
|
||||||
|
|
||||||
|
The `.fail()` method is a shortcut for raising `ValidationError` that takes a message string from the `error_messages` dictionary. For example:
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
'incorrect_type': 'Incorrect type. Expected a string, but got {input_type}',
|
||||||
|
'incorrect_format': 'Incorrect format. Expected `rgb(#,#,#)`.',
|
||||||
|
'out_of_range': 'Value out of range. Must be between 0 and 255.'
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if not isinstance(data, six.text_type):
|
||||||
|
msg = 'Incorrect type. Expected a string, but got %s'
|
||||||
|
self.fail('incorrect_type', input_type=type(data).__name__)
|
||||||
|
|
||||||
|
if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data):
|
||||||
|
self.fail('incorrect_format')
|
||||||
|
|
||||||
|
data = data.strip('rgb(').rstrip(')')
|
||||||
|
red, green, blue = [int(col) for col in data.split(',')]
|
||||||
|
|
||||||
|
if any([col > 255 or col < 0 for col in (red, green, blue)]):
|
||||||
|
self.fail('out_of_range')
|
||||||
|
|
||||||
|
return Color(red, green, blue)
|
||||||
|
|
||||||
|
This style keeps you error messages more cleanly separated from your code, and should be preferred.
|
||||||
|
|
||||||
# Third party packages
|
# Third party packages
|
||||||
|
|
||||||
The following third party packages are also available.
|
The following third party packages are also available.
|
||||||
|
|
|
@ -96,7 +96,7 @@ If we want to be able to return complete object instances based on the validated
|
||||||
If your object instances correspond to Django models you'll also want to ensure that these methods save the object to the database. For example, if `Comment` was a Django model, the methods might look like this:
|
If your object instances correspond to Django models you'll also want to ensure that these methods save the object to the database. For example, if `Comment` was a Django model, the methods might look like this:
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
return Comment.objcts.create(**validated_data)
|
return Comment.objects.create(**validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
instance.email = validated_data.get('email', instance.email)
|
instance.email = validated_data.get('email', instance.email)
|
||||||
|
@ -567,13 +567,13 @@ There needs to be a way of determining which views should be used for hyperlinki
|
||||||
|
|
||||||
By default hyperlinks are expected to correspond to a view name that matches the style `'{model_name}-detail'`, and looks up the instance by a `pk` keyword argument.
|
By default hyperlinks are expected to correspond to a view name that matches the style `'{model_name}-detail'`, and looks up the instance by a `pk` keyword argument.
|
||||||
|
|
||||||
You can override a URL field view name and lookup field by using either, or both of, the `view_name` and `lookup_field` options in the `extra_field_kwargs` setting, like so:
|
You can override a URL field view name and lookup field by using either, or both of, the `view_name` and `lookup_field` options in the `extra_kwargs` setting, like so:
|
||||||
|
|
||||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account
|
model = Account
|
||||||
fields = ('account_url', 'account_name', 'users', 'created')
|
fields = ('account_url', 'account_name', 'users', 'created')
|
||||||
extra_field_kwargs = {
|
extra_kwargs = {
|
||||||
'url': {'view_name': 'accounts', 'lookup_field': 'account_name'}
|
'url': {'view_name': 'accounts', 'lookup_field': 'account_name'}
|
||||||
'users': {'lookup_field': 'username'}
|
'users': {'lookup_field': 'username'}
|
||||||
}
|
}
|
||||||
|
@ -689,6 +689,21 @@ Here's an example of how you might choose to implement multiple updates:
|
||||||
|
|
||||||
It is possible that a third party package may be included alongside the 3.1 release that provides some automatic support for multiple update operations, similar to the `allow_add_remove` behavior that was present in REST framework 2.
|
It is possible that a third party package may be included alongside the 3.1 release that provides some automatic support for multiple update operations, similar to the `allow_add_remove` behavior that was present in REST framework 2.
|
||||||
|
|
||||||
|
#### Customizing ListSerializer initialization
|
||||||
|
|
||||||
|
When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class.
|
||||||
|
|
||||||
|
The default implementation is to pass all arguments to both classes, except for `validators`, and any custom keyword arguments, both of which are assumed to be intended for the child serializer class.
|
||||||
|
|
||||||
|
Occasionally you might need to explicitly specify how the child and parent classes should be instantiated when `many=True` is passed. You can do so by using the `many_init` class method.
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def many_init(cls, *args, **kwargs):
|
||||||
|
# Instantiate the child serializer.
|
||||||
|
kwargs['child'] = cls()
|
||||||
|
# Instantiate the parent list serializer.
|
||||||
|
return CustomListSerializer(*args, **kwargs)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# BaseSerializer
|
# BaseSerializer
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://www.django-rest-framework.org" data-count="none"></a>
|
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://www.django-rest-framework.org" data-count="none"></a>
|
||||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||||
|
|
||||||
<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
|
<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master" class="travis-build-image">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -33,7 +33,7 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b
|
||||||
|
|
||||||
Some reasons you might want to use REST framework:
|
Some reasons you might want to use REST framework:
|
||||||
|
|
||||||
* The [Web browseable API][sandbox] is a huge usability win for your developers.
|
* The [Web browsable API][sandbox] is a huge usability win for your developers.
|
||||||
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
|
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
|
||||||
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
|
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
|
||||||
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
||||||
|
@ -134,7 +134,7 @@ Here's our project's root `urls.py` module:
|
||||||
router.register(r'users', UserViewSet)
|
router.register(r'users', UserViewSet)
|
||||||
|
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
# Additionally, we include login URLs for the browseable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^', include(router.urls)),
|
url(r'^', include(router.urls)),
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
|
|
|
@ -35,7 +35,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w
|
||||||
|
|
||||||
|
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
# Additionally, we include login URLs for the browseable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^', include(router.urls)),
|
url(r'^', include(router.urls)),
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
|
@ -207,9 +207,9 @@ The old-style signature will continue to function but will raise a `PendingDepre
|
||||||
|
|
||||||
## View names and descriptions
|
## View names and descriptions
|
||||||
|
|
||||||
The mechanics of how the names and descriptions used in the browseable API are generated has been modified and cleaned up somewhat.
|
The mechanics of how the names and descriptions used in the browsable API are generated has been modified and cleaned up somewhat.
|
||||||
|
|
||||||
If you've been customizing this behavior, for example perhaps to use `rst` markup for the browseable API, then you'll need to take a look at the implementation to see what updates you need to make.
|
If you've been customizing this behavior, for example perhaps to use `rst` markup for the browsable API, then you'll need to take a look at the implementation to see what updates you need to make.
|
||||||
|
|
||||||
Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated.
|
Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated.
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,6 @@ To run the tests, clone the repository, and then:
|
||||||
virtualenv env
|
virtualenv env
|
||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install -r requirements-test.txt
|
|
||||||
|
|
||||||
# Run the tests
|
# Run the tests
|
||||||
./runtests.py
|
./runtests.py
|
||||||
|
|
|
@ -121,7 +121,7 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
|
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
|
||||||
* Fix `parse_header` argument convertion.
|
* Fix `parse_header` argument convertion.
|
||||||
* Fix mediatype detection under Python 3.
|
* Fix mediatype detection under Python 3.
|
||||||
* Web browseable API now offers blank option on dropdown when the field is not required.
|
* Web browsable API now offers blank option on dropdown when the field is not required.
|
||||||
* `APIException` representation improved for logging purposes.
|
* `APIException` representation improved for logging purposes.
|
||||||
* Allow source="*" within nested serializers.
|
* Allow source="*" within nested serializers.
|
||||||
* Better support for custom oauth2 provider backends.
|
* Better support for custom oauth2 provider backends.
|
||||||
|
@ -200,7 +200,7 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute.
|
* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute.
|
||||||
* Added `cache` attribute to throttles to allow overriding of default cache.
|
* Added `cache` attribute to throttles to allow overriding of default cache.
|
||||||
* 'Raw data' tab in browsable API now contains pre-populated data.
|
* 'Raw data' tab in browsable API now contains pre-populated data.
|
||||||
* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views.
|
* 'Raw data' and 'HTML form' tab preference in browsable API now saved between page views.
|
||||||
* Bugfix: `required=True` argument fixed for boolean serializer fields.
|
* Bugfix: `required=True` argument fixed for boolean serializer fields.
|
||||||
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
|
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
|
||||||
* Bugfix: Client sending empty string instead of file now clears `FileField`.
|
* Bugfix: Client sending empty string instead of file now clears `FileField`.
|
||||||
|
|
|
@ -93,7 +93,7 @@ The cookiecutter template includes a `runtests.py` which uses the `pytest` packa
|
||||||
|
|
||||||
Before running, you'll need to install a couple test requirements.
|
Before running, you'll need to install a couple test requirements.
|
||||||
|
|
||||||
$ pip install -r requirements-test.txt
|
$ pip install -r requirements.txt
|
||||||
|
|
||||||
Once requirements installed, you can run `runtests.py`.
|
Once requirements installed, you can run `runtests.py`.
|
||||||
|
|
||||||
|
|
|
@ -110,21 +110,21 @@ The first thing we need to get started on our Web API is to provide a way of ser
|
||||||
style = serializers.ChoiceField(choices=STYLE_CHOICES,
|
style = serializers.ChoiceField(choices=STYLE_CHOICES,
|
||||||
default='friendly')
|
default='friendly')
|
||||||
|
|
||||||
def create(self, validated_attrs):
|
def create(self, validated_data):
|
||||||
"""
|
"""
|
||||||
Create and return a new `Snippet` instance, given the validated data.
|
Create and return a new `Snippet` instance, given the validated data.
|
||||||
"""
|
"""
|
||||||
return Snippet.objects.create(**validated_attrs)
|
return Snippet.objects.create(**validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_attrs):
|
def update(self, instance, validated_data):
|
||||||
"""
|
"""
|
||||||
Update and return an existing `Snippet` instance, given the validated data.
|
Update and return an existing `Snippet` instance, given the validated data.
|
||||||
"""
|
"""
|
||||||
instance.title = validated_attrs.get('title', instance.title)
|
instance.title = validated_data.get('title', instance.title)
|
||||||
instance.code = validated_attrs.get('code', instance.code)
|
instance.code = validated_data.get('code', instance.code)
|
||||||
instance.linenos = validated_attrs.get('linenos', instance.linenos)
|
instance.linenos = validated_data.get('linenos', instance.linenos)
|
||||||
instance.language = validated_attrs.get('language', instance.language)
|
instance.language = validated_data.get('language', instance.language)
|
||||||
instance.style = validated_attrs.get('style', instance.style)
|
instance.style = validated_data.get('style', instance.style)
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ Here's our re-wired `urls.py` file.
|
||||||
router.register(r'users', views.UserViewSet)
|
router.register(r'users', views.UserViewSet)
|
||||||
|
|
||||||
# The API URLs are now determined automatically by the router.
|
# The API URLs are now determined automatically by the router.
|
||||||
# Additionally, we include the login URLs for the browseable API.
|
# Additionally, we include the login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^', include(router.urls)),
|
url(r'^', include(router.urls)),
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
|
@ -130,7 +130,7 @@ That doesn't mean it's always the right approach to take. There's a similar set
|
||||||
|
|
||||||
## Reviewing our work
|
## Reviewing our work
|
||||||
|
|
||||||
With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
|
With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
|
||||||
|
|
||||||
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
|
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
|
||||||
router.register(r'groups', views.GroupViewSet)
|
router.register(r'groups', views.GroupViewSet)
|
||||||
|
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
# Additionally, we include login URLs for the browseable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^', include(router.urls)),
|
url(r'^', include(router.urls)),
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Test requirements
|
|
||||||
pytest-django==2.6
|
|
||||||
pytest==2.5.2
|
|
||||||
pytest-cov==1.6
|
|
||||||
flake8==2.2.2
|
|
||||||
|
|
||||||
# Optional packages
|
|
||||||
markdown>=2.1.0
|
|
||||||
PyYAML>=3.10
|
|
||||||
defusedxml>=0.3
|
|
||||||
django-guardian==1.2.4
|
|
||||||
django-filter>=0.5.4
|
|
||||||
django-oauth-plus>=2.2.1
|
|
||||||
oauth2>=1.5.211
|
|
||||||
django-oauth2-provider>=0.2.4
|
|
|
@ -1 +1,18 @@
|
||||||
|
# Minimum Django version
|
||||||
Django>=1.4.11
|
Django>=1.4.11
|
||||||
|
|
||||||
|
# Test requirements
|
||||||
|
pytest-django==2.6
|
||||||
|
pytest==2.5.2
|
||||||
|
pytest-cov==1.6
|
||||||
|
flake8==2.2.2
|
||||||
|
|
||||||
|
# Optional packages
|
||||||
|
markdown>=2.1.0
|
||||||
|
PyYAML>=3.10
|
||||||
|
defusedxml>=0.3
|
||||||
|
django-guardian==1.2.4
|
||||||
|
django-filter>=0.5.4
|
||||||
|
django-oauth-plus>=2.2.1
|
||||||
|
oauth2>=1.5.211
|
||||||
|
django-oauth2-provider>=0.2.4
|
||||||
|
|
|
@ -256,23 +256,24 @@ class FileUploadParser(BaseParser):
|
||||||
chunks = ChunkIter(stream, chunk_size)
|
chunks = ChunkIter(stream, chunk_size)
|
||||||
counters = [0] * len(upload_handlers)
|
counters = [0] * len(upload_handlers)
|
||||||
|
|
||||||
for handler in upload_handlers:
|
for index, handler in enumerate(upload_handlers):
|
||||||
try:
|
try:
|
||||||
handler.new_file(None, filename, content_type,
|
handler.new_file(None, filename, content_type,
|
||||||
content_length, encoding)
|
content_length, encoding)
|
||||||
except StopFutureHandlers:
|
except StopFutureHandlers:
|
||||||
|
upload_handlers = upload_handlers[:index + 1]
|
||||||
break
|
break
|
||||||
|
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
for i, handler in enumerate(upload_handlers):
|
for index, handler in enumerate(upload_handlers):
|
||||||
chunk_length = len(chunk)
|
chunk_length = len(chunk)
|
||||||
chunk = handler.receive_data_chunk(chunk, counters[i])
|
chunk = handler.receive_data_chunk(chunk, counters[index])
|
||||||
counters[i] += chunk_length
|
counters[index] += chunk_length
|
||||||
if chunk is None:
|
if chunk is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
for i, handler in enumerate(upload_handlers):
|
for index, handler in enumerate(upload_handlers):
|
||||||
file_obj = handler.file_complete(counters[i])
|
file_obj = handler.file_complete(counters[index])
|
||||||
if file_obj:
|
if file_obj:
|
||||||
return DataAndFiles(None, {'file': file_obj})
|
return DataAndFiles(None, {'file': file_obj})
|
||||||
raise ParseError("FileUpload parse error - "
|
raise ParseError("FileUpload parse error - "
|
||||||
|
|
|
@ -374,6 +374,10 @@ class HTMLFormRenderer(BaseRenderer):
|
||||||
'base_template': 'input.html',
|
'base_template': 'input.html',
|
||||||
'input_type': 'time'
|
'input_type': 'time'
|
||||||
},
|
},
|
||||||
|
serializers.FileField: {
|
||||||
|
'base_template': 'input.html',
|
||||||
|
'input_type': 'file'
|
||||||
|
},
|
||||||
serializers.BooleanField: {
|
serializers.BooleanField: {
|
||||||
'base_template': 'checkbox.html'
|
'base_template': 'checkbox.html'
|
||||||
},
|
},
|
||||||
|
|
|
@ -127,6 +127,14 @@ class BaseSerializer(Field):
|
||||||
(self.__class__.__module__, self.__class__.__name__)
|
(self.__class__.__module__, self.__class__.__name__)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert hasattr(self, '_errors'), (
|
||||||
|
'You must call `.is_valid()` before calling `.save()`.'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not self.errors, (
|
||||||
|
'You cannot call `.save()` on a serializer with invalid data.'
|
||||||
|
)
|
||||||
|
|
||||||
validated_data = dict(
|
validated_data = dict(
|
||||||
list(self.validated_data.items()) +
|
list(self.validated_data.items()) +
|
||||||
list(kwargs.items())
|
list(kwargs.items())
|
||||||
|
@ -600,20 +608,20 @@ class ModelSerializer(Serializer):
|
||||||
})
|
})
|
||||||
_related_class = PrimaryKeyRelatedField
|
_related_class = PrimaryKeyRelatedField
|
||||||
|
|
||||||
def create(self, validated_attrs):
|
def create(self, validated_data):
|
||||||
"""
|
"""
|
||||||
We have a bit of extra checking around this in order to provide
|
We have a bit of extra checking around this in order to provide
|
||||||
descriptive messages when something goes wrong, but this method is
|
descriptive messages when something goes wrong, but this method is
|
||||||
essentially just:
|
essentially just:
|
||||||
|
|
||||||
return ExampleModel.objects.create(**validated_attrs)
|
return ExampleModel.objects.create(**validated_data)
|
||||||
|
|
||||||
If there are many to many fields present on the instance then they
|
If there are many to many fields present on the instance then they
|
||||||
cannot be set until the model is instantiated, in which case the
|
cannot be set until the model is instantiated, in which case the
|
||||||
implementation is like so:
|
implementation is like so:
|
||||||
|
|
||||||
example_relationship = validated_attrs.pop('example_relationship')
|
example_relationship = validated_data.pop('example_relationship')
|
||||||
instance = ExampleModel.objects.create(**validated_attrs)
|
instance = ExampleModel.objects.create(**validated_data)
|
||||||
instance.example_relationship = example_relationship
|
instance.example_relationship = example_relationship
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@ -625,8 +633,8 @@ class ModelSerializer(Serializer):
|
||||||
# If we don't do this explicitly they'd likely get a confusing
|
# If we don't do this explicitly they'd likely get a confusing
|
||||||
# error at the point of calling `Model.objects.create()`.
|
# error at the point of calling `Model.objects.create()`.
|
||||||
assert not any(
|
assert not any(
|
||||||
isinstance(field, BaseSerializer) and not field.read_only
|
isinstance(field, BaseSerializer) and (key in validated_attrs)
|
||||||
for field in self.fields.values()
|
for key, field in self.fields.items()
|
||||||
), (
|
), (
|
||||||
'The `.create()` method does not suport nested writable fields '
|
'The `.create()` method does not suport nested writable fields '
|
||||||
'by default. Write an explicit `.create()` method for serializer '
|
'by default. Write an explicit `.create()` method for serializer '
|
||||||
|
@ -636,16 +644,33 @@ class ModelSerializer(Serializer):
|
||||||
|
|
||||||
ModelClass = self.Meta.model
|
ModelClass = self.Meta.model
|
||||||
|
|
||||||
# Remove many-to-many relationships from validated_attrs.
|
# Remove many-to-many relationships from validated_data.
|
||||||
# They are not valid arguments to the default `.create()` method,
|
# They are not valid arguments to the default `.create()` method,
|
||||||
# as they require that the instance has already been saved.
|
# as they require that the instance has already been saved.
|
||||||
info = model_meta.get_field_info(ModelClass)
|
info = model_meta.get_field_info(ModelClass)
|
||||||
many_to_many = {}
|
many_to_many = {}
|
||||||
for field_name, relation_info in info.relations.items():
|
for field_name, relation_info in info.relations.items():
|
||||||
if relation_info.to_many and (field_name in validated_attrs):
|
if relation_info.to_many and (field_name in validated_data):
|
||||||
many_to_many[field_name] = validated_attrs.pop(field_name)
|
many_to_many[field_name] = validated_data.pop(field_name)
|
||||||
|
|
||||||
instance = ModelClass.objects.create(**validated_attrs)
|
try:
|
||||||
|
instance = ModelClass.objects.create(**validated_data)
|
||||||
|
except TypeError as exc:
|
||||||
|
msg = (
|
||||||
|
'Got a `TypeError` when calling `%s.objects.create()`. '
|
||||||
|
'This may be because you have a writable field on the '
|
||||||
|
'serializer class that is not a valid argument to '
|
||||||
|
'`%s.objects.create()`. You may need to make the field '
|
||||||
|
'read-only, or override the %s.create() method to handle '
|
||||||
|
'this correctly.\nOriginal exception text was: %s.' %
|
||||||
|
(
|
||||||
|
ModelClass.__name__,
|
||||||
|
ModelClass.__name__,
|
||||||
|
self.__class__.__name__,
|
||||||
|
exc
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
# Save many-to-many relationships after the instance is created.
|
# Save many-to-many relationships after the instance is created.
|
||||||
if many_to_many:
|
if many_to_many:
|
||||||
|
@ -654,10 +679,10 @@ class ModelSerializer(Serializer):
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_attrs):
|
def update(self, instance, validated_data):
|
||||||
assert not any(
|
assert not any(
|
||||||
isinstance(field, BaseSerializer) and not field.read_only
|
isinstance(field, BaseSerializer) and (key in validated_attrs)
|
||||||
for field in self.fields.values()
|
for key, field in self.fields.items()
|
||||||
), (
|
), (
|
||||||
'The `.update()` method does not suport nested writable fields '
|
'The `.update()` method does not suport nested writable fields '
|
||||||
'by default. Write an explicit `.update()` method for serializer '
|
'by default. Write an explicit `.update()` method for serializer '
|
||||||
|
@ -665,7 +690,7 @@ class ModelSerializer(Serializer):
|
||||||
(self.__class__.__module__, self.__class__.__name__)
|
(self.__class__.__module__, self.__class__.__name__)
|
||||||
)
|
)
|
||||||
|
|
||||||
for attr, value in validated_attrs.items():
|
for attr, value in validated_data.items():
|
||||||
setattr(instance, attr, value)
|
setattr(instance, attr, value)
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import collections
|
||||||
from rest_framework.compat import OrderedDict
|
from rest_framework.compat import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ class NestedBoundField(BoundField):
|
||||||
return BoundField(field, value, error, prefix=self.name + '.')
|
return BoundField(field, value, error, prefix=self.name + '.')
|
||||||
|
|
||||||
|
|
||||||
class BindingDict(object):
|
class BindingDict(collections.MutableMapping):
|
||||||
"""
|
"""
|
||||||
This dict-like object is used to store fields on a serializer.
|
This dict-like object is used to store fields on a serializer.
|
||||||
|
|
||||||
|
@ -92,11 +93,8 @@ class BindingDict(object):
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
del self.fields[key]
|
del self.fields[key]
|
||||||
|
|
||||||
def items(self):
|
def __iter__(self):
|
||||||
return self.fields.items()
|
return iter(self.fields)
|
||||||
|
|
||||||
def keys(self):
|
def __len__(self):
|
||||||
return self.fields.keys()
|
return len(self.fields)
|
||||||
|
|
||||||
def values(self):
|
|
||||||
return self.fields.values()
|
|
||||||
|
|
|
@ -48,6 +48,12 @@ class ViewSetMixin(object):
|
||||||
# eg. 'List' or 'Instance'.
|
# eg. 'List' or 'Instance'.
|
||||||
cls.suffix = None
|
cls.suffix = None
|
||||||
|
|
||||||
|
# actions must not be empty
|
||||||
|
if not actions:
|
||||||
|
raise TypeError("The `actions` argument must be provided when "
|
||||||
|
"calling `.as_view()` on a ViewSet. For example "
|
||||||
|
"`.as_view({'get': 'list'})`")
|
||||||
|
|
||||||
# sanitize keyword arguments
|
# sanitize keyword arguments
|
||||||
for key in initkwargs:
|
for key in initkwargs:
|
||||||
if key in cls.http_method_names:
|
if key in cls.http_method_names:
|
||||||
|
|
111
tests/models.py
111
tests/models.py
|
@ -3,62 +3,20 @@ from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
def foobar():
|
|
||||||
return 'foobar'
|
|
||||||
|
|
||||||
|
|
||||||
class CustomField(models.CharField):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs['max_length'] = 12
|
|
||||||
super(CustomField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class RESTFrameworkModel(models.Model):
|
class RESTFrameworkModel(models.Model):
|
||||||
"""
|
"""
|
||||||
Base for test models that sets app_label, so they play nicely.
|
Base for test models that sets app_label, so they play nicely.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'tests'
|
app_label = 'tests'
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class HasPositiveIntegerAsChoice(RESTFrameworkModel):
|
|
||||||
some_choices = ((1, 'A'), (2, 'B'), (3, 'C'))
|
|
||||||
some_integer = models.PositiveIntegerField(choices=some_choices)
|
|
||||||
|
|
||||||
|
|
||||||
class Anchor(RESTFrameworkModel):
|
|
||||||
text = models.CharField(max_length=100, default='anchor')
|
|
||||||
|
|
||||||
|
|
||||||
class BasicModel(RESTFrameworkModel):
|
class BasicModel(RESTFrameworkModel):
|
||||||
text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
|
text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
|
||||||
|
|
||||||
|
|
||||||
class SlugBasedModel(RESTFrameworkModel):
|
|
||||||
text = models.CharField(max_length=100)
|
|
||||||
slug = models.SlugField(max_length=32)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultValueModel(RESTFrameworkModel):
|
|
||||||
text = models.CharField(default='foobar', max_length=100)
|
|
||||||
extra = models.CharField(blank=True, null=True, max_length=100)
|
|
||||||
|
|
||||||
|
|
||||||
class CallableDefaultValueModel(RESTFrameworkModel):
|
|
||||||
text = models.CharField(default=foobar, max_length=100)
|
|
||||||
|
|
||||||
|
|
||||||
class ManyToManyModel(RESTFrameworkModel):
|
|
||||||
rel = models.ManyToManyField(Anchor, help_text='Some help text.')
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyManyToManyModel(RESTFrameworkModel):
|
|
||||||
text = models.CharField(max_length=100, default='anchor')
|
|
||||||
rel = models.ManyToManyField(Anchor)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFilterableItem(RESTFrameworkModel):
|
class BaseFilterableItem(RESTFrameworkModel):
|
||||||
text = models.CharField(max_length=100)
|
text = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
@ -71,73 +29,6 @@ class FilterableItem(BaseFilterableItem):
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
|
|
||||||
|
|
||||||
# Model for regression test for #285
|
|
||||||
|
|
||||||
class Comment(RESTFrameworkModel):
|
|
||||||
email = models.EmailField()
|
|
||||||
content = models.CharField(max_length=200)
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
|
|
||||||
class ActionItem(RESTFrameworkModel):
|
|
||||||
title = models.CharField(max_length=200)
|
|
||||||
started = models.NullBooleanField(default=False)
|
|
||||||
done = models.BooleanField(default=False)
|
|
||||||
info = CustomField(default='---', max_length=12)
|
|
||||||
|
|
||||||
|
|
||||||
# Models for reverse relations
|
|
||||||
class Person(RESTFrameworkModel):
|
|
||||||
name = models.CharField(max_length=10)
|
|
||||||
age = models.IntegerField(null=True, blank=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def info(self):
|
|
||||||
return {
|
|
||||||
'name': self.name,
|
|
||||||
'age': self.age,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BlogPost(RESTFrameworkModel):
|
|
||||||
title = models.CharField(max_length=100)
|
|
||||||
writer = models.ForeignKey(Person, null=True, blank=True)
|
|
||||||
|
|
||||||
def get_first_comment(self):
|
|
||||||
return self.blogpostcomment_set.all()[0]
|
|
||||||
|
|
||||||
|
|
||||||
class BlogPostComment(RESTFrameworkModel):
|
|
||||||
text = models.TextField()
|
|
||||||
blog_post = models.ForeignKey(BlogPost)
|
|
||||||
|
|
||||||
|
|
||||||
class Album(RESTFrameworkModel):
|
|
||||||
title = models.CharField(max_length=100, unique=True)
|
|
||||||
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Photo(RESTFrameworkModel):
|
|
||||||
description = models.TextField()
|
|
||||||
album = models.ForeignKey(Album)
|
|
||||||
|
|
||||||
|
|
||||||
# Model for issue #324
|
|
||||||
class BlankFieldModel(RESTFrameworkModel):
|
|
||||||
title = models.CharField(max_length=100, blank=True, null=False,
|
|
||||||
default="title")
|
|
||||||
|
|
||||||
|
|
||||||
# Model for issue #380
|
|
||||||
class OptionalRelationModel(RESTFrameworkModel):
|
|
||||||
other = models.ForeignKey('OptionalRelationModel', blank=True, null=True)
|
|
||||||
|
|
||||||
|
|
||||||
# Model for RegexField
|
|
||||||
class Book(RESTFrameworkModel):
|
|
||||||
isbn = models.CharField(max_length=13)
|
|
||||||
|
|
||||||
|
|
||||||
# Models for relations tests
|
# Models for relations tests
|
||||||
# ManyToMany
|
# ManyToMany
|
||||||
class ManyToManyTarget(RESTFrameworkModel):
|
class ManyToManyTarget(RESTFrameworkModel):
|
||||||
|
|
|
@ -6,12 +6,26 @@ from django.test import TestCase
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from rest_framework import generics, renderers, serializers, status
|
from rest_framework import generics, renderers, serializers, status
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
from tests.models import BasicModel, Comment, SlugBasedModel
|
from tests.models import BasicModel, RESTFrameworkModel
|
||||||
from tests.models import ForeignKeySource, ForeignKeyTarget
|
from tests.models import ForeignKeySource, ForeignKeyTarget
|
||||||
|
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
|
||||||
|
# Models
|
||||||
|
class SlugBasedModel(RESTFrameworkModel):
|
||||||
|
text = models.CharField(max_length=100)
|
||||||
|
slug = models.SlugField(max_length=32)
|
||||||
|
|
||||||
|
|
||||||
|
# Model for regression test for #285
|
||||||
|
class Comment(RESTFrameworkModel):
|
||||||
|
email = models.EmailField()
|
||||||
|
content = models.CharField(max_length=200)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
# Serializers
|
||||||
class BasicSerializer(serializers.ModelSerializer):
|
class BasicSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BasicModel
|
model = BasicModel
|
||||||
|
@ -22,6 +36,15 @@ class ForeignKeySerializer(serializers.ModelSerializer):
|
||||||
model = ForeignKeySource
|
model = ForeignKeySource
|
||||||
|
|
||||||
|
|
||||||
|
class SlugSerializer(serializers.ModelSerializer):
|
||||||
|
slug = serializers.ReadOnlyField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SlugBasedModel
|
||||||
|
fields = ('text', 'slug')
|
||||||
|
|
||||||
|
|
||||||
|
# Views
|
||||||
class RootView(generics.ListCreateAPIView):
|
class RootView(generics.ListCreateAPIView):
|
||||||
queryset = BasicModel.objects.all()
|
queryset = BasicModel.objects.all()
|
||||||
serializer_class = BasicSerializer
|
serializer_class = BasicSerializer
|
||||||
|
@ -37,14 +60,6 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
serializer_class = ForeignKeySerializer
|
serializer_class = ForeignKeySerializer
|
||||||
|
|
||||||
|
|
||||||
class SlugSerializer(serializers.ModelSerializer):
|
|
||||||
slug = serializers.ReadOnlyField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = SlugBasedModel
|
|
||||||
fields = ('text', 'slug')
|
|
||||||
|
|
||||||
|
|
||||||
class SlugBasedInstanceView(InstanceView):
|
class SlugBasedInstanceView(InstanceView):
|
||||||
"""
|
"""
|
||||||
A model with a slug-field.
|
A model with a slug-field.
|
||||||
|
@ -54,6 +69,7 @@ class SlugBasedInstanceView(InstanceView):
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
|
|
||||||
|
|
||||||
|
# Tests
|
||||||
class TestRootView(TestCase):
|
class TestRootView(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -127,13 +143,13 @@ class TestRootView(TestCase):
|
||||||
self.assertEqual(created.text, 'foobar')
|
self.assertEqual(created.text, 'foobar')
|
||||||
|
|
||||||
|
|
||||||
EXPECTED_QUERYS_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
|
EXPECTED_QUERIES_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
|
||||||
|
|
||||||
|
|
||||||
class TestInstanceView(TestCase):
|
class TestInstanceView(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Create 3 BasicModel intances.
|
Create 3 BasicModel instances.
|
||||||
"""
|
"""
|
||||||
items = ['foo', 'bar', 'baz', 'filtered out']
|
items = ['foo', 'bar', 'baz', 'filtered out']
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -173,7 +189,7 @@ class TestInstanceView(TestCase):
|
||||||
"""
|
"""
|
||||||
data = {'text': 'foobar'}
|
data = {'text': 'foobar'}
|
||||||
request = factory.put('/1', data, format='json')
|
request = factory.put('/1', data, format='json')
|
||||||
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
|
||||||
response = self.view(request, pk='1').render()
|
response = self.view(request, pk='1').render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
|
self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
|
||||||
|
@ -187,7 +203,7 @@ class TestInstanceView(TestCase):
|
||||||
data = {'text': 'foobar'}
|
data = {'text': 'foobar'}
|
||||||
request = factory.patch('/1', data, format='json')
|
request = factory.patch('/1', data, format='json')
|
||||||
|
|
||||||
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
|
||||||
response = self.view(request, pk=1).render()
|
response = self.view(request, pk=1).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
|
@ -222,7 +238,7 @@ class TestInstanceView(TestCase):
|
||||||
"""
|
"""
|
||||||
data = {'id': 999, 'text': 'foobar'}
|
data = {'id': 999, 'text': 'foobar'}
|
||||||
request = factory.put('/1', data, format='json')
|
request = factory.put('/1', data, format='json')
|
||||||
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT):
|
with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
|
||||||
response = self.view(request, pk=1).render()
|
response = self.view(request, pk=1).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
|
@ -288,9 +304,10 @@ class TestOverriddenGetObject(TestCase):
|
||||||
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
|
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
|
||||||
queryset/model mechanism but instead overrides get_object()
|
queryset/model mechanism but instead overrides get_object()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Create 3 BasicModel intances.
|
Create 3 BasicModel instances.
|
||||||
"""
|
"""
|
||||||
items = ['foo', 'bar', 'baz']
|
items = ['foo', 'bar', 'baz']
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -363,11 +380,11 @@ class ClassB(models.Model):
|
||||||
|
|
||||||
class ClassA(models.Model):
|
class ClassA(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
childs = models.ManyToManyField(ClassB, blank=True, null=True)
|
children = models.ManyToManyField(ClassB, blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
class ClassASerializer(serializers.ModelSerializer):
|
class ClassASerializer(serializers.ModelSerializer):
|
||||||
childs = serializers.PrimaryKeyRelatedField(
|
children = serializers.PrimaryKeyRelatedField(
|
||||||
many=True, queryset=ClassB.objects.all()
|
many=True, queryset=ClassB.objects.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -380,8 +397,8 @@ class ExampleView(generics.ListCreateAPIView):
|
||||||
queryset = ClassA.objects.all()
|
queryset = ClassA.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class TestM2MBrowseableAPI(TestCase):
|
class TestM2MBrowsableAPI(TestCase):
|
||||||
def test_m2m_in_browseable_api(self):
|
def test_m2m_in_browsable_api(self):
|
||||||
"""
|
"""
|
||||||
Test for particularly ugly regression with m2m in browsable API
|
Test for particularly ugly regression with m2m in browsable API
|
||||||
"""
|
"""
|
||||||
|
@ -424,7 +441,6 @@ class DynamicSerializerView(generics.ListCreateAPIView):
|
||||||
|
|
||||||
|
|
||||||
class TestFilterBackendAppliedToViews(TestCase):
|
class TestFilterBackendAppliedToViews(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Create 3 BasicModel instances to filter on.
|
Create 3 BasicModel instances to filter on.
|
||||||
|
|
|
@ -26,6 +26,10 @@ class CustomField(models.Field):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OneFieldModel(models.Model):
|
||||||
|
char_field = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
class RegularFieldsModel(models.Model):
|
class RegularFieldsModel(models.Model):
|
||||||
"""
|
"""
|
||||||
A model class for testing regular flat fields.
|
A model class for testing regular flat fields.
|
||||||
|
@ -68,6 +72,26 @@ class FieldOptionsModel(models.Model):
|
||||||
choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
|
choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelSerializer(TestCase):
|
||||||
|
def test_create_method(self):
|
||||||
|
class TestSerializer(serializers.ModelSerializer):
|
||||||
|
non_model_field = serializers.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OneFieldModel
|
||||||
|
fields = ('char_field', 'non_model_field')
|
||||||
|
|
||||||
|
serializer = TestSerializer(data={
|
||||||
|
'char_field': 'foo',
|
||||||
|
'non_model_field': 'bar',
|
||||||
|
})
|
||||||
|
serializer.is_valid()
|
||||||
|
with self.assertRaises(TypeError) as excinfo:
|
||||||
|
serializer.save()
|
||||||
|
msginitial = 'Got a `TypeError` when calling `OneFieldModel.objects.create()`.'
|
||||||
|
assert str(excinfo.exception).startswith(msginitial)
|
||||||
|
|
||||||
|
|
||||||
class TestRegularFieldMappings(TestCase):
|
class TestRegularFieldMappings(TestCase):
|
||||||
def test_regular_fields(self):
|
def test_regular_fields(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from rest_framework.compat import StringIO
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.files.uploadhandler import MemoryFileUploadHandler
|
from django.core.files.uploadhandler import MemoryFileUploadHandler
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from rest_framework.compat import etree
|
from rest_framework.compat import etree
|
||||||
|
from rest_framework.compat import StringIO
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.parsers import FormParser, FileUploadParser
|
from rest_framework.parsers import FormParser, FileUploadParser
|
||||||
from rest_framework.parsers import XMLParser
|
from rest_framework.parsers import XMLParser
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -104,13 +105,40 @@ class TestFileUploadParser(TestCase):
|
||||||
self.parser_context = {'request': request, 'kwargs': {}}
|
self.parser_context = {'request': request, 'kwargs': {}}
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
""" Make sure the `QueryDict` works OK """
|
"""
|
||||||
|
Parse raw file upload.
|
||||||
|
"""
|
||||||
parser = FileUploadParser()
|
parser = FileUploadParser()
|
||||||
self.stream.seek(0)
|
self.stream.seek(0)
|
||||||
data_and_files = parser.parse(self.stream, None, self.parser_context)
|
data_and_files = parser.parse(self.stream, None, self.parser_context)
|
||||||
file_obj = data_and_files.files['file']
|
file_obj = data_and_files.files['file']
|
||||||
self.assertEqual(file_obj._size, 14)
|
self.assertEqual(file_obj._size, 14)
|
||||||
|
|
||||||
|
def test_parse_missing_filename(self):
|
||||||
|
"""
|
||||||
|
Parse raw file upload when filename is missing.
|
||||||
|
"""
|
||||||
|
parser = FileUploadParser()
|
||||||
|
self.stream.seek(0)
|
||||||
|
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
|
||||||
|
with self.assertRaises(ParseError):
|
||||||
|
parser.parse(self.stream, None, self.parser_context)
|
||||||
|
|
||||||
|
def test_parse_missing_filename_multiple_upload_handlers(self):
|
||||||
|
"""
|
||||||
|
Parse raw file upload with multiple handlers when filename is missing.
|
||||||
|
Regression test for #2109.
|
||||||
|
"""
|
||||||
|
parser = FileUploadParser()
|
||||||
|
self.stream.seek(0)
|
||||||
|
self.parser_context['request'].upload_handlers = (
|
||||||
|
MemoryFileUploadHandler(),
|
||||||
|
MemoryFileUploadHandler()
|
||||||
|
)
|
||||||
|
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
|
||||||
|
with self.assertRaises(ParseError):
|
||||||
|
parser.parse(self.stream, None, self.parser_context)
|
||||||
|
|
||||||
def test_get_filename(self):
|
def test_get_filename(self):
|
||||||
parser = FileUploadParser()
|
parser = FileUploadParser()
|
||||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
|
|
35
tests/test_viewsets.py
Normal file
35
tests/test_viewsets.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.test import APIRequestFactory
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
|
|
||||||
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
|
||||||
|
class BasicViewSet(GenericViewSet):
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
return Response({'ACTION': 'LIST'})
|
||||||
|
|
||||||
|
|
||||||
|
class InitializeViewSetsTestCase(TestCase):
|
||||||
|
def test_initialize_view_set_with_actions(self):
|
||||||
|
request = factory.get('/', '', content_type='application/json')
|
||||||
|
my_view = BasicViewSet.as_view(actions={
|
||||||
|
'get': 'list',
|
||||||
|
})
|
||||||
|
|
||||||
|
response = my_view(request)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, {'ACTION': 'LIST'})
|
||||||
|
|
||||||
|
def test_initialize_view_set_with_empty_actions(self):
|
||||||
|
try:
|
||||||
|
BasicViewSet.as_view()
|
||||||
|
except TypeError as e:
|
||||||
|
self.assertEqual(str(e), "The `actions` argument must be provided "
|
||||||
|
"when calling `.as_view()` on a ViewSet. "
|
||||||
|
"For example `.as_view({'get': 'list'})`")
|
||||||
|
else:
|
||||||
|
self.fail("actions must not be empty.")
|
Loading…
Reference in New Issue
Block a user