aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--README.md2
-rw-r--r--docs/api-guide/fields.md6
-rw-r--r--docs/api-guide/serializers.md37
-rw-r--r--docs/api-guide/viewsets.md3
-rw-r--r--docs/index.md2
-rw-r--r--docs/topics/3.0-announcement.md12
-rw-r--r--docs/topics/release-notes.md10
-rw-r--r--docs/tutorial/1-serialization.md94
-rw-r--r--docs/tutorial/2-requests-and-responses.md57
-rw-r--r--docs/tutorial/4-authentication-and-permissions.md24
-rw-r--r--docs/tutorial/5-relationships-and-hyperlinked-apis.md2
-rw-r--r--docs/tutorial/quickstart.md32
-rw-r--r--rest_framework/authentication.py2
-rw-r--r--rest_framework/authtoken/models.py5
-rw-r--r--rest_framework/compat.py107
-rw-r--r--rest_framework/exceptions.py2
-rw-r--r--rest_framework/fields.py47
-rw-r--r--rest_framework/metadata.py7
-rw-r--r--rest_framework/parsers.py4
-rw-r--r--rest_framework/permissions.py2
-rw-r--r--rest_framework/relations.py21
-rw-r--r--rest_framework/renderers.py24
-rw-r--r--rest_framework/request.py2
-rw-r--r--rest_framework/serializers.py251
-rw-r--r--rest_framework/settings.py2
-rw-r--r--rest_framework/static/rest_framework/js/default.js3
-rw-r--r--rest_framework/templates/rest_framework/horizontal/select.html2
-rw-r--r--rest_framework/templates/rest_framework/inline/select.html2
-rw-r--r--rest_framework/templates/rest_framework/login_base.html23
-rw-r--r--rest_framework/templates/rest_framework/raw_data_form.html8
-rw-r--r--rest_framework/templates/rest_framework/vertical/select.html2
-rw-r--r--rest_framework/templatetags/rest_framework.py4
-rw-r--r--rest_framework/utils/encoders.py3
-rw-r--r--rest_framework/utils/field_mapping.py12
-rw-r--r--rest_framework/utils/mediatypes.py5
-rw-r--r--rest_framework/utils/representation.py2
-rw-r--r--rest_framework/validators.py2
-rw-r--r--rest_framework/views.py3
-rw-r--r--rest_framework/viewsets.py6
-rwxr-xr-xruntests.py5
-rw-r--r--tests/test_authentication.py2
-rw-r--r--tests/test_description.py7
-rw-r--r--tests/test_fields.py15
-rw-r--r--tests/test_model_serializer.py50
-rw-r--r--tests/test_multitable_inheritance.py2
-rw-r--r--tests/test_parsers.py2
-rw-r--r--tests/test_relations_generic.py2
-rw-r--r--tests/test_renderers.py13
-rw-r--r--tests/test_request.py2
-rw-r--r--tests/test_serializer_lists.py16
-rw-r--r--tests/test_validators.py2
-rw-r--r--tox.ini6
53 files changed, 620 insertions, 342 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 96e55161..b963a499 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -33,7 +33,7 @@ Some tips on good issue reporting:
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
-* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
+* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
## Triaging issues
@@ -82,7 +82,7 @@ GitHub's documentation for working on pull requests is [available here][pull-req
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
-Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect.
+Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are running as you'd expect.
![Travis status][travis-status]
diff --git a/README.md b/README.md
index b8957545..df0a4086 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Django REST framework
+# [Django REST framework][docs]
[![build-status-image]][travis]
[![pypi-version]][pypi]
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index aa5cc84e..e4ef1d4a 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -310,6 +310,9 @@ Used by `ModelSerializer` to automatically generate fields if the corresponding
**Signature:** `ChoiceField(choices)`
- `choices` - A list of valid values, or a list of `(key, display_name)` tuples.
+- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
+
+Both the `allow_blank` and `allow_null` are valid options on `ChoiceField`, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices.
## MultipleChoiceField
@@ -318,6 +321,9 @@ A field that can accept a set of zero, one or many values, chosen from a limited
**Signature:** `MultipleChoiceField(choices)`
- `choices` - A list of valid values, or a list of `(key, display_name)` tuples.
+- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
+
+As with `ChoiceField`, both the `allow_blank` and `allow_null` options are valid, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices.
---
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 1779c863..5fe6b4c2 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -104,7 +104,7 @@ If your object instances correspond to Django models you'll also want to ensure
instance.created = validated_data.get('created', instance.created)
instance.save()
return instance
-
+
Now when deserializing data, we can call `.save()` to return an object instance, based on the validated data.
comment = serializer.save()
@@ -113,7 +113,7 @@ Calling `.save()` will either create a new instance, or update an existing insta
# .save() will create a new instance.
serializer = CommentSerializer(data=data)
-
+
# .save() will update the existing `comment` instance.
serializer = CommentSerializer(comment, data=data)
@@ -140,7 +140,7 @@ For example:
class ContactForm(serializers.Serializer):
email = serializers.EmailField()
message = serializers.CharField()
-
+
def save(self):
email = self.validated_data['email']
message = self.validated_data['message']
@@ -230,7 +230,7 @@ Serializer classes can also include reusable validators that are applied to the
name = serializers.CharField()
room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
date = serializers.DateField()
-
+
class Meta:
# Each room only has one event per day.
validators = UniqueTogetherValidator(
@@ -326,9 +326,9 @@ Here's an example for an `update()` method on our previous `UserSerializer` clas
# would need to be handled.
profile = instance.profile
- user.username = validated_data.get('username', instance.username)
- user.email = validated_data.get('email', instance.email)
- user.save()
+ instance.username = validated_data.get('username', instance.username)
+ instance.email = validated_data.get('email', instance.email)
+ instance.save()
profile.is_premium_member = profile_data.get(
'is_premium_member',
@@ -340,7 +340,7 @@ Here's an example for an `update()` method on our previous `UserSerializer` clas
)
profile.save()
- return user
+ return instance
Because the behavior of nested creates and updates can be ambiguous, and may require complex dependancies between related models, REST framework 3 requires you to always write these methods explicitly. The default `ModelSerializer` `.create()` and `.update()` methods do not include support for writable nested representations.
@@ -448,7 +448,7 @@ To do so, open the Django shell, using `python manage.py shell`, then import the
id = IntegerField(label='ID', read_only=True)
name = CharField(allow_blank=True, max_length=100, required=False)
owner = PrimaryKeyRelatedField(queryset=User.objects.all())
-
+
## Specifying which fields should be included
If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`.
@@ -505,6 +505,21 @@ This option should be a list or tuple of field names, and is declared as follows
Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option.
+---
+
+**Note**: There is a special-case where a read-only field is part of a `unique_together` constraint at the model level. In this case the field is required by the serializer class in order to validate the constraint, but should also not be editable by the user.
+
+The right way to deal with this is to specify the field explicitly on the serializer, providing both the `read_only=True` and `default=…` keyword arguments.
+
+One example of this is a read-only relation to the currently authenticated `User` which is `unique_together` with another identifier. In this case you would declare the user field like so:
+
+ user = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault())
+
+Please review the [Validators Documentation](/api-guide/validators/) for details on the [UniqueTogetherValidator](/api-guide/validators/#uniquetogethervalidator) and [CurrentUserDefault](/api-guide/validators/#currentuserdefault) classes.
+
+---
+
+
## Specifying additional keyword arguments for fields.
There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. Similarly to `read_only_fields` this means you do not need to explicitly declare the field on the serializer.
@@ -516,7 +531,7 @@ This option is a dictionary, mapping field names to a dictionary of keyword argu
model = User
fields = ('email', 'username', 'password')
extra_kwargs = {'password': {'write_only': True}}
-
+
def create(self, validated_data):
user = User(
email=validated_data['email'],
@@ -656,7 +671,7 @@ To support multiple updates you'll need to do so explicitly. When writing your m
* How do you determine which instance should be updated for each item in the list of data?
* How should insertions be handled? Are they invalid, or do they create new objects?
* How should removals be handled? Do they imply object deletion, or removing a relationship? Should they be silently ignored, or are they invalid?
-* How should ordering be handled? Does changing the position of two items imply any state change or is it ignored?
+* How should ordering be handled? Does changing the position of two items imply any state change or is it ignored?
Here's an example of how you might choose to implement multiple updates:
diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md
index 28186c64..3e37cef8 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -201,6 +201,8 @@ Note that you can use any of the standard attributes or method overrides provide
def get_queryset(self):
return self.request.user.accounts.all()
+Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you you will have to specify the `base_name` kwarg as part of your [router registration][routers].
+
Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes.
## ReadOnlyModelViewSet
@@ -243,3 +245,4 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple viewsets across your API.
[cite]: http://guides.rubyonrails.org/routing.html
+[routers]: routers.md
diff --git a/docs/index.md b/docs/index.md
index e0ba2332..52e42fc9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -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>
<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>
---
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 8791ad08..8fa86b9a 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -28,10 +28,18 @@ Notable features of this new release include:
Significant new functionality continues to be planned for the 3.1 and 3.2 releases. These releases will correspond to the two [Kickstarter stretch goals](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) - "Feature improvements" and "Admin interface". Further 3.x releases will present simple upgrades, without the same level of fundamental API changes necessary for the 3.0 release.
-Below is an in-depth guide to the API changes and migration notes for 3.0.
+---
+
+#### REST framework: Under the hood.
+
+This talk from the [Django: Under the Hood](http://www.djangounderthehood.com/) event in Amsterdam, Nov 2014, gives some good background context on the design decisions behind 3.0.
+
+<iframe width="560" height="315" src="//www.youtube.com/embed/3cSsbe-tA0E" frameborder="0" allowfullscreen></iframe>
---
+*Below is an in-depth guide to the API changes and migration notes for 3.0.*
+
## Request objects
#### The `.data` and `.query_params` properties.
@@ -931,6 +939,7 @@ The default JSON renderer will return float objects for un-coerced `Decimal` ins
* The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1.
* Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
+* Some of the default validation error messages were rewritten and might no longer be pre-translated. You can still [create language files with Django][django-localization] if you wish to localize them.
---
@@ -952,3 +961,4 @@ You can follow development on the GitHub site, where we use [milestones to indic
[kickstarter]: http://kickstarter.com/projects/tomchristie/django-rest-framework-3
[sponsors]: http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors
[mixins.py]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py
+[django-localization]: https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 19dfbb98..550fdf75 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -38,6 +38,16 @@ You can determine your currently installed version using `pip freeze`:
---
+## 3.0.x series
+
+### 3.0.0
+
+**Date**: 1st December 2014
+
+For full details see the [3.0 release announcement](3.0-announcement.md).
+
+---
+
## 2.4.x series
### 2.4.4
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index a3c19858..dea43cc0 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -16,7 +16,6 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
- :::bash
virtualenv env
source env/bin/activate
@@ -75,12 +74,8 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
- language = models.CharField(choices=LANGUAGE_CHOICES,
- default='python',
- max_length=100)
- style = models.CharField(choices=STYLE_CHOICES,
- default='friendly',
- max_length=100)
+ language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
+ style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
class Meta:
ordering = ('created',)
@@ -101,30 +96,27 @@ The first thing we need to get started on our Web API is to provide a way of ser
class SnippetSerializer(serializers.Serializer):
pk = serializers.IntegerField(read_only=True)
- title = serializers.CharField(required=False,
- max_length=100)
+ title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'type': 'textarea'})
linenos = serializers.BooleanField(required=False)
- language = serializers.ChoiceField(choices=LANGUAGE_CHOICES,
- default='python')
- style = serializers.ChoiceField(choices=STYLE_CHOICES,
- default='friendly')
+ language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
+ style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
- def create(self, validated_attrs):
+ def create(self, 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.
"""
- instance.title = validated_attrs.get('title', instance.title)
- instance.code = validated_attrs.get('code', instance.code)
- instance.linenos = validated_attrs.get('linenos', instance.linenos)
- instance.language = validated_attrs.get('language', instance.language)
- instance.style = validated_attrs.get('style', instance.style)
+ instance.title = validated_data.get('title', instance.title)
+ instance.code = validated_data.get('code', instance.code)
+ instance.linenos = validated_data.get('linenos', instance.linenos)
+ instance.language = validated_data.get('language', instance.language)
+ instance.style = validated_data.get('style', instance.style)
instance.save()
return instance
@@ -181,7 +173,9 @@ Deserialization is similar. First we parse a stream into Python native datatype
serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
- serializer.object
+ serializer.validated_data
+ # OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
+ serializer.save()
# <Snippet: Snippet object>
Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer.
@@ -210,7 +204,7 @@ One nice property that serializers have is that you can inspect all the fields i
>>> from snippets.serializers import SnippetSerializer
>>> serializer = SnippetSerializer()
- >>> print repr(serializer) # In python 3 use `print(repr(serializer))`
+ >>> print(repr(serializer))
SnippetSerializer():
id = IntegerField(label='ID', read_only=True)
title = CharField(allow_blank=True, max_length=100, required=False)
@@ -301,7 +295,7 @@ We'll also need a view which corresponds to an individual snippet, and can be us
Finally we need to wire these views up. Create the `snippets/urls.py` file:
- from django.conf.urls import patterns, url
+ from django.conf.urls import url
from snippets import views
urlpatterns = [
@@ -332,17 +326,51 @@ Quit out of the shell...
In another terminal window, we can test the server.
-We can get a list of all of the snippets.
-
- curl http://127.0.0.1:8000/snippets/
-
- [{"id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly"}, {"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}]
+We can test our API using using [curl][curl] or [httpie][httpie]. Httpie is a user friendly http client that's written in Python. Let's install that.
+
+You can install httpie using pip:
+
+ pip install httpie
+
+Finally, we can get a list of all of the snippets:
+
+ http http://127.0.0.1:8000/snippets/
+
+ HTTP/1.1 200 OK
+ ...
+ [
+ {
+ "id": 1,
+ "title": "",
+ "code": "foo = \"bar\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ },
+ {
+ "id": 2,
+ "title": "",
+ "code": "print \"hello, world\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
+ ]
-Or we can get a particular snippet by referencing its id.
+Or we can get a particular snippet by referencing its id:
- curl http://127.0.0.1:8000/snippets/2/
+ http http://127.0.0.1:8000/snippets/2/
- {"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}
+ HTTP/1.1 200 OK
+ ...
+ {
+ "id": 2,
+ "title": "",
+ "code": "print \"hello, world\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
Similarly, you can have the same json displayed by visiting these URLs in a web browser.
@@ -359,3 +387,5 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
[sandbox]: http://restframework.herokuapp.com/
[virtualenv]: http://www.virtualenv.org/en/latest/index.html
[tut-2]: 2-requests-and-responses.md
+[httpie]: https://github.com/jakubroztocil/httpie#installation
+[curl]: http://curl.haxx.se
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
index f377c712..49e96d03 100644
--- a/docs/tutorial/2-requests-and-responses.md
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -127,31 +127,64 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][
We can get a list of all of the snippets, as before.
- curl http://127.0.0.1:8000/snippets/
-
- [{"id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly"}, {"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}]
+ http http://127.0.0.1:8000/snippets/
+
+ HTTP/1.1 200 OK
+ ...
+ [
+ {
+ "id": 1,
+ "title": "",
+ "code": "foo = \"bar\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ },
+ {
+ "id": 2,
+ "title": "",
+ "code": "print \"hello, world\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
+ ]
We can control the format of the response that we get back, either by using the `Accept` header:
- curl http://127.0.0.1:8000/snippets/ -H 'Accept: application/json' # Request JSON
- curl http://127.0.0.1:8000/snippets/ -H 'Accept: text/html' # Request HTML
+ http http://127.0.0.1:8000/snippets/ Accept:application/json # Request JSON
+ http http://127.0.0.1:8000/snippets/ Accept:text/html # Request HTML
Or by appending a format suffix:
- curl http://127.0.0.1:8000/snippets/.json # JSON suffix
- curl http://127.0.0.1:8000/snippets/.api # Browsable API suffix
+ http http://127.0.0.1:8000/snippets/.json # JSON suffix
+ http http://127.0.0.1:8000/snippets/.api # Browsable API suffix
Similarly, we can control the format of the request that we send, using the `Content-Type` header.
# POST using form data
- curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123"
+ http --form POST http://127.0.0.1:8000/snippets/ code="print 123"
- {"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"}
+ {
+ "id": 3,
+ "title": "",
+ "code": "print 123",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
# POST using JSON
- curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json"
-
- {"id": 4, "title": "", "code": "print 456", "linenos": true, "language": "python", "style": "friendly"}
+ http --json POST http://127.0.0.1:8000/snippets/ code="print 456"
+
+ {
+ "id": 4,
+ "title": "",
+ "code": "print 456",
+ "linenos": true,
+ "language": "python",
+ "style": "friendly"
+ }
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver].
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index 4e4edeea..a6d27bf7 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -43,7 +43,7 @@ And now we can add a `.save()` method to our model class:
When that's all done we'll need to update our database tables.
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
- rm tmp.db
+ rm -f tmp.db db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate
@@ -59,7 +59,7 @@ Now that we've got some users to work with, we'd better add representations of t
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
- snippets = serializers.PrimaryKeyRelatedField(many=True)
+ snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
@@ -198,15 +198,25 @@ If we're interacting with the API programmatically we need to explicitly provide
If we try to create a snippet without authenticating, we'll get an error:
- curl -i -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123"
+ http POST http://127.0.0.1:8000/snippets/ code="print 123"
- {"detail": "Authentication credentials were not provided."}
+ {
+ "detail": "Authentication credentials were not provided."
+ }
We can make a successful request by including the username and password of one of the users we created earlier.
- curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 789" -u tom:password
-
- {"id": 5, "owner": "tom", "title": "foo", "code": "print 789", "linenos": false, "language": "python", "style": "friendly"}
+ http POST -a tom:password http://127.0.0.1:8000/snippets/ code="print 789"
+
+ {
+ "id": 5,
+ "owner": "tom",
+ "title": "foo",
+ "code": "print 789",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
## Summary
diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
index 50552616..58422929 100644
--- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md
+++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
@@ -44,7 +44,7 @@ Instead of using a concrete generic view, we'll use the base class for represent
As usual we need to add the new views that we've created in to our URLconf.
We'll add a url pattern for our new API root in `snippets/urls.py`:
- url(r'^$', 'api_root'),
+ url(r'^$', views.api_root),
And then add a url pattern for the snippet highlights:
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 3e1ce0a9..c3f95994 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -19,10 +19,10 @@ Create a new Django project named `tutorial`, then start a new app called `quick
pip install djangorestframework
# Set up a new project with a single application
- django-admin.py startproject tutorial
+ django-admin.py startproject tutorial .
cd tutorial
django-admin.py startapp quickstart
- cd ..
+ cd ..
Now sync your database for the first time:
@@ -159,6 +159,33 @@ We can now access our API, both from the command-line, using tools like `curl`..
]
}
+Or using the [httpie][httpie], command line tool...
+
+ bash: http -a username:password http://127.0.0.1:8000/users/
+
+ HTTP/1.1 200 OK
+ ...
+ {
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "email": "admin@example.com",
+ "groups": [],
+ "url": "http://localhost:8000/users/1/",
+ "username": "paul"
+ },
+ {
+ "email": "tom@example.com",
+ "groups": [ ],
+ "url": "http://127.0.0.1:8000/users/2/",
+ "username": "tom"
+ }
+ ]
+ }
+
+
Or directly through the browser...
![Quick start image][image]
@@ -173,3 +200,4 @@ If you want to get a more in depth understanding of how REST framework fits toge
[image]: ../img/quickstart.png
[tutorial]: 1-serialization.md
[guide]: ../#api-guide
+[httpie]: https://github.com/jakubroztocil/httpie#installation
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index 36d74dd9..4832ad33 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -267,7 +267,7 @@ class OAuthAuthentication(BaseAuthentication):
def authenticate_header(self, request):
"""
If permission is denied, return a '401 Unauthorized' response,
- with an appropraite 'WWW-Authenticate' header.
+ with an appropriate 'WWW-Authenticate' header.
"""
return 'OAuth realm="%s"' % self.www_authenticate_realm
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index db21d44c..a1a9315f 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -1,7 +1,9 @@
import binascii
import os
+
from django.conf import settings
from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
@@ -11,6 +13,7 @@ from django.db import models
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
+@python_2_unicode_compatible
class Token(models.Model):
"""
The default authorization token model.
@@ -35,5 +38,5 @@ class Token(models.Model):
def generate_key(self):
return binascii.hexlify(os.urandom(20)).decode()
- def __unicode__(self):
+ def __str__(self):
return self.key
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 5bd85e74..71520b92 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -6,24 +6,14 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa
from __future__ import unicode_literals
+import inspect
+
from django.core.exceptions import ImproperlyConfigured
+from django.utils.encoding import force_text
+from django.utils.six.moves.urllib import parse as urlparse
from django.conf import settings
from django.utils import six
import django
-import inspect
-
-
-# Handle django.utils.encoding rename in 1.5 onwards.
-# smart_unicode -> smart_text
-# force_unicode -> force_text
-try:
- from django.utils.encoding import smart_text
-except ImportError:
- from django.utils.encoding import smart_unicode as smart_text
-try:
- from django.utils.encoding import force_text
-except ImportError:
- from django.utils.encoding import force_unicode as force_text
# OrderedDict only available in Python 2.7.
@@ -32,7 +22,7 @@ except ImportError:
# For Django <= 1.6 and Python 2.6 fall back to OrderedDict.
try:
from collections import OrderedDict
-except:
+except ImportError:
from django.utils.datastructures import SortedDict as OrderedDict
@@ -49,7 +39,6 @@ try:
except ImportError:
django_filters = None
-
if django.VERSION >= (1, 6):
def clean_manytomany_helptext(text):
return text
@@ -72,30 +61,6 @@ if 'guardian' in settings.INSTALLED_APPS:
pass
-# cStringIO only if it's available, otherwise StringIO
-try:
- import cStringIO.StringIO as StringIO
-except ImportError:
- StringIO = six.StringIO
-
-BytesIO = six.BytesIO
-
-
-# urlparse compat import (Required because it changed in python 3.x)
-try:
- from urllib import parse as urlparse
-except ImportError:
- import urlparse
-
-# UserDict moves in Python 3
-try:
- from UserDict import UserDict
- from UserDict import DictMixin
-except ImportError:
- from collections import UserDict
- from collections import MutableMapping as DictMixin
-
-
def get_model_name(model_cls):
try:
return model_cls._meta.model_name
@@ -104,14 +69,6 @@ def get_model_name(model_cls):
return model_cls._meta.module_name
-def get_concrete_model(model_cls):
- try:
- return model_cls._meta.concrete_model
- except AttributeError:
- # 1.3 does not include concrete model
- return model_cls
-
-
# View._allowed_methods only present from 1.5 onwards
if django.VERSION >= (1, 5):
from django.views.generic import View
@@ -123,7 +80,6 @@ else:
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
-
# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+
if django.VERSION >= (1, 8):
from django.core.validators import MinValueValidator, MaxValueValidator
@@ -187,6 +143,7 @@ if 'patch' not in View.http_method_names:
# RequestFactory only provides `generic` from 1.5 onwards
from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import FakePayload
+
try:
# In 1.5 the test client uses force_bytes
from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes
@@ -194,26 +151,22 @@ except ImportError:
# In 1.4 the test client just uses smart_str
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
+
class RequestFactory(DjangoRequestFactory):
def generic(self, method, path,
data='', content_type='application/octet-stream', **extra):
parsed = urlparse.urlparse(path)
data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
r = {
- 'PATH_INFO': self._get_path(parsed),
- 'QUERY_STRING': force_text(parsed[4]),
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': force_text(parsed[4]),
'REQUEST_METHOD': six.text_type(method),
}
if data:
r.update({
'CONTENT_LENGTH': len(data),
- 'CONTENT_TYPE': six.text_type(content_type),
- 'wsgi.input': FakePayload(data),
- })
- elif django.VERSION <= (1, 4):
- # For 1.3 we need an empty WSGI payload
- r.update({
- 'wsgi.input': FakePayload('')
+ 'CONTENT_TYPE': six.text_type(content_type),
+ 'wsgi.input': FakePayload(data),
})
r.update(extra)
return self.request(**r)
@@ -287,10 +240,12 @@ try:
import provider as oauth2_provider
from provider import scope as oauth2_provider_scope
from provider import constants as oauth2_constants
+
if oauth2_provider.__version__ in ('0.2.3', '0.2.4'):
# 0.2.3 and 0.2.4 are supported version that do not support
# timezone aware datetimes
import datetime
+
provider_now = datetime.datetime.now
else:
# Any other supported version does use timezone aware datetimes
@@ -301,7 +256,7 @@ except ImportError:
oauth2_constants = None
provider_now = None
-# `seperators` argument to `json.dumps()` differs between 2.x and 3.x
+# `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: http://bugs.python.org/issue22767
if six.PY3:
SHORT_SEPARATORS = (',', ':')
@@ -309,37 +264,3 @@ if six.PY3:
else:
SHORT_SEPARATORS = (b',', b':')
LONG_SEPARATORS = (b', ', b': ')
-
-
-# Handle lazy strings across Py2/Py3
-from django.utils.functional import Promise
-
-if six.PY3:
- def is_non_str_iterable(obj):
- if (isinstance(obj, str) or
- (isinstance(obj, Promise) and obj._delegate_text)):
- return False
- return hasattr(obj, '__iter__')
-else:
- def is_non_str_iterable(obj):
- return hasattr(obj, '__iter__')
-
-
-try:
- from django.utils.encoding import python_2_unicode_compatible
-except ImportError:
- def python_2_unicode_compatible(klass):
- """
- A decorator that defines __unicode__ and __str__ methods under Python 2.
- Under Python 3 it does nothing.
-
- To support Python 2 and 3 with a single code base, define a __str__ method
- returning text and apply this decorator to the class.
- """
- if '__str__' not in klass.__dict__:
- raise ValueError("@python_2_unicode_compatible cannot be applied "
- "to %s because it doesn't define __str__()." %
- klass.__name__)
- klass.__unicode__ = klass.__str__
- klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
- return klass
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 906de3b0..be41d08d 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -5,11 +5,11 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from __future__ import unicode_literals
+from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from rest_framework import status
-from rest_framework.compat import force_text
import math
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index ca9c479f..99498da7 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -5,11 +5,11 @@ from django.core.validators import RegexValidator
from django.forms import ImageField as DjangoImageField
from django.utils import six, timezone
from django.utils.dateparse import parse_date, parse_datetime, parse_time
-from django.utils.encoding import is_protected_type
+from django.utils.encoding import is_protected_type, smart_text
from django.utils.translation import ugettext_lazy as _
from rest_framework import ISO_8601
from rest_framework.compat import (
- smart_text, EmailValidator, MinValueValidator, MaxValueValidator,
+ EmailValidator, MinValueValidator, MaxValueValidator,
MinLengthValidator, MaxLengthValidator, URLValidator, OrderedDict
)
from rest_framework.exceptions import ValidationError
@@ -294,31 +294,47 @@ class Field(object):
return self.default()
return self.default
- def run_validation(self, data=empty):
+ def validate_empty_values(self, data):
"""
- Validate a simple representation and return the internal value.
-
- The provided data may be `empty` if no representation was included
- in the input.
-
- May raise `SkipField` if the field should not be included in the
- validated data.
+ Validate empty values, and either:
+
+ * Raise `ValidationError`, indicating invalid data.
+ * Raise `SkipField`, indicating that the field should be ignored.
+ * Return (True, data), indicating an empty value that should be
+ returned without any furhter validation being applied.
+ * Return (False, data), indicating a non-empty value, that should
+ have validation applied as normal.
"""
if self.read_only:
- return self.get_default()
+ return (True, self.get_default())
if data is empty:
if getattr(self.root, 'partial', False):
raise SkipField()
if self.required:
self.fail('required')
- return self.get_default()
+ return (True, self.get_default())
if data is None:
if not self.allow_null:
self.fail('null')
- return None
+ return (True, None)
+
+ return (False, data)
+
+ def run_validation(self, data=empty):
+ """
+ Validate a simple representation and return the internal value.
+
+ The provided data may be `empty` if no representation was included
+ in the input.
+ May raise `SkipField` if the field should not be included in the
+ validated data.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
value = self.to_internal_value(data)
self.run_validators(value)
return value
@@ -942,9 +958,14 @@ class ChoiceField(Field):
(six.text_type(key), key) for key in self.choices.keys()
])
+ self.allow_blank = kwargs.pop('allow_blank', False)
+
super(ChoiceField, self).__init__(**kwargs)
def to_internal_value(self, data):
+ if data == '' and self.allow_blank:
+ return ''
+
try:
return self.choice_strings_to_values[six.text_type(data)]
except KeyError:
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
index de829d00..3b058fab 100644
--- a/rest_framework/metadata.py
+++ b/rest_framework/metadata.py
@@ -1,17 +1,18 @@
"""
-The metadata API is used to allow cusomization of how `OPTIONS` requests
+The metadata API is used to allow customization of how `OPTIONS` requests
are handled. We currently provide a single default implementation that returns
some fairly ad-hoc information about the view.
-Future implementations might use JSON schema or other definations in order
+Future implementations might use JSON schema or other definitions in order
to return this information in a more standardized way.
"""
from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
from django.http import Http404
+from django.utils.encoding import force_text
from rest_framework import exceptions, serializers
-from rest_framework.compat import force_text, OrderedDict
+from rest_framework.compat import OrderedDict
from rest_framework.request import clone_request
from rest_framework.utils.field_mapping import ClassLookupDict
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index d229abec..3e3395c0 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -12,7 +12,9 @@ from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from django.utils import six
-from rest_framework.compat import etree, yaml, force_text, urlparse
+from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.encoding import force_text
+from rest_framework.compat import etree, yaml
from rest_framework.exceptions import ParseError
from rest_framework import renderers
import json
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index 29f60d6d..3f6f5961 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -184,7 +184,7 @@ class DjangoObjectPermissions(DjangoModelPermissions):
if not user.has_perms(perms, obj):
# If the user does not have permissions we need to determine if
# they have read permissions to see 403, or not, and simply see
- # a 404 reponse.
+ # a 404 response.
if request.method in ('GET', 'OPTIONS', 'HEAD'):
# Read permissions already checked and failed, no need
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index d1ea497a..75d68204 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -1,4 +1,4 @@
-from rest_framework.compat import smart_text, urlparse
+from django.utils.encoding import smart_text
from rest_framework.fields import get_attribute, empty, Field
from rest_framework.reverse import reverse
from rest_framework.utils import html
@@ -6,6 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
from django.db.models.query import QuerySet
from django.utils import six
+from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _
@@ -114,9 +115,9 @@ class StringRelatedField(RelatedField):
class PrimaryKeyRelatedField(RelatedField):
default_error_messages = {
- 'required': 'This field is required.',
- 'does_not_exist': "Invalid pk '{pk_value}' - object does not exist.",
- 'incorrect_type': 'Incorrect type. Expected pk value, received {data_type}.',
+ 'required': _('This field is required.'),
+ 'does_not_exist': _("Invalid pk '{pk_value}' - object does not exist."),
+ 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
}
def to_internal_value(self, data):
@@ -141,7 +142,7 @@ class PrimaryKeyRelatedField(RelatedField):
def get_iterable(self, instance, source_attrs):
# For consistency with `get_attribute` we're using `serializable_value()`
# here. Typically there won't be any difference, but some custom field
- # types might return a non-primative value for the pk otherwise.
+ # types might return a non-primitive value for the pk otherwise.
#
# We could try to get smart with `values_list('pk', flat=True)`, which
# would be better in some case, but would actually end up with *more*
@@ -161,11 +162,11 @@ class HyperlinkedRelatedField(RelatedField):
lookup_field = 'pk'
default_error_messages = {
- 'required': 'This field is required.',
- 'no_match': 'Invalid hyperlink - No URL match',
- 'incorrect_match': 'Invalid hyperlink - Incorrect URL match.',
- 'does_not_exist': 'Invalid hyperlink - Object does not exist.',
- 'incorrect_type': 'Incorrect type. Expected URL string, received {data_type}.',
+ 'required': _('This field is required.'),
+ 'no_match': _('Invalid hyperlink - No URL match'),
+ 'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),
+ 'does_not_exist': _('Invalid hyperlink - Object does not exist.'),
+ 'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'),
}
def __init__(self, view_name=None, **kwargs):
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index e87d16d0..cfcf1f5d 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -12,15 +12,16 @@ import json
import django
from django import forms
from django.core.exceptions import ImproperlyConfigured
+from django.core.paginator import Page
from django.http.multipartparser import parse_header
from django.template import Context, RequestContext, loader, Template
from django.test.client import encode_multipart
from django.utils import six
+from django.utils.encoding import smart_text
from django.utils.xmlutils import SimplerXMLGenerator
+from django.utils.six.moves import StringIO
from rest_framework import exceptions, serializers, status, VERSION
-from rest_framework.compat import (
- SHORT_SEPARATORS, LONG_SEPARATORS, StringIO, smart_text, yaml
-)
+from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS, yaml
from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
from rest_framework.request import is_form_media_type, override_method
@@ -102,6 +103,11 @@ class JSONRenderer(BaseRenderer):
# and may (or may not) be unicode.
# On python 3.x json.dumps() returns unicode strings.
if isinstance(ret, six.text_type):
+ # We always fully escape \u2028 and \u2029 to ensure we output JSON
+ # that is a strict javascript subset. If bytes were returned
+ # by json.dumps() then we don't have these characters in any case.
+ # See: http://timelessrepo.com/json-isnt-a-javascript-subset
+ ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
return bytes(ret.encode('utf-8'))
return ret
@@ -282,7 +288,9 @@ class TemplateHTMLRenderer(BaseRenderer):
return view.get_template_names()
elif hasattr(view, 'template_name'):
return [view.template_name]
- raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response')
+ raise ImproperlyConfigured(
+ 'Returned a template response with no `template_name` attribute set on either the view or response'
+ )
def get_exception_template(self, response):
template_names = [name % {'status_code': response.status_code}
@@ -374,6 +382,10 @@ class HTMLFormRenderer(BaseRenderer):
'base_template': 'input.html',
'input_type': 'time'
},
+ serializers.FileField: {
+ 'base_template': 'input.html',
+ 'input_type': 'file'
+ },
serializers.BooleanField: {
'base_template': 'checkbox.html'
},
@@ -522,6 +534,8 @@ class BrowsableAPIRenderer(BaseRenderer):
serializer = getattr(data, 'serializer', None)
if serializer and not getattr(serializer, 'many', False):
instance = getattr(serializer, 'instance', None)
+ if isinstance(instance, Page):
+ instance = None
else:
instance = None
@@ -580,6 +594,8 @@ class BrowsableAPIRenderer(BaseRenderer):
serializer = getattr(data, 'serializer', None)
if serializer and not getattr(serializer, 'many', False):
instance = getattr(serializer, 'instance', None)
+ if isinstance(instance, Page):
+ instance = None
else:
instance = None
diff --git a/rest_framework/request.py b/rest_framework/request.py
index d7e74674..20e049ed 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -14,9 +14,9 @@ from django.http import QueryDict
from django.http.multipartparser import parse_header
from django.utils.datastructures import MultiValueDict
from django.utils.datastructures import MergeDict as DjangoMergeDict
+from django.utils.six import BytesIO
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions
-from rest_framework.compat import BytesIO
from rest_framework.settings import api_settings
import warnings
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d417ca80..b0c0efa7 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -10,17 +10,13 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
-from django.core.exceptions import ImproperlyConfigured
-from django.core.exceptions import ValidationError as DjangoValidationError
+import warnings
+
from django.db import models
from django.db.models.fields import FieldDoesNotExist
-from django.utils import six
from django.utils.translation import ugettext_lazy as _
-from rest_framework.compat import OrderedDict
-from rest_framework.exceptions import ValidationError
-from rest_framework.fields import empty, set_value, Field, SkipField
-from rest_framework.settings import api_settings
-from rest_framework.utils import html, model_meta, representation
+
+from rest_framework.utils import model_meta
from rest_framework.utils.field_mapping import (
get_url_kwargs, get_field_kwargs,
get_relation_kwargs, get_nested_relation_kwargs,
@@ -33,9 +29,7 @@ from rest_framework.validators import (
UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
UniqueTogetherValidator
)
-import copy
-import inspect
-import warnings
+
# Note: We do the following so that users of the framework can use this style:
#
@@ -65,6 +59,7 @@ class BaseSerializer(Field):
The BaseSerializer class provides a minimal class which may be used
for writing custom serializer implementations.
"""
+
def __init__(self, instance=None, data=None, **kwargs):
self.instance = instance
self._initial_data = data
@@ -234,6 +229,35 @@ class SerializerMetaclass(type):
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
+def get_validation_error_detail(exc):
+ assert isinstance(exc, (ValidationError, DjangoValidationError))
+
+ if isinstance(exc, DjangoValidationError):
+ # Normally you should raise `serializers.ValidationError`
+ # inside your codebase, but we handle Django's validation
+ # exception class as well for simpler compat.
+ # Eg. Calling Model.clean() explicitly inside Serializer.validate()
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
+ }
+ elif isinstance(exc.detail, dict):
+ # If errors may be a dict we use the standard {key: list of values}.
+ # Here we ensure that all the values are *lists* of errors.
+ return dict([
+ (key, value if isinstance(value, list) else [value])
+ for key, value in exc.detail.items()
+ ])
+ elif isinstance(exc.detail, list):
+ # Errors raised as a list are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: exc.detail
+ }
+ # Errors raised as a string are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
+ }
+
+
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
default_error_messages = {
@@ -245,7 +269,7 @@ class Serializer(BaseSerializer):
"""
A dictionary of {field_name: field_instance}.
"""
- # `fields` is evalutated lazily. We do this to ensure that we don't
+ # `fields` is evaluated lazily. We do this to ensure that we don't
# have issues importing modules that use ModelSerializers as fields,
# even if Django's app-loading stage has not yet run.
if not hasattr(self, '_fields'):
@@ -298,55 +322,17 @@ class Serializer(BaseSerializer):
performed by validators and the `.validate()` method should
be coerced into an error dictionary with a 'non_fields_error' key.
"""
- if data is empty:
- if getattr(self.root, 'partial', False):
- raise SkipField()
- if self.required:
- self.fail('required')
- return self.get_default()
-
- if data is None:
- if not self.allow_null:
- self.fail('null')
- return None
-
- if not isinstance(data, dict):
- message = self.error_messages['invalid'].format(
- datatype=type(data).__name__
- )
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: [message]
- })
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
value = self.to_internal_value(data)
try:
self.run_validators(value)
value = self.validate(value)
assert value is not None, '.validate() should return the validated data'
- except ValidationError as exc:
- if isinstance(exc.detail, dict):
- # .validate() errors may be a dict, in which case, use
- # standard {key: list of values} style.
- raise ValidationError(dict([
- (key, value if isinstance(value, list) else [value])
- for key, value in exc.detail.items()
- ]))
- elif isinstance(exc.detail, list):
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: exc.detail
- })
- else:
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
- })
- except DjangoValidationError as exc:
- # Normally you should raise `serializers.ValidationError`
- # inside your codebase, but we handle Django's validation
- # exception class as well for simpler compat.
- # Eg. Calling Model.clean() explictily inside Serializer.validate()
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
- })
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
return value
@@ -354,6 +340,14 @@ class Serializer(BaseSerializer):
"""
Dict of native values <- Dict of primitive datatypes.
"""
+ if not isinstance(data, dict):
+ message = self.error_messages['invalid'].format(
+ datatype=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ })
+
ret = OrderedDict()
errors = OrderedDict()
fields = [
@@ -467,6 +461,26 @@ class ListSerializer(BaseSerializer):
return html.parse_html_list(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
+ def run_validation(self, data=empty):
+ """
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
+
+ value = self.to_internal_value(data)
+ try:
+ self.run_validators(value)
+ value = self.validate(value)
+ assert value is not None, '.validate() should return the validated data'
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
+
+ return value
+
def to_internal_value(self, data):
"""
List of dicts of native values <- List of dicts of primitive datatypes.
@@ -508,6 +522,9 @@ class ListSerializer(BaseSerializer):
self.child.to_representation(item) for item in iterable
]
+ def validate(self, attrs):
+ return attrs
+
def update(self, instance, validated_data):
raise NotImplementedError(
"Serializers with many=True do not support multiple update by "
@@ -566,6 +583,64 @@ class ListSerializer(BaseSerializer):
# ModelSerializer & HyperlinkedModelSerializer
# --------------------------------------------
+def raise_errors_on_nested_writes(method_name, serializer, validated_data):
+ """
+ Give explicit errors when users attempt to pass writable nested data.
+
+ If we don't do this explicitly they'd get a less helpful error when
+ calling `.save()` on the serializer.
+
+ We don't *automatically* support these sorts of nested writes brecause
+ there are too many ambiguities to define a default behavior.
+
+ Eg. Suppose we have a `UserSerializer` with a nested profile. How should
+ we handle the case of an update, where the `profile` realtionship does
+ not exist? Any of the following might be valid:
+
+ * Raise an application error.
+ * Silently ignore the nested part of the update.
+ * Automatically create a profile instance.
+ """
+
+ # Ensure we don't have a writable nested field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # profile = ProfileSerializer()
+ assert not any(
+ isinstance(field, BaseSerializer) and (key in validated_data)
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable nested'
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'nested serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
+
+ # Ensure we don't have a writable dotted-source field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # address = serializer.CharField('profile.address')
+ assert not any(
+ '.' in field.source and (key in validated_data)
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable dotted-source '
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'dotted-source serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
+
+
class ModelSerializer(Serializer):
"""
A `ModelSerializer` is just a regular `Serializer`, except that:
@@ -576,7 +651,7 @@ class ModelSerializer(Serializer):
The process of automatically determining a set of serializer fields
based on the model fields is reasonably complex, but you almost certainly
- don't need to dig into the implemention.
+ don't need to dig into the implementation.
If the `ModelSerializer` class *doesn't* generate the set of fields that
you need you should either declare the extra/differing fields explicitly on
@@ -608,20 +683,20 @@ class ModelSerializer(Serializer):
})
_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
descriptive messages when something goes wrong, but this method is
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
cannot be set until the model is instantiated, in which case the
implementation is like so:
- example_relationship = validated_attrs.pop('example_relationship')
- instance = ExampleModel.objects.create(**validated_attrs)
+ example_relationship = validated_data.pop('example_relationship')
+ instance = ExampleModel.objects.create(**validated_data)
instance.example_relationship = example_relationship
return instance
@@ -629,32 +704,21 @@ class ModelSerializer(Serializer):
If you want to support writable nested relationships you'll need
to write an explicit `.create()` method.
"""
- # Check that the user isn't trying to handle a writable nested field.
- # If we don't do this explicitly they'd likely get a confusing
- # error at the point of calling `Model.objects.create()`.
- assert not any(
- isinstance(field, BaseSerializer) and not field.read_only
- for field in self.fields.values()
- ), (
- 'The `.create()` method does not suport nested writable fields '
- 'by default. Write an explicit `.create()` method for serializer '
- '`%s.%s`, or set `read_only=True` on nested serializer fields.' %
- (self.__class__.__module__, self.__class__.__name__)
- )
+ raise_errors_on_nested_writes('create', self, validated_data)
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,
# as they require that the instance has already been saved.
info = model_meta.get_field_info(ModelClass)
many_to_many = {}
for field_name, relation_info in info.relations.items():
- if relation_info.to_many and (field_name in validated_attrs):
- many_to_many[field_name] = validated_attrs.pop(field_name)
+ if relation_info.to_many and (field_name in validated_data):
+ many_to_many[field_name] = validated_data.pop(field_name)
try:
- instance = ModelClass.objects.create(**validated_attrs)
+ instance = ModelClass.objects.create(**validated_data)
except TypeError as exc:
msg = (
'Got a `TypeError` when calling `%s.objects.create()`. '
@@ -679,20 +743,13 @@ class ModelSerializer(Serializer):
return instance
- def update(self, instance, validated_attrs):
- assert not any(
- isinstance(field, BaseSerializer) and not field.read_only
- for field in self.fields.values()
- ), (
- 'The `.update()` method does not suport nested writable fields '
- 'by default. Write an explicit `.update()` method for serializer '
- '`%s.%s`, or set `read_only=True` on nested serializer fields.' %
- (self.__class__.__module__, self.__class__.__name__)
- )
+ def update(self, instance, validated_data):
+ raise_errors_on_nested_writes('update', self, validated_data)
- for attr, value in validated_attrs.items():
+ for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
+
return instance
def get_validators(self):
@@ -759,6 +816,18 @@ class ModelSerializer(Serializer):
depth = getattr(self.Meta, 'depth', 0)
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
+ if fields and not isinstance(fields, (list, tuple)):
+ raise TypeError(
+ 'The `fields` option must be a list or tuple. Got %s.' %
+ type(fields).__name__
+ )
+
+ if exclude and not isinstance(exclude, (list, tuple)):
+ raise TypeError(
+ 'The `exclude` option must be a list or tuple. Got %s.' %
+ type(exclude).__name__
+ )
+
assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'."
extra_kwargs = self._include_additional_options(extra_kwargs)
@@ -824,7 +893,7 @@ class ModelSerializer(Serializer):
# applied, we can add the extra 'required=...' or 'default=...'
# arguments that are appropriate to these fields, or add a `HiddenField` for it.
for unique_constraint_name in unique_constraint_names:
- # Get the model field that is refered too.
+ # Get the model field that is referred too.
unique_constraint_field = model._meta.get_field(unique_constraint_name)
if getattr(unique_constraint_field, 'auto_now_add', None):
@@ -873,7 +942,7 @@ class ModelSerializer(Serializer):
# `ModelField`, which is used when no other typed field
# matched to the model field.
kwargs.pop('model_field', None)
- if not issubclass(field_cls, CharField):
+ if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
# `allow_blank` is only valid for textual fields.
kwargs.pop('allow_blank', None)
@@ -907,7 +976,7 @@ class ModelSerializer(Serializer):
)
# Check that any fields declared on the class are
- # also explicity included in `Meta.fields`.
+ # also explicitly included in `Meta.fields`.
missing_fields = set(declared_fields.keys()) - set(fields)
if missing_fields:
missing_field = list(missing_fields)[0]
@@ -1001,6 +1070,7 @@ class ModelSerializer(Serializer):
class Meta:
model = relation_info.related
depth = nested_depth
+
return NestedSerializer
@@ -1027,4 +1097,5 @@ class HyperlinkedModelSerializer(ModelSerializer):
class Meta:
model = relation_info.related
depth = nested_depth
+
return NestedSerializer
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 1e8c27fc..79da23ca 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -47,7 +47,7 @@ DEFAULTS = {
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
- # Genric view behavior
+ # Generic view behavior
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'rest_framework.pagination.PaginationSerializer',
'DEFAULT_FILTER_BACKENDS': (),
diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js
index bcb1964d..c8812132 100644
--- a/rest_framework/static/rest_framework/js/default.js
+++ b/rest_framework/static/rest_framework/js/default.js
@@ -24,7 +24,8 @@ prettyPrint();
// Bootstrap tooltips.
$('.js-tooltip').tooltip({
- delay: 1000
+ delay: 1000,
+ container: 'body'
});
// Deal with rounded tab styling after tab clicks.
diff --git a/rest_framework/templates/rest_framework/horizontal/select.html b/rest_framework/templates/rest_framework/horizontal/select.html
index 380b38e9..8a7fca37 100644
--- a/rest_framework/templates/rest_framework/horizontal/select.html
+++ b/rest_framework/templates/rest_framework/horizontal/select.html
@@ -4,7 +4,7 @@
{% endif %}
<div class="col-sm-10">
<select class="form-control" name="{{ field.name }}">
- {% if field.allow_null %}
+ {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/inline/select.html b/rest_framework/templates/rest_framework/inline/select.html
index 53af2772..6b30e4d6 100644
--- a/rest_framework/templates/rest_framework/inline/select.html
+++ b/rest_framework/templates/rest_framework/inline/select.html
@@ -3,7 +3,7 @@
<label class="sr-only">{{ field.label }}</label>
{% endif %}
<select class="form-control" name="{{ field.name }}">
- {% if field.allow_null %}
+ {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index e050cbdc..8e6240a6 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -21,11 +21,11 @@
{% csrf_token %}
<div id="div_id_username"
class="clearfix control-group {% if form.username.errors %}error{% endif %}">
- <div class="controls">
- <label class="span4">Username:</label>
- <input style="height: 25px" type="text" name="username" maxlength="100"
+ <div class="form-group">
+ <label for="id_username">Username:</label>
+ <input type="text" name="username" maxlength="100"
autocapitalize="off"
- autocorrect="off" class="span12 textinput textInput"
+ autocorrect="off" class="form-control textinput textInput"
id="id_username" required
{% if form.username.value %}value="{{ form.username.value }}"{% endif %}>
{% if form.username.errors %}
@@ -36,12 +36,11 @@
</div>
</div>
<div id="div_id_password"
- class="clearfix control-group {% if form.password.errors %}error{% endif %}"
- style="margin-top: 10px">
- <div class="controls">
- <label class="span4">Password:</label>
- <input style="height: 25px" type="password" name="password" maxlength="100"
- autocapitalize="off" autocorrect="off" class="span12 textinput textInput"
+ class="clearfix control-group {% if form.password.errors %}error{% endif %}">
+ <div class="form-group">
+ <label for="id_password">Password:</label>
+ <input type="password" name="password" maxlength="100"
+ autocapitalize="off" autocorrect="off" class="form-control textinput textInput"
id="id_password" required>
{% if form.password.errors %}
<p class="text-error">
@@ -56,8 +55,8 @@
<div class="well well-small text-error" style="border: none">{{ error }}</div>
{% endfor %}
{% endif %}
- <div class="form-actions-no-box" style="margin-top: 20px">
- <input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
+ <div class="form-actions-no-box">
+ <input type="submit" name="submit" value="Log in" class="btn btn-primary form-control" id="submit-id-submit">
</div>
</form>
</div>
diff --git a/rest_framework/templates/rest_framework/raw_data_form.html b/rest_framework/templates/rest_framework/raw_data_form.html
index 075279f7..b4c9f1a1 100644
--- a/rest_framework/templates/rest_framework/raw_data_form.html
+++ b/rest_framework/templates/rest_framework/raw_data_form.html
@@ -2,10 +2,10 @@
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
- <div class="control-group">
- {{ field.label_tag|add_class:"control-label" }}
- <div class="controls">
- {{ field }}
+ <div class="form-group">
+ {{ field.label_tag|add_class:"col-sm-2 control-label" }}
+ <div class="col-sm-10">
+ {{ field|add_class:"form-control" }}
<span class="help-block">{{ field.help_text }}</span>
</div>
</div>
diff --git a/rest_framework/templates/rest_framework/vertical/select.html b/rest_framework/templates/rest_framework/vertical/select.html
index de72e1dd..1d1109f6 100644
--- a/rest_framework/templates/rest_framework/vertical/select.html
+++ b/rest_framework/templates/rest_framework/vertical/select.html
@@ -3,7 +3,7 @@
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
{% endif %}
<select class="form-control" name="{{ field.name }}">
- {% if field.allow_null %}
+ {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index f1825a24..69e03af4 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -3,11 +3,11 @@ from django import template
from django.core.urlresolvers import reverse, NoReverseMatch
from django.http import QueryDict
from django.utils import six
-from django.utils.encoding import iri_to_uri
+from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.encoding import iri_to_uri, force_text
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe
from django.utils.html import smart_urlquote
-from rest_framework.compat import urlparse, force_text
from rest_framework.renderers import HTMLFormRenderer
import re
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 4d6bb3a3..adc83e57 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -4,8 +4,9 @@ Helper classes for parsers.
from __future__ import unicode_literals
from django.db.models.query import QuerySet
from django.utils import six, timezone
+from django.utils.encoding import force_text
from django.utils.functional import Promise
-from rest_framework.compat import force_text, OrderedDict
+from rest_framework.compat import OrderedDict
import datetime
import decimal
import types
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index 9c187176..86ceff31 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -91,18 +91,18 @@ def get_field_kwargs(field_name, model_field):
if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False
- if model_field.flatchoices:
- # If this model field contains choices, then return early.
- # Further keyword arguments are not valid.
- kwargs['choices'] = model_field.flatchoices
- return kwargs
-
if model_field.null and not isinstance(model_field, models.NullBooleanField):
kwargs['allow_null'] = True
if model_field.blank:
kwargs['allow_blank'] = True
+ if model_field.flatchoices:
+ # If this model field contains choices, then return early.
+ # Further keyword arguments are not valid.
+ kwargs['choices'] = model_field.flatchoices
+ return kwargs
+
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py
index 87b3cc6a..de2931c2 100644
--- a/rest_framework/utils/mediatypes.py
+++ b/rest_framework/utils/mediatypes.py
@@ -5,6 +5,7 @@ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
"""
from __future__ import unicode_literals
from django.http.multipartparser import parse_header
+from django.utils.encoding import python_2_unicode_compatible
from rest_framework import HTTP_HEADER_ENCODING
@@ -43,6 +44,7 @@ def order_by_precedence(media_type_lst):
return [media_types for media_types in ret if media_types]
+@python_2_unicode_compatible
class _MediaType(object):
def __init__(self, media_type_str):
if media_type_str is None:
@@ -79,9 +81,6 @@ class _MediaType(object):
return 3
def __str__(self):
- return self.__unicode__().encode('utf-8')
-
- def __unicode__(self):
ret = "%s/%s" % (self.main_type, self.sub_type)
for key, val in self.params.items():
ret += "; %s=%s" % (key, val)
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
index 2a7c4675..3f17a8b9 100644
--- a/rest_framework/utils/representation.py
+++ b/rest_framework/utils/representation.py
@@ -3,8 +3,8 @@ Helper functions for creating user-friendly representations
of serializer classes and serializer fields.
"""
from django.db import models
+from django.utils.encoding import force_text
from django.utils.functional import Promise
-from rest_framework.compat import force_text
import re
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index 7ca4e6a9..63eb7b22 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -4,7 +4,7 @@ the using Django's `.full_clean()`.
This gives us better separation of concerns, allows us to use single-step
object creation, and makes it possible to switch between using the implicit
-`ModelSerializer` class and an equivelent explicit `Serializer` class.
+`ModelSerializer` class and an equivalent explicit `Serializer` class.
"""
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 292431c8..bc870417 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -5,9 +5,10 @@ from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
from django.http import Http404
+from django.utils.encoding import smart_text
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
-from rest_framework.compat import smart_text, HttpResponseBase, View
+from rest_framework.compat import HttpResponseBase, View
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index 70d14695..88c763da 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -44,7 +44,7 @@ class ViewSetMixin(object):
instantiated view, we need to totally reimplement `.as_view`,
and slightly modify the view function that is created and returned.
"""
- # The suffix initkwarg is reserved for identifing the viewset type
+ # The suffix initkwarg is reserved for identifying the viewset type
# eg. 'List' or 'Instance'.
cls.suffix = None
@@ -98,12 +98,12 @@ class ViewSetMixin(object):
view.suffix = initkwargs.get('suffix', None)
return csrf_exempt(view)
- def initialize_request(self, request, *args, **kargs):
+ def initialize_request(self, request, *args, **kwargs):
"""
Set the `.action` attribute on the view,
depending on the request method.
"""
- request = super(ViewSetMixin, self).initialize_request(request, *args, **kargs)
+ request = super(ViewSetMixin, self).initialize_request(request, *args, **kwargs)
self.action = self.action_map.get(request.method.lower())
return request
diff --git a/runtests.py b/runtests.py
index 4da05ac3..abf15a62 100755
--- a/runtests.py
+++ b/runtests.py
@@ -17,24 +17,29 @@ FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501']
sys.path.append(os.path.dirname(__file__))
+
def exit_on_failure(ret, message=None):
if ret:
sys.exit(ret)
+
def flake8_main(args):
print('Running flake8 code linting')
ret = subprocess.call(['flake8'] + args)
print('flake8 failed' if ret else 'flake8 passed')
return ret
+
def split_class_and_function(string):
class_string, function_string = string.split('.', 1)
return "%s and %s" % (class_string, function_string)
+
def is_function(string):
# `True` if it looks like a test function is included in the string.
return string.startswith('test_') or '.test_' in string
+
def is_class(string):
# `True` if first character is uppercase - assume it's a class name.
return string[0] == string[0].upper()
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index 28c3a8b3..44837c4e 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -142,7 +142,7 @@ class SessionAuthTests(TestCase):
cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810)
"""
response = self.csrf_client.get('/auth/login/')
- self.assertContains(response, '<label class="span4">Username:</label>')
+ self.assertContains(response, '<label for="id_username">Username:</label>')
def test_post_form_session_auth_failing_csrf(self):
"""
diff --git a/tests/test_description.py b/tests/test_description.py
index 0675d209..78ce2350 100644
--- a/tests/test_description.py
+++ b/tests/test_description.py
@@ -2,7 +2,8 @@
from __future__ import unicode_literals
from django.test import TestCase
-from rest_framework.compat import apply_markdown, smart_text
+from django.utils.encoding import python_2_unicode_compatible, smart_text
+from rest_framework.compat import apply_markdown
from rest_framework.views import APIView
from .description import ViewWithNonASCIICharactersInDocstring
from .description import UTF8_TEST_DOCSTRING
@@ -107,6 +108,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
# use a mock object instead of gettext_lazy to ensure that we can't end
# up with a test case string in our l10n catalog
+ @python_2_unicode_compatible
class MockLazyStr(object):
def __init__(self, string):
self.s = string
@@ -114,9 +116,6 @@ class TestViewNamesAndDescriptions(TestCase):
def __str__(self):
return self.s
- def __unicode__(self):
- return self.s
-
class MockView(APIView):
__doc__ = MockLazyStr("a gettext string")
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 13525632..3f4e65f2 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -804,6 +804,21 @@ class TestChoiceField(FieldValues):
]
)
+ def test_allow_blank(self):
+ """
+ If `allow_blank=True` then '' is a valid input.
+ """
+ field = serializers.ChoiceField(
+ allow_blank=True,
+ choices=[
+ ('poor', 'Poor quality'),
+ ('medium', 'Medium quality'),
+ ('good', 'Good quality'),
+ ]
+ )
+ output = field.run_validation('')
+ assert output is ''
+
class TestChoiceFieldWithType(FieldValues):
"""
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index 1bcd58e0..da79164a 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -559,3 +559,53 @@ class TestBulkCreate(TestCase):
# Serializer returns correct data.
assert serializer.data == data
+
+
+class TestMetaClassModel(models.Model):
+ text = models.CharField(max_length=100)
+
+
+class TestSerializerMetaClass(TestCase):
+ def test_meta_class_fields_option(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ fields = 'text'
+
+ with self.assertRaises(TypeError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ assert str(exception).startswith(
+ "The `fields` option must be a list or tuple"
+ )
+
+ def test_meta_class_exclude_option(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ exclude = 'text'
+
+ with self.assertRaises(TypeError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ assert str(exception).startswith(
+ "The `exclude` option must be a list or tuple"
+ )
+
+ def test_meta_class_fields_and_exclude_options(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ fields = ('text',)
+ exclude = ('text',)
+
+ with self.assertRaises(AssertionError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ self.assertEqual(
+ str(exception),
+ "Cannot set both 'fields' and 'exclude'."
+ )
diff --git a/tests/test_multitable_inheritance.py b/tests/test_multitable_inheritance.py
index ce1bf3ea..e1b40cc7 100644
--- a/tests/test_multitable_inheritance.py
+++ b/tests/test_multitable_inheritance.py
@@ -31,7 +31,7 @@ class AssociatedModelSerializer(serializers.ModelSerializer):
# Tests
-class IneritedModelSerializationTests(TestCase):
+class InheritedModelSerializationTests(TestCase):
def test_multitable_inherited_model_fields_as_expected(self):
"""
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
index 88eccef3..d28d8bd4 100644
--- a/tests/test_parsers.py
+++ b/tests/test_parsers.py
@@ -5,8 +5,8 @@ from django import forms
from django.core.files.uploadhandler import MemoryFileUploadHandler
from django.test import TestCase
from django.utils import unittest
+from django.utils.six.moves import StringIO
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 XMLParser
diff --git a/tests/test_relations_generic.py b/tests/test_relations_generic.py
index 380ad91d..b600b333 100644
--- a/tests/test_relations_generic.py
+++ b/tests/test_relations_generic.py
@@ -3,8 +3,8 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
from django.db import models
from django.test import TestCase
+from django.utils.encoding import python_2_unicode_compatible
from rest_framework import serializers
-from rest_framework.compat import python_2_unicode_compatible
@python_2_unicode_compatible
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 416d7f22..00a24fb1 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -7,9 +7,11 @@ from django.core.cache import cache
from django.db import models
from django.test import TestCase
from django.utils import six, unittest
+from django.utils.six import BytesIO
+from django.utils.six.moves import StringIO
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions
-from rest_framework.compat import yaml, etree, StringIO, BytesIO
+from rest_framework.compat import yaml, etree
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
@@ -384,6 +386,15 @@ class UnicodeJSONRendererTests(TestCase):
content = renderer.render(obj, 'application/json')
self.assertEqual(content, '{"countries":["United Kingdom","France","España"]}'.encode('utf-8'))
+ def test_u2028_u2029(self):
+ # The \u2028 and \u2029 characters should be escaped,
+ # even when the non-escaping unicode representation is used.
+ # Regression test for #2169
+ obj = {'should_escape': '\u2028\u2029'}
+ renderer = JSONRenderer()
+ content = renderer.render(obj, 'application/json')
+ self.assertEqual(content, '{"should_escape":"\\u2028\\u2029"}'.encode('utf-8'))
+
class AsciiJSONRendererTests(TestCase):
"""
diff --git a/tests/test_request.py b/tests/test_request.py
index 44afd243..7cf8c327 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -187,7 +187,7 @@ class MockView(APIView):
if request.POST.get('example') is not None:
return Response(status=status.HTTP_200_OK)
- return Response(status=status.INTERNAL_SERVER_ERROR)
+ return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
urlpatterns = patterns(
'',
diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py
index 640067e3..35b68ae7 100644
--- a/tests/test_serializer_lists.py
+++ b/tests/test_serializer_lists.py
@@ -272,3 +272,19 @@ class TestNestedListOfListsSerializer:
serializer = self.Serializer(data=input_data)
assert serializer.is_valid()
assert serializer.validated_data == expected_output
+
+
+class TestListSerializerClass:
+ """Tests for a custom list_serializer_class."""
+ def test_list_serializer_class_validate(self):
+ class CustomListSerializer(serializers.ListSerializer):
+ def validate(self, attrs):
+ raise serializers.ValidationError('Non field error')
+
+ class TestSerializer(serializers.Serializer):
+ class Meta:
+ list_serializer_class = CustomListSerializer
+
+ serializer = TestSerializer(data=[], many=True)
+ assert not serializer.is_valid()
+ assert serializer.errors == {'non_field_errors': ['Non field error']}
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 9226cc7a..072cec36 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -168,7 +168,7 @@ class TestUniquenessTogetherValidation(TestCase):
def test_ignore_excluded_fields(self):
"""
When model fields are not included in a serializer, then uniqueness
- validtors should not be added for that field.
+ validators should not be added for that field.
"""
class ExcludedFieldSerializer(serializers.ModelSerializer):
class Meta:
diff --git a/tox.ini b/tox.ini
index d5cb9ef9..933ee560 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
envlist =
- py27-flake8,
+ py27-{flake8,docs},
{py26,py27}-django14,
{py26,py27,py32,py33,py34}-django{15,16},
{py27,py32,py33,py34}-django{17,master}
@@ -10,8 +10,8 @@ commands = ./runtests.py --fast
setenv =
PYTHONDONTWRITEBYTECODE=1
deps =
- django14: Django==1.4.16
- django15: Django==1.5.11
+ django14: Django==1.4.11
+ django15: Django==1.5.5
django16: Django==1.6.8
django17: Django==1.7.1
djangomaster: https://github.com/django/django/zipball/master