Merge pull request #3 from tomchristie/master

Merge upstream
This commit is contained in:
BrickXu 2014-12-05 13:30:56 +08:00
commit 4042180392
24 changed files with 305 additions and 215 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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'))

View File

@ -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.

View File

@ -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

View File

@ -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`.

View File

@ -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`.

View File

@ -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

View File

@ -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.

View File

@ -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'))

View File

@ -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

View File

@ -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

View File

@ -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 - "

View File

@ -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'
}, },

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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):

View File

@ -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.

View File

@ -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):
""" """

View File

@ -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
View 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.")